/** * FilePreview — universal file renderer. * * Renders any file type beautifully: * - PDF: embedded viewer * - CSV/XLSX: tabular preview * - Code (py/json/yaml/ts/js): syntax-highlighted, line-numbered viewer * - Markdown: rendered prose * - Images: inline display * - Text: monospace preview */ import { Download, X, FileText, } from "lucide-react"; import { cn } from "../lib/utils"; import { Markdown } from "../markdown/markdown"; import { CodeBlock, CopyButton } from "../markdown/code-block"; import { detectFileFormat, fileExtension, getCodeLanguage, getFormatLabel, type FileFormat, } from "./file-format"; export interface FilePreviewProps { filename: string; content?: string; blobUrl?: string; mimeType?: string; onClose?: () => void; onDownload?: () => void; hideHeader?: boolean; className?: string; } function CodePreview({ content, filename, format, }: { content: string; filename: string; format: FileFormat; }) { const lineCount = content.split("\n").length; const language = getCodeLanguage(filename, format); // Prefer the extension; for an extensionless file (e.g. one detected from its // MIME type) fall back to the highlight language so the label stays meaningful. const labelToken = fileExtension(filename) || language || "txt"; // Same theme-aware highlighter the chat markdown renderer uses, so code looks // identical in an artifact pane and inline in a message. return ( ); } function parseCsvRow(line: string) { const cells: string[] = []; let current = ""; let inQuotes = false; for (let index = 0; index < line.length; index += 1) { const char = line[index]; const next = line[index + 1]; if (char === "\"") { if (inQuotes && next === "\"") { current += "\""; index += 1; continue; } inQuotes = !inQuotes; continue; } if (char === "," && !inQuotes) { cells.push(current.trim()); current = ""; continue; } current += char; } cells.push(current.trim()); return cells; } function CsvPreview({ content }: { content: string }) { const lines = content .trim() .split(/\r?\n/) .filter(Boolean); if (lines.length === 0) return null; const headers = parseCsvRow(lines[0]).map((header) => header.replace(/^"|"$/g, "")); const rows = lines.slice(1).map((line) => parseCsvRow(line).map((cell) => cell.replace(/^"|"$/g, "")), ); return (
{headers.map((h, i) => ( ))} {rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))}
{h}
{cell}
); } function ImagePreview({ src, filename }: { src: string; filename: string }) { return (
{filename}
); } function PdfPreview({ blobUrl, filename }: { blobUrl: string; filename: string }) { // Simple iframe-based PDF viewer. For richer rendering, consumers can // swap in react-pdf at the app level. return (