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;
|
||||
|
||||
@@ -371,4 +371,42 @@ body {
|
||||
}
|
||||
.text-glow{
|
||||
text-shadow: #86b7fe 2px 1px 14px;
|
||||
}
|
||||
|
||||
span.think {
|
||||
color: #86b7fe;
|
||||
}
|
||||
|
||||
.think {
|
||||
display: inline-block;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0) 32%,
|
||||
rgba(255, 255, 255, 1) 50%,
|
||||
rgba(255, 255, 255, 0) 58%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
background-size: 180% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 220% 0;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: thinkScan 2.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes thinkScan {
|
||||
0% {
|
||||
background-position: 180% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -20% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.think {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user