Files
deerflow-code/frontend-web/src/open-canvas/components/artifact/CodeArtifactRenderer.tsx
T
2026-05-17 22:33:43 +08:00

121 lines
3.6 KiB
TypeScript

import { html } from "@codemirror/lang-html";
import { javascript } from "@codemirror/lang-javascript";
import { json } from "@codemirror/lang-json";
import { python } from "@codemirror/lang-python";
import type { Extension } from "@codemirror/state";
import { basicLightInit } from "@uiw/codemirror-theme-basic";
import { monokaiInit } from "@uiw/codemirror-theme-monokai";
import CodeMirror, { type ViewUpdate } from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
import { useCallback, useMemo } from "react";
import type {
CanvasArtifactCode,
ProgrammingLanguageOptions,
} from "../../types/artifact";
import { useCanvasSelection } from "../../hooks/useCanvasSelection";
// ---------------------------------------------------------------------------
// Themes — match code-editor.tsx defaults
// ---------------------------------------------------------------------------
const darkTheme = monokaiInit({
settings: {
background: "transparent",
gutterBackground: "transparent",
gutterForeground: "#555",
gutterActiveForeground: "#fff",
fontSize: "var(--text-sm)",
},
});
const lightTheme = basicLightInit({
settings: {
background: "transparent",
fontSize: "var(--text-sm)",
},
});
// ---------------------------------------------------------------------------
// Language detection
// ---------------------------------------------------------------------------
function getLanguageExtension(
lang: ProgrammingLanguageOptions,
): Extension | null {
switch (lang) {
case "typescript":
return javascript({ typescript: true, jsx: true });
case "javascript":
return javascript({ jsx: true });
case "python":
return python();
case "html":
return html();
case "json":
return json();
// cpp, java, rust, sql, xml, clojure, csharp, php → plain text fallback
default:
return null;
}
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
interface CodeArtifactRendererProps {
content: CanvasArtifactCode;
isStreaming?: boolean;
}
export function CodeArtifactRenderer({ content, isStreaming: _isStreaming }: CodeArtifactRendererProps) {
const { resolvedTheme } = useTheme();
const { handleCodeSelectionChange } = useCanvasSelection();
const extensions = useMemo<Extension[]>(() => {
const ext = getLanguageExtension(content.language);
return ext ? [ext] : [];
}, [content.language]);
const handleUpdate = useCallback(
(update: ViewUpdate) => {
if (!update.selectionSet) return;
const { from, to } = update.state.selection.main;
handleCodeSelectionChange(from, to);
},
[handleCodeSelectionChange],
);
if (!content.code) {
return (
<div className="h-full flex items-center justify-center">
<p className="text-sm text-muted-foreground">
空白代码,发送消息让 AI 填充内容
</p>
</div>
);
}
return (
<div className="h-full overflow-auto">
<CodeMirror
value={content.code}
readOnly
height="100%"
className="h-full font-mono [&_.cm-editor]:h-full [&_.cm-focused]:outline-none!"
theme={resolvedTheme === "dark" ? darkTheme : lightTheme}
extensions={extensions}
onUpdate={handleUpdate}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: false,
highlightActiveLineGutter: false,
autocompletion: false,
}}
/>
</div>
);
}