From bc9645cbbfe02e2fd760ad34999b27daeaac556f Mon Sep 17 00:00:00 2001 From: imagineer99 Date: Fri, 13 Feb 2026 05:04:11 +0000 Subject: [PATCH] feat: add dataset preview dialog using /check-format endpoint --- studio/frontend/bun.lock | 5 + studio/frontend/package.json | 1 + .../frontend/src/components/ui/data-table.tsx | 110 +++++++ .../sections/dataset-preview-dialog.tsx | 287 ++++++++++++++++++ .../studio/sections/dataset-section.tsx | 14 +- studio/frontend/vite.config.ts | 6 + 6 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 studio/frontend/src/components/ui/data-table.tsx create mode 100644 studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx diff --git a/studio/frontend/bun.lock b/studio/frontend/bun.lock index f1c02b63fc..f438f4e3ea 100644 --- a/studio/frontend/bun.lock +++ b/studio/frontend/bun.lock @@ -26,6 +26,7 @@ "@streamdown/mermaid": "^1.0.1", "@tailwindcss/vite": "^4.1.17", "@tanstack/react-router": "^1.156.0", + "@tanstack/react-table": "^8.21.3", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", @@ -656,10 +657,14 @@ "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + "@tanstack/router-core": ["@tanstack/router-core@1.156.0", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-v4/ecOxEHn9Wd9xvDX5sgHVK9NOQCKYg3VSx3xPwEkJL9mV20/raYj8uY9n3+uSCpE6TpSsak0br9Nw64if85w=="], "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@toolwind/corner-shape": ["@toolwind/corner-shape@0.0.8-3", "", { "dependencies": { "@types/node": "^20.4.1" } }, "sha512-MPIF81F2bhtXbzEeXF0vnL+PKpnopCHOzBspOkK8osMzWQvPUujZn2XZOMdsu4DF6wsVbbRYQtdsJr486HmIPQ=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], diff --git a/studio/frontend/package.json b/studio/frontend/package.json index e968f6ee18..e7e10695b5 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -34,6 +34,7 @@ "@streamdown/mermaid": "^1.0.1", "@tailwindcss/vite": "^4.1.17", "@tanstack/react-router": "^1.156.0", + "@tanstack/react-table": "^8.21.3", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", diff --git a/studio/frontend/src/components/ui/data-table.tsx b/studio/frontend/src/components/ui/data-table.tsx new file mode 100644 index 0000000000..954d2ae0b7 --- /dev/null +++ b/studio/frontend/src/components/ui/data-table.tsx @@ -0,0 +1,110 @@ +import { + type ColumnDef, + type SortingState, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useState } from "react"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/utils"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + className?: string; +} + +export function DataTable({ + columns, + data, + className, +}: DataTableProps) { + const [sorting, setSorting] = useState([]); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + state: { sorting }, + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row, idx) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx new file mode 100644 index 0000000000..e9d70846be --- /dev/null +++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx @@ -0,0 +1,287 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { DataTable } from "@/components/ui/data-table"; +import { Badge } from "@/components/ui/badge"; +import { Spinner } from "@/components/ui/spinner"; +import { Database02Icon, AlertCircleIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { type ReactNode, useEffect, useMemo, useState } from "react"; + +// --------------------------------------------------------------------------- +// Types (matches CheckFormatResponse from backend) +// --------------------------------------------------------------------------- + +type CheckFormatResponse = { + requires_manual_mapping: boolean; + detected_format: string; + columns: string[]; + suggested_mapping?: Record | null; + detected_image_column?: string | null; + detected_text_column?: string | null; + preview_samples?: Record[] | null; + total_rows?: number | null; +}; + +type DatasetPreviewDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + datasetName: string | null; + hfToken: string | null; +}; + +// --------------------------------------------------------------------------- +// API -- uses existing /check-format endpoint +// --------------------------------------------------------------------------- + +async function fetchCheckFormat( + datasetName: string, + hfToken: string | null, +): Promise { + const res = await fetch("/api/datasets/check-format", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + dataset_name: datasetName, + hf_token: hfToken || undefined, + split: "train", + }), + }); + if (!res.ok) { + const body = await res.json().catch(() => null); + throw new Error(body?.detail || `Request failed (${res.status})`); + } + return res.json(); +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export function DatasetPreviewDialog({ + open, + onOpenChange, + datasetName, + hfToken, +}: DatasetPreviewDialogProps) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open || !datasetName) { + setData(null); + setError(null); + return; + } + let cancelled = false; + setLoading(true); + setError(null); + + fetchCheckFormat(datasetName, hfToken) + .then((res) => { + if (!cancelled) { + setData(res); + setError(null); + } + }) + .catch((err) => { + if (!cancelled) setError(err.message || "Failed to load preview"); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [open, datasetName, hfToken]); + + const rows = data?.preview_samples ?? []; + const columns = data?.columns ?? []; + + // Determine source label + const sourceLabel = useMemo(() => { + if (!datasetName) return ""; + if (datasetName.includes("/")) return `Hugging Face (${datasetName})`; + return `Local Files (${datasetName})`; + }, [datasetName]); + + // Build TanStack Table columns from the column names + const tableColumns = useMemo>[]>(() => { + if (!columns.length) return []; + return columns.map((colName) => ({ + accessorKey: colName, + header: () => ( + + {colName} + + ), + cell: ({ getValue }: { getValue: () => unknown }) => { + const value = getValue(); + const text = formatCell(value); + if (!text) { + return ( + + -- + + ); + } + const full = + typeof value === "string" ? value : JSON.stringify(value); + return ( +

+ {text} +

+ ); + }, + })); + }, [columns]); + + return ( + + + {/* Header */} + +
+
+ +
+ + Dataset Preview + +
+
+ + {/* Body */} +
+ {/* Loading */} + {loading && ( +
+
+ +
+

+ Loading preview... +

+
+ )} + + {/* Error */} + {error && ( +
+
+ +
+
+

{error}

+

+ Make sure the backend is running on port 8000. +

+
+
+ )} + + {/* Content */} + {!loading && !error && data && ( + <> + {/* Metadata card */} +
+ + + + + {columns.map((col) => ( + + {col} + + ))} + + } + /> +
+ + {/* Data table */} +
+ +
+ + {/* Footer */} +

+ Showing {rows.length} + {data.total_rows != null && + ` of ${data.total_rows.toLocaleString()}`}{" "} + rows +

+ + )} +
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Metadata row +// --------------------------------------------------------------------------- + +function MetaRow({ + label, + value, +}: { + label: string; + value: ReactNode; +}) { + return ( +
+ + {label}: + + {value} +
+ ); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function formatCell(value: unknown): string { + if (value == null) return ""; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") + return String(value); + if (Array.isArray(value) || typeof value === "object") + return JSON.stringify(value).slice(0, 500); + return String(value); +} diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index 6fd4d127c1..abcc81f621 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -40,6 +40,7 @@ import { import { HugeiconsIcon } from "@hugeicons/react"; import { useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; +import { DatasetPreviewDialog } from "./dataset-preview-dialog"; export function DatasetSection() { const { dataset, setDataset, datasetFormat, setDatasetFormat, hfToken } = @@ -62,6 +63,7 @@ export function DatasetSection() { ); const [inputValue, setInputValue] = useState(""); + const [previewOpen, setPreviewOpen] = useState(false); const selectingRef = useRef(false); const debouncedQuery = useDebouncedValue(inputValue); @@ -305,13 +307,21 @@ export function DatasetSection() { + ); } diff --git a/studio/frontend/vite.config.ts b/studio/frontend/vite.config.ts index a943e95e0b..84565f26d1 100644 --- a/studio/frontend/vite.config.ts +++ b/studio/frontend/vite.config.ts @@ -8,6 +8,12 @@ export default defineConfig({ plugins: [react(), tailwindcss()], server: { allowedHosts: ["playground.wasimhub.dev"], + proxy: { + "/api": { + target: "http://127.0.0.1:8000", + changeOrigin: true, + }, + }, }, resolve: { alias: {