import { useEffect, useMemo, useState } from "react"; import hljs from "highlight.js/lib/core"; import bash from "highlight.js/lib/languages/bash"; import c from "highlight.js/lib/languages/c"; import cpp from "highlight.js/lib/languages/cpp"; import css from "highlight.js/lib/languages/css"; import go from "highlight.js/lib/languages/go"; import javascript from "highlight.js/lib/languages/javascript"; import json from "highlight.js/lib/languages/json"; import python from "highlight.js/lib/languages/python"; import rust from "highlight.js/lib/languages/rust"; import sql from "highlight.js/lib/languages/sql"; import typescript from "highlight.js/lib/languages/typescript"; import xml from "highlight.js/lib/languages/xml"; import { ensureHljs, renderMarkdown } from "../../lib/markdown"; interface ArtifactViewerProps { artifact: { filename: string; content: string; language?: string; }; onDownload?: () => void; } let codeHlReady = false; function ensureCodeHljs() { if (codeHlReady) return; hljs.registerLanguage("javascript", javascript); hljs.registerLanguage("typescript", typescript); hljs.registerLanguage("python", python); hljs.registerLanguage("rust", rust); hljs.registerLanguage("bash", bash); hljs.registerLanguage("json", json); hljs.registerLanguage("sql", sql); hljs.registerLanguage("css", css); hljs.registerLanguage("xml", xml); hljs.registerLanguage("go", go); hljs.registerLanguage("cpp", cpp); hljs.registerLanguage("c", c); codeHlReady = true; } function getExtension(filename: string) { const trimmed = filename.trim().toLowerCase(); const parts = trimmed.split("."); if (parts.length < 2) return ""; return parts[parts.length - 1] ?? ""; } function getLanguage(filename: string, language?: string) { if (language) return language.toLowerCase(); const ext = getExtension(filename); const map: Record = { md: "markdown", markdown: "markdown", ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", py: "python", rs: "rust", go: "go", c: "c", h: "c", cpp: "cpp", cc: "cpp", cxx: "cpp", css: "css", html: "xml", xml: "xml", json: "json", sql: "sql", sh: "bash", bash: "bash", zsh: "bash", }; return map[ext] ?? ext ?? "plaintext"; } function fileTypeTone(ext: string) { switch (ext) { case "md": case "markdown": return "from-sky-400/20 to-cyan-400/10 text-sky-300 border-sky-500/20"; case "json": return "from-emerald-400/20 to-teal-400/10 text-emerald-300 border-emerald-500/20"; case "ts": case "tsx": case "js": case "jsx": return "from-indigo-400/20 to-violet-400/10 text-indigo-300 border-indigo-500/20"; case "py": return "from-amber-400/20 to-orange-400/10 text-amber-300 border-amber-500/20"; default: return "from-zinc-400/20 to-zinc-500/10 text-zinc-300 border-zinc-700"; } } function FileGlyph({ filename }: { filename: string }) { const ext = getExtension(filename); const tone = fileTypeTone(ext); if (ext === "md" || ext === "markdown") { return (
); } if (["ts", "tsx", "js", "jsx", "c", "cpp", "go", "rs", "py"].includes(ext)) { return (
); } if (ext === "json") { return (
); } return (
); } function downloadFile(filename: string, content: string, language?: string) { const safeFilename = filename || "artifact.txt"; const lower = safeFilename.toLowerCase(); const mime = language === "markdown" || lower.endsWith(".md") ? "text/markdown;charset=utf-8" : "text/plain;charset=utf-8"; const blob = new Blob([content], { type: mime }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = safeFilename; link.click(); URL.revokeObjectURL(url); } export default function ArtifactViewer({ artifact, onDownload }: ArtifactViewerProps) { const [expanded, setExpanded] = useState(true); const [copied, setCopied] = useState(false); const ext = getExtension(artifact.filename); const isMarkdown = ext === "md" || ext === "markdown" || artifact.language?.toLowerCase() === "markdown"; const language = getLanguage(artifact.filename, artifact.language); const previewId = useMemo(() => `artifact-preview-${artifact.filename.replace(/[^a-zA-Z0-9_-]+/g, "-")}`, [artifact.filename]); useEffect(() => { void ensureHljs(); ensureCodeHljs(); }, []); const previewHtml = useMemo(() => { if (isMarkdown) return renderMarkdown(artifact.content); let highlighted = artifact.content; try { highlighted = language !== "plaintext" && hljs.getLanguage(language) ? hljs.highlight(artifact.content, { language }).value : hljs.highlightAuto(artifact.content).value; } catch { highlighted = artifact.content.replace(/&/g, "&").replace(//g, ">"); } return `
${highlighted}
`; }, [artifact.content, isMarkdown, language]); const handleCopy = async () => { await navigator.clipboard.writeText(artifact.content); setCopied(true); window.setTimeout(() => setCopied(false), 1200); }; const handleDownload = () => { if (onDownload) { onDownload(); return; } downloadFile(artifact.filename, artifact.content, artifact.language ?? language); }; return (

{artifact.filename}

{language}

Preview, copy, or download the generated artifact.

{expanded ? (
{isMarkdown ? (
) : (
)}
) : (
Preview collapsed.
)}
); }