From 4100d908da1f72e3ba57bc689dfe4d1b2d9b1274 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Mon, 2 Mar 2026 02:55:55 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E7=9A=84bug=EF=BC=8C=E4=BC=98=E5=8C=96html?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=92=8C=E6=96=87=E6=9C=AC=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Echo <1711788888@qq.com> --- web-app/src/components/MessageContent.tsx | 168 +++++++++++++--------- 1 file changed, 103 insertions(+), 65 deletions(-) diff --git a/web-app/src/components/MessageContent.tsx b/web-app/src/components/MessageContent.tsx index 4532efa..0ef6cac 100644 --- a/web-app/src/components/MessageContent.tsx +++ b/web-app/src/components/MessageContent.tsx @@ -164,91 +164,126 @@ 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(false) // 默认禁用脚本 + const [allowScript, setAllowScript] = useState(false) const [choices, setChoices] = useState([]) const [displayContent, setDisplayContent] = useState(content) + const [remainingText, setRemainingText] = useState('') const [statusPanel, setStatusPanel] = useState(null) const iframeRef = useRef(null) useEffect(() => { console.log('[MessageContent] 原始内容:', content) - // 提取 markdown 代码块中的 HTML(仅识别 ```html 标识符的代码块) let processedContent = content - let remainingContent = content - let isHtmlCodeBlock = false - 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) - } // 解析状态面板 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) - } + // 解析选择项 + const { choices: parsedChoices, cleanContent } = parseChoices(contentAfterStatus) setChoices(parsedChoices) + console.log('[MessageContent] 选择项:', parsedChoices) - // 清理脚本输出(只在非 HTML 代码块时清理) - // 如果是HTML代码块,直接使用提取的HTML内容(processedContent),而不是contentAfterStatus - const finalContent = isHtmlCodeBlock ? processedContent : cleanScriptOutput(cleanContent) + // 清理脚本输出 + const finalContent = cleanScriptOutput(cleanContent) console.log('[MessageContent] 清理后内容:', finalContent) - // 先检测内容类型(需要在转换代码块之前检测,以便使用原始 finalContent) + // 检测是否包含 HTML 标签或代码块 const htmlRegex = /<[^>]+>/g + const codeBlockRegex = /```[\s\S]*?```/g const scriptRegex = //gi - const hasHtmlContent = htmlRegex.test(finalContent) - const hasScriptContent = scriptRegex.test(finalContent) - console.log('[MessageContent] hasHtml:', hasHtmlContent, 'hasScript:', hasScriptContent) - // 对于含 HTML 的内容,将剩余的 markdown 代码块(``` ... ```)转换为

-    // 避免反引号作为纯文本出现在 dangerouslySetInnerHTML 的渲染结果中
+    const hasCodeBlocks = codeBlockRegex.test(finalContent)
+    const hasHtmlTags = htmlRegex.test(finalContent)
+    const hasScriptContent = scriptRegex.test(finalContent)
+
+    console.log('[MessageContent] hasCodeBlocks:', hasCodeBlocks, 'hasHtmlTags:', hasHtmlTags, 'hasScript:', hasScriptContent)
+
     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}
` + + // 如果包含 HTML 或代码块,进行混合渲染处理 + if (hasHtmlTags || hasCodeBlocks) { + // 步骤1: 先处理代码块,将其转换为 HTML 或保护起来 + const codeBlockPlaceholders: { [key: string]: string } = {} + let codeBlockIndex = 0 + + // 处理 ```html 代码块 + renderedContent = renderedContent.replace(/```html\s*([\s\S]*?)```/gi, (_match, code) => { + const placeholder = `__CODEBLOCK_${codeBlockIndex}__` + codeBlockPlaceholders[placeholder] = code.trim() + codeBlockIndex++ + return placeholder + }) + + // 处理其他代码块 + renderedContent = renderedContent.replace(/```(\w*)\s*([\s\S]*?)```/gi, (_match, lang, code) => { + const trimmedCode = code.trim() + const placeholder = `__CODEBLOCK_${codeBlockIndex}__` + + // 如果包含 HTML 标签,直接渲染 + if (/<[^>]+>/.test(trimmedCode)) { + codeBlockPlaceholders[placeholder] = trimmedCode + } else { + // 否则转换为

+          const escaped = trimmedCode
+            .replace(/&/g, '&')
+            .replace(//g, '>')
+          const langLabel = lang ? `${lang}` : ''
+          codeBlockPlaceholders[placeholder] = `
${langLabel}${escaped}
` + } + + codeBlockIndex++ + return placeholder + }) + + // 步骤2: 保护现有的 HTML 标签(不包括代码块占位符) + const htmlPlaceholders: { [key: string]: string } = {} + let htmlIndex = 0 + + renderedContent = renderedContent.replace(/<[^>]+>/g, (match) => { + const placeholder = `__HTML_${htmlIndex}__` + htmlPlaceholders[placeholder] = match + htmlIndex++ + return placeholder + }) + + // 步骤3: 处理纯文本部分 - 换行和对白高亮 + // 将换行转换为
+ renderedContent = renderedContent.replace(/\n/g, '
') + + // 高亮对白(只匹配单行内的引号) + renderedContent = renderedContent.replace(/([""「『])([^""」』\n<]*?)([""」』])|(")(.*?)(")|(')([^'\n<]*?)(')/g, (_match, q1, t1, q2, q3, t2, q4, q5, t3, q6) => { + const quote1 = q1 || q3 || q5 + const text = t1 || t2 || t3 + const quote2 = q2 || q4 || q6 + + if (!text) return _match + + return `${quote1}${text}${quote2}` + }) + + // 步骤4: 恢复 HTML 标签 + Object.keys(htmlPlaceholders).forEach(placeholder => { + renderedContent = renderedContent.replace(placeholder, htmlPlaceholders[placeholder]) + }) + + // 步骤5: 恢复代码块 + Object.keys(codeBlockPlaceholders).forEach(placeholder => { + renderedContent = renderedContent.replace(placeholder, codeBlockPlaceholders[placeholder]) }) } - setDisplayContent(renderedContent) - setHasHtml(hasHtmlContent) + setDisplayContent(renderedContent) + setRemainingText('') + + setHasHtml(hasHtmlTags || hasCodeBlocks) setHasScript(hasScriptContent) // 如果有 HTML 内容或脚本,自动启用 - if (hasHtmlContent || hasScriptContent) { + if (hasHtmlTags || hasCodeBlocks || hasScriptContent) { setAllowScript(true) console.log('[MessageContent] 自动启用脚本') } @@ -381,16 +416,19 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag /> ) : hasHtml ? ( - // 有 HTML 但无脚本或脚本未启用时,直接渲染 HTML -
-
-
+ // 有 HTML 内容时,直接渲染(HTML 已经在原位置) +
) : ( -
- + // 纯文本内容 - 使用 DialogueText 高亮对白,保持换行 +
+ {displayContent.split('\n').map((line, index) => ( +
+ {line ? :
} +
+ ))}
)}