fix: strip markdown for TTS + render rich markdown in chat UI
- _md_to_speech(): AST-based markdown stripping via markdown-it-py - Truncate TTS input at 450 chars on sentence boundary (chatterbox overflow) - chat.js: render assistant messages as markdown via marked.js + DOMPurify - Typewriter accumulates raw text, renders markdown progressively
This commit is contained in:
+21
-2
@@ -21,9 +21,22 @@ let ws = null;
|
||||
let connectAttempts = 0;
|
||||
let lastSpeaker = null; // 'user' | 'bt' | 'system' | null
|
||||
let currentBtBody = null; // active streaming .msg__body element
|
||||
let currentBtRaw = ''; // raw markdown accumulator for streaming
|
||||
let queue = [];
|
||||
let draining = false;
|
||||
|
||||
// Configure marked for safe markdown rendering
|
||||
if (typeof marked !== 'undefined') {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
}
|
||||
|
||||
function renderMarkdown(raw) {
|
||||
if (typeof marked === 'undefined') return raw;
|
||||
const html = marked.parse(raw);
|
||||
if (typeof DOMPurify !== 'undefined') return DOMPurify.sanitize(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
function speakerLabel(role) {
|
||||
@@ -97,7 +110,9 @@ async function drain() {
|
||||
if (!currentBtBody) return;
|
||||
draining = true;
|
||||
while (queue.length) {
|
||||
currentBtBody.textContent += queue.shift();
|
||||
currentBtRaw += queue.shift();
|
||||
// Re-render markdown on each char (marked.parse is fast enough)
|
||||
currentBtBody.innerHTML = renderMarkdown(currentBtRaw);
|
||||
if (Math.random() < 0.04) scrollToBottom();
|
||||
await sleep(TYPEWRITER_MS);
|
||||
}
|
||||
@@ -107,16 +122,19 @@ async function drain() {
|
||||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
|
||||
function finishBt() {
|
||||
// wait for the queue to drain before adding caret
|
||||
// wait for the queue to drain before final render
|
||||
const tick = () => {
|
||||
if (draining || queue.length) { setTimeout(tick, 30); return; }
|
||||
if (!currentBtBody) return;
|
||||
// Final markdown render with complete text
|
||||
currentBtBody.innerHTML = renderMarkdown(currentBtRaw);
|
||||
const caret = document.createElement('span');
|
||||
caret.className = 'caret';
|
||||
currentBtBody.appendChild(caret);
|
||||
scrollToBottom();
|
||||
setTimeout(() => caret.remove(), 900);
|
||||
currentBtBody = null;
|
||||
currentBtRaw = '';
|
||||
};
|
||||
tick();
|
||||
}
|
||||
@@ -188,6 +206,7 @@ function handleMessage(msg) {
|
||||
if (!currentBtBody) {
|
||||
removeThinking();
|
||||
currentBtBody = makeMsg('bt');
|
||||
currentBtRaw = '';
|
||||
}
|
||||
if (msg.delta) enqueue(msg.delta);
|
||||
if (msg.done) finishBt();
|
||||
|
||||
Reference in New Issue
Block a user