import {useEffect, useRef, useState} from 'react' import {Code, Eye, Play} from 'lucide-react' interface MessageContentProps { content: string role: 'user' | 'assistant' onChoiceSelect?: (choice: string) => void } interface Choice { label: string text: string } // 解析并高亮对白的组件 function DialogueText({ text }: { text: string }) { // 匹配各种引号格式的对白 const dialogueRegex = /([""「『])(.*?)([""」』])|(")(.*?)(")|(')(.*?)(')/g const parts: JSX.Element[] = [] let lastIndex = 0 let match let key = 0 while ((match = dialogueRegex.exec(text)) !== null) { // 添加对白之前的文本 if (match.index > lastIndex) { parts.push( {text.substring(lastIndex, match.index)} ) } // 提取对白内容(处理不同的引号组) const dialogue = match[2] || match[5] || match[8] const openQuote = match[1] || match[4] || match[7] const closeQuote = match[3] || match[6] || match[9] // 添加高亮的对白 parts.push( {openQuote} {dialogue} {closeQuote} ) lastIndex = match.index + match[0].length } // 添加剩余的文本 if (lastIndex < text.length) { parts.push( {text.substring(lastIndex)} ) } return <>{parts.length > 0 ? parts : {text}} } // 解析选择项的函数 function parseChoices(content: string): { choices: Choice[]; cleanContent: string } { // 匹配 ... 或 [choice]...[/choice] 格式 const choiceRegex = /(?:|\\[choice\\])([\s\S]*?)(?:<\/choice>|\\[\/choice\\])/i const match = content.match(choiceRegex) if (!match) { // 尝试匹配纯文本格式的选项列表 // 匹配格式: A: xxx\nB: xxx\nC: xxx 或 A. xxx\nB. xxx const textChoiceRegex = /^([A-E])[.、::]\s*(.+?)(?=\n[A-E][.、::]|\n*$)/gm const choices: Choice[] = [] let textMatch while ((textMatch = textChoiceRegex.exec(content)) !== null) { choices.push({ label: textMatch[1], text: textMatch[2].trim() }) } // 如果找到了至少2个选项,认为是有效的选择列表 if (choices.length >= 2) { // 移除选项列表,保留其他内容 const cleanContent = content.replace(textChoiceRegex, '').trim() return { choices, cleanContent } } // 如果没有纯文本格式,尝试从HTML中提取选择项 const htmlChoiceRegex = /

