optimize ux think mode

This commit is contained in:
team 1
2026-04-11 20:55:42 +02:00
parent 521f8bd5a3
commit 6559f87dbc
3 changed files with 280 additions and 39 deletions

View File

@@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
const chatEl = document.getElementById('chat');
const chatEl = document.getElementById('chat');
const promptEl = document.getElementById('prompt');
const sendBtn = document.getElementById('send');
const sendBtn = document.getElementById('send');
const abortBtn = document.getElementById('abort');
const clearBtn = document.getElementById('clear');
@@ -32,14 +32,165 @@ document.addEventListener('DOMContentLoaded', () => {
return addMessage('assistant', 'AI is thinking…', 'loader');
}
function cleanupEmptyBlocks(container) {
container.querySelectorAll('p, div, li, blockquote').forEach((el) => {
const html = el.innerHTML
.replace(/<br\s*\/?>/gi, '')
.replace(/&nbsp;/gi, '')
.trim();
const text = (el.textContent || '').trim();
if (html === '' || text === '') {
el.remove();
}
});
}
function stripAllThinkContent(container) {
const blockSelector = 'p, div, li, blockquote';
const thinkSpans = Array.from(container.querySelectorAll('.think'));
if (thinkSpans.length === 0) {
return;
}
const handledBlocks = new Set();
thinkSpans.forEach((span) => {
const block = span.closest(blockSelector) || span.parentElement;
if (!block || handledBlocks.has(block)) {
return;
}
handledBlocks.add(block);
const thinksInBlock = Array.from(block.querySelectorAll('.think'));
const lastThinkInBlock = thinksInBlock[thinksInBlock.length - 1];
if (!lastThinkInBlock) {
return;
}
let node = block.firstChild;
while (node) {
const next = node.nextSibling;
node.remove();
if (node === lastThinkInBlock) {
break;
}
node = next;
}
while (
block.firstChild &&
(
(block.firstChild.nodeType === Node.TEXT_NODE &&
block.firstChild.textContent.trim() === '') ||
(block.firstChild.nodeType === Node.ELEMENT_NODE &&
block.firstChild.tagName === 'BR')
)
) {
block.firstChild.remove();
}
});
cleanupEmptyBlocks(container);
}
function hasNonThinkContent(container) {
const clone = container.cloneNode(true);
stripAllThinkContent(clone);
cleanupEmptyBlocks(clone);
if ((clone.textContent || '').trim() !== '') {
return true;
}
return clone.querySelector(
'img, table, pre, code, ul, ol, h1, h2, h3, h4, h5, h6, a'
) !== null;
}
function cleanupThinkSpans(container) {
if (!container) {
return;
}
const thinkSpans = Array.from(container.querySelectorAll('.think'));
if (thinkSpans.length === 0) {
return;
}
if (hasNonThinkContent(container)) {
stripAllThinkContent(container);
return;
}
if (thinkSpans.length <= 1) {
return;
}
const blockSelector = 'p, div, li, blockquote';
const lastThink = thinkSpans[thinkSpans.length - 1];
const lastBlock = lastThink.closest(blockSelector) || lastThink.parentElement;
thinkSpans.slice(0, -1).forEach((span) => {
const block = span.closest(blockSelector) || span.parentElement;
if (block && block !== lastBlock) {
block.remove();
return;
}
if (block === lastBlock) {
span.remove();
}
});
if (lastBlock && lastBlock.contains(lastThink)) {
let node = lastBlock.firstChild;
while (node && node !== lastThink) {
const next = node.nextSibling;
node.remove();
node = next;
}
while (
lastThink.nextSibling &&
(
(lastThink.nextSibling.nodeType === Node.TEXT_NODE &&
lastThink.nextSibling.textContent.trim() === '') ||
(lastThink.nextSibling.nodeType === Node.ELEMENT_NODE &&
lastThink.nextSibling.tagName === 'BR')
)
) {
lastThink.nextSibling.remove();
}
}
cleanupEmptyBlocks(container);
}
function renderBubbleContent(bubble, raw) {
bubble.innerHTML = renderMarkdown(raw);
cleanupThinkSpans(bubble);
chatEl.scrollTop = chatEl.scrollHeight;
}
async function loadHistory() {
try {
const res = await fetch('/history');
if (!res.ok) return;
const messages = await res.json();
messages.forEach(m =>
addMessage(m.role, renderMarkdown(m.text))
);
messages.forEach(m => addMessage(m.role, renderMarkdown(m.text)));
} catch {}
}
@@ -47,8 +198,8 @@ document.addEventListener('DOMContentLoaded', () => {
promptEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // verhindert normalen Zeilenumbruch
sendBtn.click(); // löst vorhandene Send-Logik aus
e.preventDefault();
sendBtn.click();
}
});
@@ -64,14 +215,11 @@ document.addEventListener('DOMContentLoaded', () => {
let firstChunk = true;
let renderTimer = null;
// 🔥 LÖSUNG: Throttled Rendering - maximal alle 100ms
function scheduleRender() {
if (renderTimer) return;
renderTimer = setTimeout(() => {
// Der StreamChunker sendet bereits korrekt strukturierte Chunks
bubble.innerHTML = renderMarkdown(raw);
chatEl.scrollTop = chatEl.scrollHeight;
renderBubbleContent(bubble, raw);
renderTimer = null;
}, 100);
}
@@ -101,14 +249,12 @@ document.addEventListener('DOMContentLoaded', () => {
const chunk = decoder.decode(value, { stream: true });
sseBuffer += chunk;
// Parse SSE Events (können mehrere data:-Zeilen haben)
const events = sseBuffer.split('\n\n');
sseBuffer = events.pop() || ''; // Letzten unvollständigen Event behalten
sseBuffer = events.pop() || '';
events.forEach(event => {
if (!event.trim()) return;
// Sammle alle "data:"-Zeilen und füge \n wieder ein
const dataLines = event
.split('\n')
.filter(line => line.startsWith('data: '))
@@ -116,20 +262,15 @@ document.addEventListener('DOMContentLoaded', () => {
if (dataLines.length === 0) return;
// Verbinde mit \n (so wie es vom Backend kam)
const text = dataLines.join('\n');
if (text === '[DONE]') {
// 🔥 Final render flush
if (renderTimer) {
clearTimeout(renderTimer);
renderTimer = null;
}
bubble.innerHTML = renderMarkdown(raw);
chatEl.scrollTop = chatEl.scrollHeight;
renderBubbleContent(bubble, raw);
abort = true;
return;
}
@@ -141,16 +282,17 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('ai-cloud')?.classList.add('d-none');
}
// Text sammeln und verzögert rendern
raw += text;
scheduleRender();
});
}
} catch {
bubble.innerHTML += '<br><em>Error occurred.</em>';
} finally {
if (renderTimer) clearTimeout(renderTimer);
if (renderTimer) {
clearTimeout(renderTimer);
}
sendBtn.disabled = false;
abortBtn.disabled = true;
clearBtn.disabled = false;