diff --git a/web-app/src/components/MessageContent.tsx b/web-app/src/components/MessageContent.tsx index 740888b..e72ffc9 100644 --- a/web-app/src/components/MessageContent.tsx +++ b/web-app/src/components/MessageContent.tsx @@ -68,21 +68,40 @@ function parseChoices(content: string): { choices: Choice[]; cleanContent: strin const match = content.match(choiceRegex) if (!match) { - // 如果没有标准格式,尝试从HTML中提取选择项 - // 匹配类似 "1.xxx" 或 "A.xxx" 的列表项 - const htmlChoiceRegex = /

\s*(\d+|[A-Z])\s*[.、::]\s*([^<]+)/gi + // 尝试匹配纯文本格式的选项列表 + // 匹配格式: 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) { - choices.push({ + htmlChoices.push({ label: htmlMatch[1], text: htmlMatch[2].trim() }) } - if (choices.length > 0) { - return { choices, cleanContent: content } + if (htmlChoices.length > 0) { + return { choices: htmlChoices, cleanContent: content } } return { choices: [], cleanContent: content } @@ -145,7 +164,7 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag const [showRaw, setShowRaw] = useState(false) const [hasHtml, setHasHtml] = useState(false) const [hasScript, setHasScript] = useState(false) - const [allowScript, setAllowScript] = useState(true) // 默认启用脚本 + const [allowScript, setAllowScript] = useState(false) // 默认禁用脚本 const [choices, setChoices] = useState([]) const [displayContent, setDisplayContent] = useState(content) const [statusPanel, setStatusPanel] = useState(null) @@ -154,32 +173,106 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag 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(content) + const { status, cleanContent: contentAfterStatus } = parseStatusPanel(processedContent) console.log('[MessageContent] 状态面板:', status) setStatusPanel(status) - // 解析选择项 - const { choices: parsedChoices, cleanContent } = parseChoices(contentAfterStatus) - console.log('[MessageContent] 选择项:', parsedChoices) + // 解析选择项(从剩余内容中解析,而不是从 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) - // 清理脚本输出 - const finalContent = cleanScriptOutput(cleanContent) + // 清理脚本输出(只在非 HTML 代码块时清理) + // 如果是HTML代码块,直接使用提取的HTML内容(processedContent),而不是contentAfterStatus + const finalContent = isHtmlCodeBlock ? processedContent : cleanScriptOutput(cleanContent) console.log('[MessageContent] 清理后内容:', finalContent) - setDisplayContent(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 = () => { @@ -197,15 +290,36 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag @@ -220,17 +334,20 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag setTimeout(() => { if (doc.body) { const height = doc.body.scrollHeight - iframe.style.height = `${Math.max(height, 100)}px` + iframe.style.height = `${Math.max(height + 32, 100)}px` } - }, 100) + }, 150) } } useEffect(() => { - if (allowScript && hasScript) { - renderInIframe() + if (allowScript && hasScript && iframeRef.current) { + // 延迟渲染确保 iframe 已挂载 + setTimeout(() => { + renderInIframe() + }, 50) } - }, [allowScript, displayContent]) + }, [allowScript, hasScript, displayContent]) // 如果是用户消息,直接显示纯文本 if (role === 'user') { @@ -254,14 +371,16 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag {hasScript && ( )} @@ -273,17 +392,23 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag {content}
) : hasScript && allowScript ? ( -