import { useState, useEffect, useRef } from "react"; import { Copy, Check } from "lucide-react"; import hljs from "highlight.js/lib/core"; import typescript from "highlight.js/lib/languages/typescript"; import javascript from "highlight.js/lib/languages/javascript"; import python from "highlight.js/lib/languages/python"; import css from "highlight.js/lib/languages/css"; import json from "highlight.js/lib/languages/json"; import xml from "highlight.js/lib/languages/xml"; import bash from "highlight.js/lib/languages/bash"; import sql from "highlight.js/lib/languages/sql"; import go from "highlight.js/lib/languages/go"; import rust from "highlight.js/lib/languages/rust"; import java from "highlight.js/lib/languages/java"; import cpp from "highlight.js/lib/languages/cpp"; import markdownLang from "highlight.js/lib/languages/markdown"; import yaml from "highlight.js/lib/languages/yaml"; import { Button } from "./ui/button"; import { ChatFileCard } from "./ChatFileCard"; import type { ChatFile } from "@paperclipai/shared"; // Register languages with hljs hljs.registerLanguage("typescript", typescript); hljs.registerLanguage("javascript", javascript); hljs.registerLanguage("python", python); hljs.registerLanguage("css", css); hljs.registerLanguage("json", json); hljs.registerLanguage("xml", xml); hljs.registerLanguage("bash", bash); hljs.registerLanguage("sql", sql); hljs.registerLanguage("go", go); hljs.registerLanguage("rust", rust); hljs.registerLanguage("java", java); hljs.registerLanguage("cpp", cpp); hljs.registerLanguage("markdown", markdownLang); hljs.registerLanguage("yaml", yaml); function extToLang(filename: string): string { const ext = filename.split(".").pop()?.toLowerCase() ?? ""; const map: Record = { ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", py: "python", rb: "ruby", rs: "rust", go: "go", java: "java", css: "css", html: "xml", json: "json", sh: "bash", bash: "bash", yaml: "yaml", yml: "yaml", toml: "ini", md: "markdown", c: "c", cpp: "cpp", cs: "csharp", kt: "kotlin", swift: "swift", php: "php", sql: "sql", xml: "xml", }; return map[ext] ?? ext; } /** * Safely renders hljs-highlighted HTML into a DOM node. * hljs.highlight() produces only tokens from source code. * We parse through DOMParser (sandboxed document) and transfer child nodes * rather than assigning raw HTML strings, so no script execution is possible. */ function applyHighlightedHtml(el: HTMLElement, highlightedHtml: string): void { const parser = new DOMParser(); const doc = parser.parseFromString( `${highlightedHtml}`, "text/html" ); const sourceCode = doc.body.firstChild; if (!sourceCode) return; // Transfer parsed child nodes into the target element el.replaceChildren(...Array.from(sourceCode.childNodes)); } interface ChatCodeFilePreviewProps { file: ChatFile; contentPath: string; } export function ChatCodeFilePreview({ file, contentPath }: ChatCodeFilePreviewProps) { const [content, setContent] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [copied, setCopied] = useState(false); const codeRef = useRef(null); useEffect(() => { let cancelled = false; setLoading(true); setError(false); setContent(null); fetch(contentPath, { credentials: "include" }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.text(); }) .then((text) => { if (cancelled) return; const capped = text.length > 50000 ? text.slice(0, 50000) + "\n// ... truncated" : text; setContent(capped); setLoading(false); }) .catch(() => { if (cancelled) return; setError(true); setLoading(false); }); return () => { cancelled = true; }; }, [contentPath]); // Apply highlighting whenever content or code element changes useEffect(() => { if (!codeRef.current || content === null) return; const lang = extToLang(file.originalFilename); let highlighted: string; try { const registeredLangs = hljs.listLanguages(); if (registeredLangs.includes(lang)) { highlighted = hljs.highlight(content, { language: lang }).value; } else { highlighted = hljs.highlightAuto(content).value; } } catch { highlighted = hljs.highlightAuto(content).value; } applyHighlightedHtml(codeRef.current, highlighted); }, [content, file.originalFilename]); if (loading) { return
; } if (error || content === null) { return ; } const lang = extToLang(file.originalFilename); function handleCopy() { navigator.clipboard.writeText(content ?? "").catch(() => { // Ignore clipboard errors in restricted environments }); setCopied(true); setTimeout(() => setCopied(false), 1500); } return (
{lang || "text"}
            
          
); }