feat(chat): render streaming code blocks
This commit is contained in:
parent
f383b0ec71
commit
f50d87fc79
@ -11,6 +11,7 @@ import rehypeStringify from 'rehype-stringify';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
let latexRenderTimer: number | null = null;
|
||||
let streamingCodeHighlightTimer: number | null = null;
|
||||
const markdownCache = new Map<string, string>();
|
||||
let showHtmlDebugCount = 0;
|
||||
const SHOW_HTML_DEBUG_MAX = 500;
|
||||
@ -395,17 +396,14 @@ function buildCacheKey(text: string): string {
|
||||
}
|
||||
|
||||
function wrapCodeBlocks(html: string, isStreaming = false) {
|
||||
if (isStreaming) {
|
||||
return html;
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
return html.replace(
|
||||
/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g,
|
||||
(match, attributes, content) => {
|
||||
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
|
||||
const langMatch = attributes.match(/class="[^"]*language-([\w-]+)/);
|
||||
const language = langMatch ? langMatch[1] : 'text';
|
||||
const blockId = `code-${stableHash(`${attributes}|${content}|${counter++}`)}`;
|
||||
const blockId = `code-${stableHash(`${attributes}|${counter++}`)}`;
|
||||
const streamingAttr = isStreaming ? ' data-streaming="1"' : '';
|
||||
const escapedContent = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@ -413,7 +411,7 @@ function wrapCodeBlocks(html: string, isStreaming = false) {
|
||||
.replace(/"/g, '"');
|
||||
|
||||
return `
|
||||
<div class="code-block-wrapper">
|
||||
<div class="code-block-wrapper"${streamingAttr} data-md-code-block="1">
|
||||
<div class="code-block-header">
|
||||
<span class="code-language">${language}</span>
|
||||
<button class="copy-code-btn" data-code="${blockId}" title="复制代码" aria-label="复制代码"></button>
|
||||
@ -424,6 +422,26 @@ function wrapCodeBlocks(html: string, isStreaming = false) {
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleStreamingCodeHighlight() {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (streamingCodeHighlightTimer !== null) return;
|
||||
|
||||
streamingCodeHighlightTimer = window.setTimeout(() => {
|
||||
streamingCodeHighlightTimer = null;
|
||||
|
||||
if (typeof Prism === 'undefined') return;
|
||||
|
||||
const codeBlocks = document.querySelectorAll('.code-block-wrapper[data-streaming="1"] pre code');
|
||||
codeBlocks.forEach((block) => {
|
||||
try {
|
||||
Prism.highlightElement(block as HTMLElement);
|
||||
} catch (error) {
|
||||
console.warn('流式代码高亮失败:', error);
|
||||
}
|
||||
});
|
||||
}, 120);
|
||||
}
|
||||
|
||||
function renderMarkdownToHtml(text: string): string {
|
||||
const file = markdownProcessor.processSync(text);
|
||||
return String(file);
|
||||
@ -460,6 +478,10 @@ export function renderMarkdown(text: string, isStreaming = false) {
|
||||
}
|
||||
html = wrapCodeBlocks(html, isStreaming);
|
||||
|
||||
if (isStreaming) {
|
||||
scheduleStreamingCodeHighlight();
|
||||
}
|
||||
|
||||
if (!isStreaming && text.length < 10000) {
|
||||
const cacheKey = buildCacheKey(text);
|
||||
markdownCache.set(cacheKey, html);
|
||||
|
||||
@ -1371,6 +1371,10 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.code-block-wrapper[data-streaming='1'] {
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
min-height: 44px;
|
||||
background: var(--theme-surface-strong);
|
||||
@ -1448,6 +1452,10 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.code-block-wrapper[data-streaming='1'] pre {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.code-block-wrapper pre code {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user