\s*(\d+|[A-Z])\s*[.、::]\s*([^<]+)/gi const htmlChoices: Choice[] = [] let htmlMatch while ((htmlMatch = htmlChoiceRegex.exec(content)) !== null) { htmlChoices.push({ label: htmlMatch[1], text: htmlMatch[2].trim() }) } if (htmlChoices.length > 0) { return { choices: htmlChoices, cleanContent: content } } return { choices: [], cleanContent: content } } const choiceBlock = match[1] const choices: Choice[] = [] // 匹配 A. text, B. text 等格式 const optionRegex = /^([A-Z])[.、::]\s*(.+)$/gm let optionMatch while ((optionMatch = optionRegex.exec(choiceBlock)) !== null) { choices.push({ label: optionMatch[1], text: optionMatch[2].trim() }) } // 移除选择块,返回清理后的内容 const cleanContent = content.replace(choiceRegex, '').trim() return { choices, cleanContent } } // 清理脚本输出内容 function cleanScriptOutput(content: string): string { // 移除 ... 块 let cleaned = content.replace(/[\s\S]*?<\/UpdateVariable>/gi, '') // 移除 ... 块 cleaned = cleaned.replace(/[\s\S]*?<\/Analysis>/gi, '') // 移除 _.set() 调用 cleaned = cleaned.replace(/^\s*_.set\([^)]+\);\s*$/gm, '') return cleaned.trim() } // 解析状态面板数据 function parseStatusPanel(content: string): { status: any; cleanContent: string } { const statusRegex = /([\s\S]*?)<\/status_current_variable>/i const match = content.match(statusRegex) if (!match) { return { status: null, cleanContent: content } } try { const statusData = JSON.parse(match[1].trim()) const cleanContent = content.replace(statusRegex, '').trim() return { status: statusData, cleanContent } } catch (e) { console.error('解析状态面板失败:', e) return { status: null, cleanContent: content } } } export default function MessageContent({ content, role, onChoiceSelect }: MessageContentProps) { const [showRaw, setShowRaw] = useState(false) const [hasHtml, setHasHtml] = useState(false) const [hasScript, setHasScript] = useState(false) const [allowScript, setAllowScript] = useState(false) // 默认禁用脚本 const [choices, setChoices] = useState([]) const [displayContent, setDisplayContent] = useState(content) const [statusPanel, setStatusPanel] = useState(null) const iframeRef = useRef(null) useEffect(() => { console.log('[MessageContent] 原始内容:', content) // 提取 markdown 代码块中的 HTML,并从原始内容中移除代码块标记 // 支持 ```html 和不带标识符的 ``` 包裹(后者在内容含 HTML 标签时识别为 HTML 块) let processedContent = content let remainingContent = content let isHtmlCodeBlock = false const htmlTagRegex = /<[a-zA-Z][^>]*>/ // 先尝试匹配 ```html 代码块 const explicitHtmlRegex = /```html\s*([\s\S]*?)```/gi const htmlCodeBlocks: string[] = [] let htmlMatch while ((htmlMatch = explicitHtmlRegex.exec(content)) !== null) { htmlCodeBlocks.push(htmlMatch[1].trim()) } if (htmlCodeBlocks.length > 0) { // 有明确的 ```html 标识 processedContent = htmlCodeBlocks.join('\n') remainingContent = content.replace(/```html\s*[\s\S]*?```/gi, '').trim() isHtmlCodeBlock = true console.log('[MessageContent] 提取到 ```html 代码块:', processedContent) console.log('[MessageContent] 剩余内容:', remainingContent) } else { // 尝试匹配无语言标识的 ``` 代码块,内容含 HTML 标签时也视为 HTML 块 const genericCodeRegex = /```\s*\n?([\s\S]*?)```/g const genericBlocks: string[] = [] let genericMatch while ((genericMatch = genericCodeRegex.exec(content)) !== null) { const blockContent = genericMatch[1].trim() if (htmlTagRegex.test(blockContent)) { genericBlocks.push(blockContent) } } if (genericBlocks.length > 0) { processedContent = genericBlocks.join('\n') remainingContent = content.replace(/```\s*\n?[\s\S]*?```/g, '').trim() isHtmlCodeBlock = true console.log('[MessageContent] 提取到通用 HTML 代码块:', processedContent) console.log('[MessageContent] 剩余内容:', remainingContent) } } // 解析状态面板 const { status, cleanContent: contentAfterStatus } = parseStatusPanel(processedContent) console.log('[MessageContent] 状态面板:', status) setStatusPanel(status) // 解析选择项(从剩余内容中解析,而不是从 HTML 中解析) let parsedChoices: Choice[] = [] let cleanContent = contentAfterStatus if (isHtmlCodeBlock && remainingContent) { // 如果有 HTML 代码块,从剩余内容中解析选项 const choiceResult = parseChoices(remainingContent) parsedChoices = choiceResult.choices console.log('[MessageContent] 从剩余内容解析选择项:', parsedChoices) } else if (!isHtmlCodeBlock) { // 如果没有 HTML 代码块,正常解析 const choiceResult = parseChoices(contentAfterStatus) parsedChoices = choiceResult.choices cleanContent = choiceResult.cleanContent console.log('[MessageContent] 选择项:', parsedChoices) } setChoices(parsedChoices) // 清理脚本输出(只在非 HTML 代码块时清理) // 如果是HTML代码块,直接使用提取的HTML内容(processedContent),而不是contentAfterStatus const finalContent = isHtmlCodeBlock ? processedContent : cleanScriptOutput(cleanContent) console.log('[MessageContent] 清理后内容:', finalContent) // 先检测内容类型(需要在转换代码块之前检测,以便使用原始 finalContent) const htmlRegex = /<[^>]+>/g const scriptRegex = //gi const hasHtmlContent = htmlRegex.test(finalContent) const hasScriptContent = scriptRegex.test(finalContent) console.log('[MessageContent] hasHtml:', hasHtmlContent, 'hasScript:', hasScriptContent) // 对于含 HTML 的内容,将剩余的 markdown 代码块(``` ... ```)转换为


    // 避免反引号作为纯文本出现在 dangerouslySetInnerHTML 的渲染结果中
    let renderedContent = finalContent
    if (hasHtmlContent) {
      renderedContent = finalContent.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
        const escaped = code
          .replace(/&/g, '&')
          .replace(//g, '>')
        const langLabel = lang ? `${lang}` : ''
        return `
${langLabel}${escaped}
` }) } setDisplayContent(renderedContent) setHasHtml(hasHtmlContent) setHasScript(hasScriptContent) // 如果有 HTML 内容或脚本,自动启用 if (hasHtmlContent || hasScriptContent) { setAllowScript(true) console.log('[MessageContent] 自动启用脚本') } }, [content]) const renderInIframe = () => { if (!iframeRef.current) return const iframe = iframeRef.current const doc = iframe.contentDocument || iframe.contentWindow?.document if (doc) { doc.open() doc.write(` ${displayContent} `) doc.close() // 自动调整iframe高度 setTimeout(() => { if (doc.body) { const height = doc.body.scrollHeight iframe.style.height = `${Math.max(height + 32, 100)}px` } }, 150) } } useEffect(() => { if (allowScript && hasScript && iframeRef.current) { // 延迟渲染确保 iframe 已挂载 setTimeout(() => { renderInIframe() }, 50) } }, [allowScript, hasScript, displayContent]) // 如果是用户消息,直接显示纯文本 if (role === 'user') { return

{content}

} // AI消息 - 支持多种渲染模式 return (
{/* 控制按钮 */} {(hasHtml || hasScript) && (
{hasScript && ( )}
)} {/* 内容渲染 */} {showRaw ? (
          {content}
        
) : hasScript && allowScript ? ( // 有脚本时使用 iframe 渲染