Improved codeblock-markdown-parser for a better experiencing when the LLM responds with code in Stream-mode
This commit is contained in:
35
src/App.jsx
35
src/App.jsx
@@ -111,32 +111,27 @@ export default function App() {
|
||||
const [editingMessageIndex, setEditingMessageIndex] = useState(null);
|
||||
const [editText, setEditText] = useState('');
|
||||
// Helpers + handlers for message copy/edit/regenerate (must live inside App)
|
||||
function getVisibleTextForCopy(message) {
|
||||
function getMarkdownForCopy(message) {
|
||||
const raw = message.content || '';
|
||||
if (message.role !== 'assistant') {
|
||||
// For user messages, copy exactly what they typed.
|
||||
return raw;
|
||||
}
|
||||
// Assistant messages: strip think, render as HTML, then extract readable text.
|
||||
let shown = raw;
|
||||
try {
|
||||
const { think, answer } = splitThinkBlocks(raw);
|
||||
shown = answer || raw;
|
||||
} catch (_) {}
|
||||
|
||||
const html = markdownToHTML(shown);
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
tempDiv.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
|
||||
tempDiv.querySelectorAll('p,div,li,pre,code,h1,h2,h3,h4,h5,h6,table,tr').forEach(el => {
|
||||
el.insertAdjacentText('beforeend', '\n');
|
||||
});
|
||||
return tempDiv.innerText.replace(/\n{3,}/g, '\n\n').trim();
|
||||
if (message.role === 'assistant') {
|
||||
// Copy the assistant's raw *markdown answer*, not rendered text,
|
||||
// and strip any <think>...</think> block.
|
||||
try {
|
||||
const { answer } = splitThinkBlocks(raw);
|
||||
return (answer || raw).trim();
|
||||
} catch {
|
||||
return raw.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// User messages: copy exactly as typed
|
||||
return raw;
|
||||
}
|
||||
|
||||
async function handleCopyMessage(message) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(getVisibleTextForCopy(message));
|
||||
await navigator.clipboard.writeText(getMarkdownForCopy(message));
|
||||
} catch (err) {
|
||||
console.error('Failed to copy message:', err);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export function markdownToHTML(text) {
|
||||
// </think> or </thinking> tag is found, OR the end of the string ($).
|
||||
text = text.replace(/<think(?:ing)?>[\s\S]*?(?:<\/think(?:ing)?>|$)/gi, '');
|
||||
|
||||
text = balanceStreamingCodeFence(text);
|
||||
|
||||
// 1) Normalize line endings
|
||||
let tmp = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
@@ -164,4 +166,45 @@ export function markdownToHTML(text) {
|
||||
.replace(/(<div class="codeblock"[^>]*>[\s\S]*?<\/div>)\s*<br>/g, "$1"); // <br> right after closing
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Virtually close an unfinished fenced code block so it renders during streaming.
|
||||
function balanceStreamingCodeFence(md) {
|
||||
// Split into lines; we only consider fences that start a line.
|
||||
const lines = md.split(/\r?\n/);
|
||||
|
||||
// Track the last unmatched opening fence we see while scanning.
|
||||
// { fenceChar: '`' or '~', fenceLen: number }
|
||||
let open = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Opening fence? ^\s*([`~]{3,})(.*)$
|
||||
if (!open) {
|
||||
const m = line.match(/^\s*([`~]{3,})([^\s]*)?.*$/);
|
||||
if (m) {
|
||||
// Treat as an opening fence
|
||||
open = { fenceChar: m[1][0], fenceLen: m[1].length };
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Closing fence: must match same char and length or longer
|
||||
const re = new RegExp(`^\\s*(${open.fenceChar}{${open.fenceLen},})\\s*$`);
|
||||
if (re.test(line)) {
|
||||
// Closed
|
||||
open = null;
|
||||
continue;
|
||||
}
|
||||
// Otherwise still inside the code block; keep scanning
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
// Virtually close with the same fence so the block renders now
|
||||
const virtual = `${open.fenceChar.repeat(open.fenceLen)}`;
|
||||
return md.endsWith('\n') ? md + virtual : md + '\n' + virtual;
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
Reference in New Issue
Block a user