"use client"; import * as React from "react"; import { Check, ChevronsDownUp, ChevronsUpDown, Copy, Download, WrapText } from "lucide-react"; import { extensionForLanguage } from "@/lib/chat/markdown-blocks"; import { useDebounced } from "@/lib/client/hooks"; import { cn } from "@/lib/utils"; type Highlighter = { codeToHtml: (code: string, opts: { lang: string; themes: { light: string; dark: string }; defaultColor: false | "light" }) => string; getLoadedLanguages: () => string[]; loadLanguage: (lang: never) => Promise }; let highlighterPromise: Promise | null = null; const KNOWN = new Set(); async function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = (async () => { const { createHighlighter } = await import("shiki/bundle/web"); const { createJavaScriptRegexEngine } = await import("shiki/engine/javascript"); const hl = await createHighlighter({ themes: ["github-light-default", "github-dark-default"], langs: ["javascript", "typescript", "tsx", "jsx", "json", "bash", "shell", "python", "markdown", "html", "css", "sql", "yaml"], engine: createJavaScriptRegexEngine({ forgiving: true }) }); for (const l of hl.getLoadedLanguages()) KNOWN.add(l); return hl as unknown as Highlighter; })(); } return highlighterPromise; } const ALIASES: Record = { js: "javascript", ts: "typescript", sh: "bash", zsh: "bash", shell: "bash", py: "python", yml: "yaml", md: "markdown", rb: "ruby", rs: "rust", golang: "go", "c++": "cpp", cs: "csharp", kt: "kotlin", plaintext: "text", txt: "text" }; /** Lines above which a block starts collapsed (Expand reveals everything). */ const COLLAPSE_LINES = 28; export function CodeBlock({ code, lang, wrap }: { code: string; lang?: string; wrap?: boolean }) { const [html, setHtml] = React.useState(null); const [copied, setCopied] = React.useState(false); const [localWrap, setLocalWrap] = React.useState(null); const [expanded, setExpanded] = React.useState(false); const language = React.useMemo(() => { const l = (lang ?? "").toLowerCase().trim(); return ALIASES[l] ?? l; }, [lang]); const effectiveWrap = localWrap ?? wrap ?? false; // While streaming the block changes on every token; highlight the settled version only. const settled = useDebounced(code, 120); const lineCount = React.useMemo(() => (code ? code.split("\n").length : 0), [code]); const collapsible = lineCount > COLLAPSE_LINES; React.useEffect(() => { let cancelled = false; if (!language || language === "text") { // eslint-disable-next-line react-hooks/set-state-in-effect setHtml(null); return; } (async () => { try { const hl = await getHighlighter(); if (!KNOWN.has(language)) { try { await hl.loadLanguage(language as never); KNOWN.add(language); } catch { if (!cancelled) setHtml(null); return; } } const out = hl.codeToHtml(settled, { lang: language, themes: { light: "github-light-default", dark: "github-dark-default" }, defaultColor: "light" }); if (!cancelled) setHtml(out); } catch { if (!cancelled) setHtml(null); } })(); return () => { cancelled = true; }; }, [settled, language]); const copy = async () => { try { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1400); } catch { /* ignore */ } }; const download = () => { const ext = extensionForLanguage(language); const blob = new Blob([code], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = ext === "Dockerfile" ? "Dockerfile" : `snippet.${ext}`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }; // Show the highlighted HTML only when it matches the current code (avoids stale colouring while streaming). const showHtml = html && settled === code; return (
{language || "text"} {lineCount > 1 ? ยท {lineCount} lines : null}
{collapsible ? ( setExpanded((e) => !e)}> {expanded ? : } ) : null} setLocalWrap((w) => !(w ?? wrap ?? false))} active={effectiveWrap}>
{showHtml ? (
) : (
              {code}
            
)}
{collapsible && !expanded ? ( ) : null}
); } function ToolBtn({ label, onClick, active, children }: { label: string; onClick: () => void; active?: boolean; children: React.ReactNode }) { return ( ); }