"use client"; import { useState, useEffect } from "react"; import { read, utils, type WorkBook } from "xlsx"; const SPREADSHEET_EXTENSIONS = new Set([ "xlsx", "xls", "xlsb", "xlsm", "xltx", "xltm", "ods", "fods", "csv", "tsv", "numbers", ]); export function isSpreadsheetFile(filename: string): boolean { const ext = filename.split(".").pop()?.toLowerCase() ?? ""; return SPREADSHEET_EXTENSIONS.has(ext); } type FileViewerProps = | { content: string; filename: string; type: "yaml" | "text" } | { filename: string; type: "spreadsheet"; url: string; content?: never }; export function FileViewer(props: FileViewerProps) { if (props.type === "spreadsheet") { return ; } const { content, filename, type } = props; const lines = content.split("\n"); return (
          
            {lines.map((line, idx) => (
              
{idx + 1} {type === "yaml" ? ( ) : ( line || " " )}
))}
); } function FileHeader({ filename, label, icon }: { filename: string; label: string; icon?: React.ReactNode }) { return (
{icon ?? ( )} {filename} {label}
); } // --------------------------------------------------------------------------- // Spreadsheet viewer // --------------------------------------------------------------------------- function SpreadsheetViewer({ filename, url }: { filename: string; url: string }) { const [workbook, setWorkbook] = useState(null); const [activeSheet, setActiveSheet] = useState(0); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setWorkbook(null); setActiveSheet(0); setError(null); fetch(url) .then((res) => { if (!res.ok) {throw new Error(`Failed to load file (${res.status})`);} return res.arrayBuffer(); }) .then((buf) => { if (cancelled) {return;} const wb = read(buf, { type: "array" }); setWorkbook(wb); }) .catch((err) => { if (!cancelled) {setError(String(err));} }); return () => { cancelled = true; }; }, [url]); const ext = filename.split(".").pop()?.toUpperCase() ?? "SPREADSHEET"; if (error) { return (
} />
Failed to load spreadsheet: {error}
); } if (!workbook) { return (
} />
Loading spreadsheet...
); } const sheetNames = workbook.SheetNames; const sheet = workbook.Sheets[sheetNames[activeSheet]]; const rows: string[][] = sheet ? utils.sheet_to_json(sheet, { header: 1, defval: "" }) : []; return (
} /> {/* Sheet tabs */} {sheetNames.length > 1 && (
{sheetNames.map((name, idx) => ( ))}
)} {/* Table */}
{rows.length === 0 ? (
This sheet is empty.
) : ( {/* Row number header */} ))} {rows.map((row, rowIdx) => ( {row.map((cell, colIdx) => ( ))} ))}
{rows[0]?.map((_cell, colIdx) => ( {columnLabel(colIdx)}
{rowIdx + 1} {String(cell)}
)}
{rows.length} row{rows.length !== 1 ? "s" : ""} {rows[0] ? ` \u00d7 ${rows[0].length} column${rows[0].length !== 1 ? "s" : ""}` : ""} {sheetNames.length > 1 ? ` \u00b7 ${sheetNames.length} sheets` : ""}
); } /** Convert zero-based column index to Excel-style label (A, B, ..., Z, AA, AB, ...) */ function columnLabel(idx: number): string { let label = ""; let n = idx; do { label = String.fromCharCode(65 + (n % 26)) + label; n = Math.floor(n / 26) - 1; } while (n >= 0); return label; } function SpreadsheetIcon() { return ( ); } /** Simple YAML syntax highlighting */ function YamlLine({ line }: { line: string }) { // Comment if (line.trim().startsWith("#")) { return {line}; } // Key: value const kvMatch = line.match(/^(\s*)([\w][\w_-]*)\s*(:)(.*)/); if (kvMatch) { const [, indent, key, colon, value] = kvMatch; return ( <> {indent} {key} {colon} ); } // List item const listMatch = line.match(/^(\s*)(-)(\s*)(.*)/); if (listMatch) { const [, indent, dash, space, value] = listMatch; return ( <> {indent} {dash} {space} {value} ); } return {line || " "}; } function YamlValue({ value }: { value: string }) { const trimmed = value.trim(); // String in quotes if ( (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")) ) { return {trimmed}; } // Boolean if (trimmed === "true" || trimmed === "false") { return {trimmed}; } // Number if (/^-?\d+(\.\d+)?$/.test(trimmed)) { return {trimmed}; } // Null if (trimmed === "null") { return ( {" "} {trimmed} ); } return {value}; }