import { useState, useEffect, useCallback, useRef } from "react"; import { HF_ORG } from "../../../config"; const PAGE_SIZE = 100; const CELL_TRUNCATE_LEN = 200; interface HfRow { row_idx: number; row: Record; } interface HfFeature { feature_idx: number; name: string; type: Record; } interface HfResponse { rows: HfRow[]; features: HfFeature[]; num_rows_total: number; } interface TableViewerProps { datasetRepo: string; split?: string; onClose: () => void; } // ─── Cell Expansion Modal ───────────────────────────────────────────────────── interface CellModalProps { value: string; colName: string; onClose: () => void; } function CellModal({ value, colName, onClose }: CellModalProps) { const [copied, setCopied] = useState(false); const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // fallback: do nothing } }, [value]); // Close on Escape useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} >
{/* Header */}
{colName}
{/* Full value — scrollable, monospace */}
            {value}
          
{/* Footer with char count */}
{value.length.toLocaleString()} characters — complete, untruncated
); } // ─── Table Cell ─────────────────────────────────────────────────────────────── interface CellProps { value: unknown; colName: string; onExpand: (value: string, colName: string) => void; } function TableCell({ value, colName, onExpand }: CellProps) { const str = value === null || value === undefined ? "" : typeof value === "object" ? JSON.stringify(value, null, 2) : String(value); const isTruncated = str.length > CELL_TRUNCATE_LEN; const display = isTruncated ? str.slice(0, CELL_TRUNCATE_LEN) + "..." : str; return ( {display} {isTruncated && ( )} ); } // ─── Sort icon ──────────────────────────────────────────────────────────────── function SortIcon({ dir }: { dir: "asc" | "desc" | null }) { if (dir === null) return ; if (dir === "asc") return ; return ; } // ─── Main Component ─────────────────────────────────────────────────────────── export default function TableViewer({ datasetRepo, split = "train", onClose }: TableViewerProps) { // Ensure dataset repo has org prefix for HF API calls const fullRepo = datasetRepo.includes("/") ? datasetRepo : `${HF_ORG}/${datasetRepo}`; const [rows, setRows] = useState([]); const [columns, setColumns] = useState([]); const [totalRows, setTotalRows] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [page, setPage] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [sortCol, setSortCol] = useState(null); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const [expandedCell, setExpandedCell] = useState<{ value: string; colName: string } | null>(null); const fetchRef = useRef(0); const fetchRows = useCallback(async (pageIndex: number) => { setLoading(true); setError(null); const fetchId = ++fetchRef.current; const offset = pageIndex * PAGE_SIZE; const baseUrl = "https://datasets-server.huggingface.co/rows"; const urlWithConfig = `${baseUrl}?dataset=${encodeURIComponent(fullRepo)}&config=default&split=${split}&offset=${offset}&length=${PAGE_SIZE}`; const urlWithoutConfig = `${baseUrl}?dataset=${encodeURIComponent(fullRepo)}&split=${split}&offset=${offset}&length=${PAGE_SIZE}`; let data: HfResponse | null = null; try { const res = await fetch(urlWithConfig); if (res.ok) { data = await res.json() as HfResponse; } else { // Try without config param const res2 = await fetch(urlWithoutConfig); if (res2.ok) { data = await res2.json() as HfResponse; } else { const errText = await res2.text(); throw new Error(`API error ${res2.status}: ${errText.slice(0, 200)}`); } } } catch (e) { if (fetchRef.current === fetchId) { setError(e instanceof Error ? e.message : "Failed to fetch dataset rows"); setLoading(false); } return; } if (fetchRef.current !== fetchId) return; if (data) { const cols = data.features.map((f) => f.name); setColumns(cols); setRows(data.rows); setTotalRows(data.num_rows_total); } setLoading(false); }, [fullRepo, split]); useEffect(() => { fetchRows(page); }, [fetchRows, page]); // ── Derived: search + sort applied to currently loaded page ── const filteredRows = (() => { let result = rows; if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); result = result.filter((r) => columns.some((col) => { const v = r.row[col]; if (v === null || v === undefined) return false; return String(typeof v === "object" ? JSON.stringify(v) : v).toLowerCase().includes(q); }) ); } if (sortCol) { result = [...result].sort((a, b) => { const av = a.row[sortCol]; const bv = b.row[sortCol]; const as = av === null || av === undefined ? "" : typeof av === "object" ? JSON.stringify(av) : String(av); const bs = bv === null || bv === undefined ? "" : typeof bv === "object" ? JSON.stringify(bv) : String(bv); // Try numeric sort first const an = Number(as); const bn = Number(bs); if (!isNaN(an) && !isNaN(bn)) { return sortDir === "asc" ? an - bn : bn - an; } return sortDir === "asc" ? as.localeCompare(bs) : bs.localeCompare(as); }); } return result; })(); const handleSort = (col: string) => { if (sortCol === col) { if (sortDir === "asc") { setSortDir("desc"); } else { setSortCol(null); setSortDir("asc"); } } else { setSortCol(col); setSortDir("asc"); } }; const handleExpand = (value: string, colName: string) => { setExpandedCell({ value, colName }); }; const startRow = page * PAGE_SIZE + 1; const endRow = totalRows !== null ? Math.min((page + 1) * PAGE_SIZE, totalRows) : (page + 1) * PAGE_SIZE; const hasPrev = page > 0; const hasNext = totalRows !== null ? (page + 1) * PAGE_SIZE < totalRows : rows.length === PAGE_SIZE; const shortName = datasetRepo.split("/").pop() ?? datasetRepo; return ( <> {/* Full-screen overlay */}
{/* ── Top bar ────────────────────────────────────────────────── */}
Table Viewer · {shortName} ({datasetRepo.split("/")[0]}) {totalRows !== null && ( {totalRows.toLocaleString()} rows )}
{/* Search */} setSearchQuery(e.target.value)} className="text-xs bg-gray-800 border border-gray-700 rounded px-2.5 py-1.5 text-gray-200 placeholder-gray-500 focus:outline-none focus:border-cyan-600 w-48 sm:w-64" /> {/* Close */}
{/* ── Body ─────────────────────────────────────────────────────── */}
{loading && (
Loading rows…
)} {error && !loading && (

{error}

)} {!loading && !error && filteredRows.length === 0 && (

{searchQuery.trim() ? "No rows match your search." : "No rows returned."}

)} {!loading && !error && filteredRows.length > 0 && ( {/* Row index column */} {columns.map((col) => ( ))} {filteredRows.map((r) => ( {columns.map((col) => ( ))} ))}
# handleSort(col)} className="px-3 py-2.5 text-xs font-medium text-gray-300 border-b border-gray-700 cursor-pointer select-none hover:text-gray-100 hover:bg-gray-800 transition-colors whitespace-nowrap bg-gray-900" > {col}
{r.row_idx}
)}
{/* ── Pagination bar ───────────────────────────────────────────── */} {!loading && !error && totalRows !== null && (
{searchQuery.trim() ? `${filteredRows.length} matching rows on this page` : `Rows ${startRow}–${endRow} of ${totalRows.toLocaleString()}`}
Page {page + 1} / {Math.ceil(totalRows / PAGE_SIZE)}
)}
{/* ── Cell Expansion Modal ─────────────────────────────────────── */} {expandedCell && ( setExpandedCell(null)} /> )} ); }