optimize ux think mode
This commit is contained in:
@@ -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(/ /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;
|
||||
|
||||
Reference in New Issue
Block a user