🐛 修复前端渲染的bug,优化html渲染和文本内容的显示格式
Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user