tfrere's picture
tfrere HF Staff
feat(editor): embed studio with data files and agent-aware editing
8fc8501
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>
);
}