Spaces:
Sleeping
Sleeping
| import { useMemo, useState } from "react"; | |
| import { X, Table as TableIcon, FileText } from "lucide-react"; | |
| import type { EmbedDataFile } from "../editor/embeds/embed-data-store"; | |
| import { formatBytes } from "../utils/data-files"; | |
| interface DataFileViewerProps { | |
| file: EmbedDataFile; | |
| onClose: () => void; | |
| } | |
| const MAX_PREVIEW_ROWS = 100; | |
| function splitCsvLine(line: string, delim: string): string[] { | |
| const out: string[] = []; | |
| let cur = ""; | |
| let inQuotes = false; | |
| for (let i = 0; i < line.length; i++) { | |
| const c = line[i]; | |
| if (inQuotes) { | |
| if (c === '"') { | |
| if (line[i + 1] === '"') { | |
| cur += '"'; | |
| i++; | |
| } else { | |
| inQuotes = false; | |
| } | |
| } else { | |
| cur += c; | |
| } | |
| } else if (c === '"') { | |
| inQuotes = true; | |
| } else if (c === delim) { | |
| out.push(cur); | |
| cur = ""; | |
| } else { | |
| cur += c; | |
| } | |
| } | |
| out.push(cur); | |
| return out; | |
| } | |
| function parseTable(content: string, delim: string, maxRows: number): { | |
| columns: string[]; | |
| rows: string[][]; | |
| total: number; | |
| } { | |
| const lines = content.split(/\r\n|\n|\r/).filter((l) => l.length > 0); | |
| if (lines.length === 0) return { columns: [], rows: [], total: 0 }; | |
| const columns = splitCsvLine(lines[0], delim); | |
| const rows = lines | |
| .slice(1, 1 + maxRows) | |
| .map((l) => splitCsvLine(l, delim)); | |
| return { columns, rows, total: Math.max(0, lines.length - 1) }; | |
| } | |
| function prettyJson(content: string): string { | |
| try { | |
| return JSON.stringify(JSON.parse(content), null, 2); | |
| } catch { | |
| return content; | |
| } | |
| } | |
| export function DataFileViewer({ file, onClose }: DataFileViewerProps) { | |
| const { ext } = file.meta; | |
| const isTabular = ext === "csv" || ext === "tsv"; | |
| const [mode, setMode] = useState<"table" | "raw">(isTabular ? "table" : "raw"); | |
| const parsed = useMemo(() => { | |
| if (!isTabular) return null; | |
| return parseTable(file.content, ext === "tsv" ? "\t" : ",", MAX_PREVIEW_ROWS); | |
| }, [file.content, ext, isTabular]); | |
| const raw = useMemo(() => { | |
| if (ext === "json") return prettyJson(file.content); | |
| return file.content; | |
| }, [file.content, ext]); | |
| return ( | |
| <div className="es-data-viewer"> | |
| <div className="es-data-viewer__header"> | |
| <div className="es-data-viewer__title"> | |
| <strong>{file.meta.name}</strong> | |
| <span className="es-data-viewer__meta"> | |
| {ext.toUpperCase()} - {formatBytes(file.meta.size)} | |
| {file.meta.rowCount !== undefined ? ` - ${file.meta.rowCount} rows` : ""} | |
| </span> | |
| </div> | |
| <div className="es-data-viewer__actions"> | |
| {isTabular && ( | |
| <div className="es-data-viewer__tabs"> | |
| <button | |
| type="button" | |
| className={`es-preview__tab ${mode === "table" ? "es-preview__tab--active" : ""}`} | |
| onClick={() => setMode("table")} | |
| > | |
| <TableIcon size={12} /> | |
| Table | |
| </button> | |
| <button | |
| type="button" | |
| className={`es-preview__tab ${mode === "raw" ? "es-preview__tab--active" : ""}`} | |
| onClick={() => setMode("raw")} | |
| > | |
| <FileText size={12} /> | |
| Raw | |
| </button> | |
| </div> | |
| )} | |
| <button | |
| type="button" | |
| className="embed-btn" | |
| onClick={onClose} | |
| aria-label="Close file viewer" | |
| > | |
| <X size={14} /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="es-data-viewer__body"> | |
| {isTabular && mode === "table" && parsed ? ( | |
| <div className="es-data-table-wrap"> | |
| <table className="es-data-table"> | |
| <thead> | |
| <tr> | |
| {parsed.columns.map((col, i) => ( | |
| <th key={i}>{col}</th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {parsed.rows.map((row, ri) => ( | |
| <tr key={ri}> | |
| {parsed.columns.map((_, ci) => ( | |
| <td key={ci}>{row[ci] ?? ""}</td> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| {parsed.total > parsed.rows.length && ( | |
| <div className="es-data-table__footer"> | |
| Showing first {parsed.rows.length} of {parsed.total} rows | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <pre className="es-data-viewer__raw"> | |
| <code>{raw}</code> | |
| </pre> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |