🐛 修复前端渲染的bug,优化html渲染和文本内容的显示格式

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-03-02 02:55:55 +08:00
parent d0582f6aad
commit 4100d908da

View File

@@ -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<Choice[]>([])
const [displayContent, setDisplayContent] = useState(content)
const [remainingText, setRemainingText] = useState('')
const [statusPanel, setStatusPanel] = useState<any>(null)
const iframeRef = useRef<HTMLIFrameElement>(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 = /<script[\s\S]*?<\/script>/gi
const hasHtmlContent = htmlRegex.test(finalContent)
const hasScriptContent = scriptRegex.test(finalContent)
console.log('[MessageContent] hasHtml:', hasHtmlContent, 'hasScript:', hasScriptContent)
// 对于含 HTML 的内容,将剩余的 markdown 代码块(``` ... ```)转换为 <pre><code>
// 避免反引号作为纯文本出现在 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
const langLabel = lang ? `<span style="font-size:11px;color:rgba(255,255,255,0.4);display:block;margin-bottom:4px;">${lang}</span>` : ''
return `<pre style="background:rgba(0,0,0,0.35);padding:10px 12px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.5;margin:8px 0;">${langLabel}<code>${escaped}</code></pre>`
// 如果包含 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 {
// 否则转换为 <pre><code>
const escaped = trimmedCode
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
const langLabel = lang ? `<span style="font-size:11px;color:rgba(255,255,255,0.4);display:block;margin-bottom:4px;">${lang}</span>` : ''
codeBlockPlaceholders[placeholder] = `<pre style="background:rgba(0,0,0,0.35);padding:10px 12px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.5;margin:8px 0;">${langLabel}<code>${escaped}</code></pre>`
}
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: 处理纯文本部分 - 换行和对白高亮
// 将换行转换为 <br>
renderedContent = renderedContent.replace(/\n/g, '<br>')
// 高亮对白(只匹配单行内的引号)
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 `<span style="color:rgba(139,92,246,0.6)">${quote1}</span><span style="color:rgb(139,92,246);font-weight:500;padding:0 2px">${text}</span><span style="color:rgba(139,92,246,0.6)">${quote2}</span>`
})
// 步骤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
/>
</div>
) : hasHtml ? (
// 有 HTML 但无脚本或脚本未启用时,直接渲染 HTML
<div className="w-full border border-white/10 rounded-lg bg-black/20 overflow-hidden">
<div
className="prose prose-invert max-w-none text-sm leading-relaxed break-words overflow-wrap-anywhere p-4"
dangerouslySetInnerHTML={{ __html: displayContent }}
/>
</div>
// 有 HTML 内容时,直接渲染HTML 已经在原位置)
<div
className="prose prose-invert max-w-none text-sm leading-relaxed break-words overflow-wrap-anywhere"
dangerouslySetInnerHTML={{ __html: displayContent }}
/>
) : (
<div className="text-sm leading-relaxed whitespace-pre-wrap break-words overflow-wrap-anywhere">
<DialogueText text={displayContent} />
// 纯文本内容 - 使用 DialogueText 高亮对白,保持换行
<div className="text-sm leading-relaxed break-words overflow-wrap-anywhere">
{displayContent.split('\n').map((line, index) => (
<div key={index} className="min-h-[1.5em]">
{line ? <DialogueText text={line} /> : <br />}
</div>
))}
</div>
)}