feat: add dataset preview dialog using /check-format endpoint
This commit is contained in:
parent
363ffb7d1a
commit
bc9645cbbf
6 changed files with 421 additions and 2 deletions
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
110
studio/frontend/src/components/ui/data-table.tsx
Normal file
110
studio/frontend/src/components/ui/data-table.tsx
Normal file
|
|
@ -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<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
className,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
state: { sorting },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="bg-muted/60 hover:bg-muted/60 border-b border-border/60"
|
||||
>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="border-r border-border/40 last:border-r-0 h-11 px-4 text-xs"
|
||||
style={{
|
||||
width:
|
||||
header.getSize() !== 150 ? header.getSize() : undefined,
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row, idx) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() ? "selected" : undefined}
|
||||
className={cn(
|
||||
"transition-colors border-b border-border/30",
|
||||
idx % 2 === 0
|
||||
? "bg-background"
|
||||
: "bg-muted/20",
|
||||
"hover:bg-primary/[0.03]",
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="border-r border-border/20 last:border-r-0 text-[13px] py-3 px-4 align-top whitespace-normal"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-32 text-center text-muted-foreground text-sm"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<string, string> | null;
|
||||
detected_image_column?: string | null;
|
||||
detected_text_column?: string | null;
|
||||
preview_samples?: Record<string, unknown>[] | 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<CheckFormatResponse> {
|
||||
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<CheckFormatResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<ColumnDef<Record<string, unknown>>[]>(() => {
|
||||
if (!columns.length) return [];
|
||||
return columns.map((colName) => ({
|
||||
accessorKey: colName,
|
||||
header: () => (
|
||||
<span className="font-heading text-[13px] font-semibold tracking-tight text-foreground">
|
||||
{colName}
|
||||
</span>
|
||||
),
|
||||
cell: ({ getValue }: { getValue: () => unknown }) => {
|
||||
const value = getValue();
|
||||
const text = formatCell(value);
|
||||
if (!text) {
|
||||
return (
|
||||
<span className="text-muted-foreground/40 italic text-[13px]">
|
||||
--
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const full =
|
||||
typeof value === "string" ? value : JSON.stringify(value);
|
||||
return (
|
||||
<p
|
||||
className="text-[13px] leading-relaxed line-clamp-6"
|
||||
title={full}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
}));
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="sm:max-w-5xl w-[90vw] max-h-[88vh] flex flex-col gap-0 p-0 overflow-hidden rounded-3xl corner-squircle"
|
||||
showCloseButton={true}
|
||||
>
|
||||
{/* Header */}
|
||||
<DialogHeader className="px-6 pt-5 pb-4 shrink-0">
|
||||
<div className="flex items-center gap-3 pr-10">
|
||||
<div className="rounded-xl corner-squircle p-2 ring-1 ring-indigo-200 bg-indigo-50 text-indigo-600 dark:ring-indigo-800 dark:bg-indigo-950 dark:text-indigo-400 shrink-0">
|
||||
<HugeiconsIcon icon={Database02Icon} className="size-4" />
|
||||
</div>
|
||||
<DialogTitle className="font-heading text-lg font-semibold tracking-tight">
|
||||
Dataset Preview
|
||||
</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex flex-col min-h-0 flex-1 overflow-hidden px-6 pb-6">
|
||||
{/* Loading */}
|
||||
{loading && (
|
||||
<div className="py-24 flex flex-col items-center justify-center gap-3">
|
||||
<div className="rounded-2xl corner-squircle bg-primary/5 p-4">
|
||||
<Spinner className="size-5 text-primary" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-medium">
|
||||
Loading preview...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="py-20 flex flex-col items-center justify-center gap-3">
|
||||
<div className="rounded-2xl corner-squircle bg-destructive/10 p-3">
|
||||
<HugeiconsIcon
|
||||
icon={AlertCircleIcon}
|
||||
className="size-5 text-destructive"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center space-y-1">
|
||||
<p className="text-sm font-medium text-destructive">{error}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Make sure the backend is running on port 8000.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{!loading && !error && data && (
|
||||
<>
|
||||
{/* Metadata card */}
|
||||
<div className="rounded-xl corner-squircle ring-1 ring-border/60 bg-muted/30 px-5 py-4 mb-4 space-y-2">
|
||||
<MetaRow label="Source" value={sourceLabel} />
|
||||
<MetaRow
|
||||
label="Format"
|
||||
value={data.detected_format || "--"}
|
||||
/>
|
||||
<MetaRow
|
||||
label="Total Rows"
|
||||
value={
|
||||
data.total_rows != null
|
||||
? data.total_rows.toLocaleString()
|
||||
: "--"
|
||||
}
|
||||
/>
|
||||
<MetaRow
|
||||
label="Columns"
|
||||
value={
|
||||
<span className="flex items-center gap-1.5 flex-wrap">
|
||||
{columns.map((col) => (
|
||||
<Badge
|
||||
key={col}
|
||||
variant="outline"
|
||||
className="text-[11px] font-mono h-5"
|
||||
>
|
||||
{col}
|
||||
</Badge>
|
||||
))}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Data table */}
|
||||
<div className="flex-1 min-h-0 rounded-xl corner-squircle ring-1 ring-border/60 overflow-auto">
|
||||
<DataTable columns={tableColumns} data={rows} />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-[11px] text-muted-foreground/60 mt-3 text-center tabular-nums">
|
||||
Showing {rows.length}
|
||||
{data.total_rows != null &&
|
||||
` of ${data.total_rows.toLocaleString()}`}{" "}
|
||||
rows
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function MetaRow({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-baseline gap-3 text-sm">
|
||||
<span className="text-muted-foreground font-medium text-xs w-24 shrink-0">
|
||||
{label}:
|
||||
</span>
|
||||
<span className="text-foreground text-[13px] min-w-0">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer gap-1.5 text-muted-foreground"
|
||||
className="cursor-pointer gap-1.5"
|
||||
disabled={!dataset}
|
||||
onClick={() => setPreviewOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={ViewIcon} className="size-3.5" />
|
||||
Preview
|
||||
View dataset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DatasetPreviewDialog
|
||||
open={previewOpen}
|
||||
onOpenChange={setPreviewOpen}
|
||||
datasetName={dataset}
|
||||
hfToken={hfToken}
|
||||
/>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue