/**
* 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) => (
{h}
))}
{rows.map((row, i) => (
{row.map((cell, j) => (
{cell}
))}
))}
);
}
function ImagePreview({ src, filename }: { src: string; filename: string }) {
return (
);
}
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 (
);
}
function TextPreview({ content }: { content: string }) {
return (
{content}
);
}
function UnsupportedPreview({
filename,
title,
description,
}: {
filename: string;
title: string;
description: string;
}) {
return (
{title}
{description}
{filename}
);
}
function MarkdownPreview({ content }: { content: string }) {
return (
{content}
);
}
function EmptyPreview({ filename }: { filename: string }) {
return (
Cannot preview {filename}
Download to view this file
);
}
export function FilePreview({
filename,
content,
blobUrl,
mimeType,
onClose,
onDownload,
hideHeader = false,
className,
}: FilePreviewProps) {
const format: FileFormat = detectFileFormat(filename, mimeType);
const previewLabel = getFormatLabel(format);
const hasRenderableSource =
Boolean(content) ||
Boolean(blobUrl) ||
format === "unknown" ||
format === "spreadsheet";
return (
{!hideHeader && (
{filename}
{previewLabel}
{onDownload && (
)}
{onClose && (
)}
)}
{format === "pdf" && blobUrl &&
}
{format === "image" && blobUrl &&
}
{format === "csv" && typeof content === "string" &&
}
{(format === "code" || format === "json" || format === "yaml") && typeof content === "string" && (
)}
{format === "text" && typeof content === "string" &&
}
{format === "markdown" && typeof content === "string" &&
}
{format === "spreadsheet" && (
)}
{format === "unknown" && typeof content !== "string" &&
}
{format === "unknown" && typeof content === "string" &&
}
{!hasRenderableSource && typeof content !== "string" && (
)}
);
}