diff --git a/src/components/markdown/CodeBlock.tsx b/src/components/markdown/CodeBlock.tsx index 975bf1e..d7f2a3d 100644 --- a/src/components/markdown/CodeBlock.tsx +++ b/src/components/markdown/CodeBlock.tsx @@ -1,10 +1,20 @@ 'use client'; -import { useEffect, useRef, useState, useMemo } from 'react'; -import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react'; +import { useState, useMemo, useCallback } from 'react'; +import { Copy, Check, Eye, ChevronDown, ChevronUp, Play, Square, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import Prism from 'prismjs'; import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal'; +import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult'; +import { + executeCode, + stopExecution, + isRunnableLanguage, + isCodeExecutable, + getLanguageConfig, + setPyodideLoadingCallbacks, + type ExecutionResult, +} from '@/lib/code-runner'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-jsx'; @@ -24,6 +34,10 @@ import 'prismjs/components/prism-markdown'; import 'prismjs/components/prism-css'; import 'prismjs/components/prism-scss'; import 'prismjs/components/prism-markup'; +import 'prismjs/components/prism-kotlin'; +import 'prismjs/components/prism-swift'; +import 'prismjs/components/prism-ruby'; +import 'prismjs/components/prism-php'; interface CodeBlockProps { code: string; @@ -62,6 +76,15 @@ export function CodeBlock({ const [previewOpen, setPreviewOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); + // 代码执行相关状态 + const [isRunning, setIsRunning] = useState(false); + const [executionResult, setExecutionResult] = useState(null); + const [pyodideStatus, setPyodideStatus] = useState<{ + stage: 'loading' | 'ready' | 'error'; + message: string; + progress?: number; + } | null>(null); + // 规范化语言名称 const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase(); @@ -69,6 +92,10 @@ export function CodeBlock({ const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) || ['html', 'htm'].includes(language.toLowerCase()); + // 判断是否可执行:语言支持 + 代码满足执行条件 + const canRun = isRunnableLanguage(language) && isCodeExecutable(code, language); + const languageConfig = canRun ? getLanguageConfig(language) : null; + const lines = code.split('\n'); const totalLines = lines.length; const shouldCollapse = totalLines > maxCollapsedLines; @@ -86,11 +113,20 @@ export function CodeBlock({ // 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮 const highlightedCode = useMemo(() => { - const grammar = Prism.languages[normalizedLanguage]; - if (grammar) { - return Prism.highlight(displayCode, grammar, normalizedLanguage); + try { + const grammar = Prism.languages[normalizedLanguage]; + if (grammar) { + return Prism.highlight(displayCode, grammar, normalizedLanguage); + } + } catch (error) { + // 某些语言的 grammar 可能不完整,fallback 到纯文本 + console.warn(`Prism highlight failed for ${normalizedLanguage}:`, error); } - return displayCode; + // 对纯文本进行 HTML 转义 + return displayCode + .replace(/&/g, '&') + .replace(//g, '>'); }, [displayCode, normalizedLanguage]); const handleCopy = async () => { @@ -108,6 +144,67 @@ export function CodeBlock({ setIsExpanded(!isExpanded); }; + // 运行代码 + const handleRun = useCallback(async () => { + if (isRunning || !canRun) return; + + setIsRunning(true); + setExecutionResult(null); + + // 如果是 Python,设置 Pyodide 加载回调 + if (languageConfig?.engine === 'pyodide') { + setPyodideLoadingCallbacks({ + onLoadingStart: () => { + setPyodideStatus({ + stage: 'loading', + message: '正在加载 Python 运行时...', + progress: 0, + }); + }, + onLoadingProgress: (message, progress) => { + setPyodideStatus({ + stage: 'loading', + message, + progress, + }); + }, + onLoadingComplete: () => { + setPyodideStatus(null); + }, + onLoadingError: (error) => { + setPyodideStatus({ + stage: 'error', + message: error, + }); + }, + }); + } + + try { + const result = await executeCode(code, language); + setExecutionResult(result); + } catch (error) { + setExecutionResult({ + success: false, + output: '', + error: error instanceof Error ? error.message : '执行失败', + executionTime: 0, + engine: languageConfig?.engine || 'sandbox', + }); + } finally { + setIsRunning(false); + setPyodideStatus(null); + setPyodideLoadingCallbacks(null); + } + }, [code, language, isRunning, canRun, languageConfig]); + + // 停止执行 + const handleStop = useCallback(() => { + stopExecution(); + setIsRunning(false); + setPyodideStatus(null); + }, []); + return (
@@ -141,6 +238,35 @@ export function CodeBlock({ {/* 右侧:操作按钮 */}
+ {/* 运行按钮 */} + {canRun && ( + + )} {/* HTML 预览按钮 */} {isHtmlPreviewable && (