feat: add dataset preview dialog using /check-format endpoint

This commit is contained in:
imagineer99 2026-02-13 05:04:11 +00:00
commit bc9645cbbf
6 changed files with 421 additions and 2 deletions

View file

@ -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=="],

View file

@ -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",

View 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>
);
}

View file

@ -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);
}

View file

@ -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>
);
}

View file

@ -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: {