import type { ReactNode } from "react"; import { CopyOutlined } from "@ant-design/icons"; import { App, Button } from "antd"; import { useCallback, useEffect, useState } from "react"; import { codeToHtml } from "shiki"; import { useIsDark } from "../../../shared/hooks/use-is-dark"; interface CodeBlockProps { children: ReactNode; className?: string; isStreaming: boolean; } export function CodeBlock({ children, className: _className, isStreaming }: CodeBlockProps) { const { message } = App.useApp(); const isDark = useIsDark(); const [highlighted, setHighlighted] = useState(null); const { codeText, lang } = extractCode(children); const handleCopy = useCallback(() => { navigator.clipboard.writeText(codeText).then( () => message.success("已复制"), () => message.error("复制失败"), ); }, [codeText, message]); useEffect(() => { if (isStreaming || !codeText) return; let cancelled = false; codeToHtml(codeText, { lang, theme: isDark ? "github-dark" : "github-light", }) .then((html) => { if (!cancelled) setHighlighted(html); }) .catch(() => { if (!cancelled) setHighlighted(null); }); return () => { cancelled = true; }; }, [codeText, lang, isDark, isStreaming]); if (isStreaming) { return (
        {codeText}
      
); } return (
{lang}
{highlighted ? (
) : (
            {codeText}
          
)}
); } function extractCode(children: ReactNode): { codeText: string; lang: string } { if (children && typeof children === "object" && "props" in children && children.props) { const props = children.props as Record; const codeText = typeof props["children"] === "string" ? props["children"] : ""; const codeClassName = typeof props["className"] === "string" ? props["className"] : ""; const classes = codeClassName.split(/\s+/); const langClass = classes.find((c) => c.startsWith("lang-") || c.startsWith("language-")) ?? ""; const lang = langClass.replace(/^(language|lang)-/, "") || "text"; return { codeText, lang }; } return { codeText: typeof children === "string" ? children : "", lang: "text" }; }