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';
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
let latexRenderTimer: number | null = null;
|
let latexRenderTimer: number | null = null;
|
||||||
|
let streamingCodeHighlightTimer: number | null = null;
|
||||||
const markdownCache = new Map<string, string>();
|
const markdownCache = new Map<string, string>();
|
||||||
let showHtmlDebugCount = 0;
|
let showHtmlDebugCount = 0;
|
||||||
const SHOW_HTML_DEBUG_MAX = 500;
|
const SHOW_HTML_DEBUG_MAX = 500;
|
||||||
@ -395,17 +396,14 @@ function buildCacheKey(text: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wrapCodeBlocks(html: string, isStreaming = false) {
|
function wrapCodeBlocks(html: string, isStreaming = false) {
|
||||||
if (isStreaming) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
return html.replace(
|
return html.replace(
|
||||||
/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g,
|
/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g,
|
||||||
(match, attributes, content) => {
|
(match, attributes, content) => {
|
||||||
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
|
const langMatch = attributes.match(/class="[^"]*language-([\w-]+)/);
|
||||||
const language = langMatch ? langMatch[1] : 'text';
|
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
|
const escapedContent = content
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
@ -413,7 +411,7 @@ function wrapCodeBlocks(html: string, isStreaming = false) {
|
|||||||
.replace(/"/g, '"');
|
.replace(/"/g, '"');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="code-block-wrapper">
|
<div class="code-block-wrapper"${streamingAttr} data-md-code-block="1">
|
||||||
<div class="code-block-header">
|
<div class="code-block-header">
|
||||||
<span class="code-language">${language}</span>
|
<span class="code-language">${language}</span>
|
||||||
<button class="copy-code-btn" data-code="${blockId}" title="复制代码" aria-label="复制代码"></button>
|
<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 {
|
function renderMarkdownToHtml(text: string): string {
|
||||||
const file = markdownProcessor.processSync(text);
|
const file = markdownProcessor.processSync(text);
|
||||||
return String(file);
|
return String(file);
|
||||||
@ -460,6 +478,10 @@ export function renderMarkdown(text: string, isStreaming = false) {
|
|||||||
}
|
}
|
||||||
html = wrapCodeBlocks(html, isStreaming);
|
html = wrapCodeBlocks(html, isStreaming);
|
||||||
|
|
||||||
|
if (isStreaming) {
|
||||||
|
scheduleStreamingCodeHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
if (!isStreaming && text.length < 10000) {
|
if (!isStreaming && text.length < 10000) {
|
||||||
const cacheKey = buildCacheKey(text);
|
const cacheKey = buildCacheKey(text);
|
||||||
markdownCache.set(cacheKey, html);
|
markdownCache.set(cacheKey, html);
|
||||||
|
|||||||
@ -1371,6 +1371,10 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block-wrapper[data-streaming='1'] {
|
||||||
|
min-height: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
.code-block-header {
|
.code-block-header {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
background: var(--theme-surface-strong);
|
background: var(--theme-surface-strong);
|
||||||
@ -1448,6 +1452,10 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block-wrapper[data-streaming='1'] pre {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.code-block-wrapper pre code {
|
.code-block-wrapper pre code {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user