Fix: Chat scrollt nach Font-Loading nochmal nach unten

scrollDown() wartet jetzt auf document.fonts.ready und scrollt erneut.
chat-page.js wartet nach loadHistory() zusätzlich per requestAnimationFrame
auf den Repaint nach Font-Swap.
This commit is contained in:
Meleki committed 2026-02-28 15:54:47 +01:00
1 parent 3cb468f4e8
commit 76341dc42b
14 files changed
+349 -121

No files matched your search

+7 -4
View File
@@ -331,14 +331,17 @@
/* File Ref Chips in User-Bubbles */
.file-ref-chip {
display: inline-block;
padding: var(--badge-padding);
display: inline-flex;
align-items: center;
gap: .3em;
padding: .15em .5em;
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
font-family: var(--mono);
font-size: .85em;
font-size: .9em;
white-space: nowrap;
vertical-align: baseline;
transition: background .15s, border-color .15s;
}
.file-ref-chip:hover {
+11
View File
@@ -111,3 +111,14 @@ async function apiExecCommand(sessionId, command) {
body: JSON.stringify({ command })
});
}
async function apiIsStreaming(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/streaming`);
return resp.json();
}
async function apiGetChain(chainId) {
const resp = await fetch(`/api/chains/${chainId}`);
if (!resp.ok) return null;
return resp.json();
}
+14 -2
View File
@@ -149,10 +149,22 @@ async function sendMessage() {
} catch (e) {
endStreamBubble();
// Prüfen ob Backend noch streamt (z.B. bei Netzwerkfehler)
try {
const status = await apiIsStreaming(getCurrentSessionId());
if (status.streaming) {
// Backend streamt noch weiter – Recovery-Poll starten
setMetaHtml('<span>⏳ Streaming... (Verbindung wiederhergestellt)</span>');
startStreamingPoll(getCurrentSessionId(), btn, status.chainId);
return;
}
} catch (_) {}
addErrorBubble('Netzwerk-Fehler: ' + e.message);
} finally {
btn.disabled = false;
isStreaming = false;
if (!isStreaming || !_streamingPollInterval) {
btn.disabled = false;
isStreaming = false;
}
endStreamBubble();
// Erst neue aus DB laden, dann gestreamte Elemente ersetzen
+26 -2
View File
@@ -12,7 +12,8 @@ function openFileExplorer(opts = {}) {
onSelect: opts.onSelect || null,
currentPath: '',
history: [],
selectedFile: null
selectedFile: null,
initialFile: opts.initialFile || null // z.B. "src/foo.cs" – Datei direkt öffnen
};
const overlay = document.createElement('div');
@@ -55,7 +56,14 @@ function openFileExplorer(opts = {}) {
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
document.addEventListener('keydown', _feState._keyHandler);
feLoadDir('');
// Zum Verzeichnis der initialFile navigieren, oder Root
if (_feState.initialFile) {
const filePath = _feState.initialFile;
const dir = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : '';
feLoadDir(dir);
} else {
feLoadDir('');
}
}
function closeFileExplorer() {
@@ -140,6 +148,22 @@ async function feLoadDir(path) {
}
backBtn.disabled = path === '';
// Wenn initialFile gesetzt, Datei automatisch selektieren
if (_feState && _feState.initialFile) {
const targetPath = _feState.initialFile;
_feState.initialFile = null; // Nur einmal triggern
// data-path Selektor sicher aufbauen
const allFiles = sidebar.querySelectorAll('.fe-file');
let targetItem = null;
allFiles.forEach(el => { if (el.dataset.path === targetPath) targetItem = el; });
if (targetItem) {
const entry = entries.find(e => e.path === targetPath);
if (entry) {
feSelectFile(entry, targetItem);
}
}
}
} catch (e) {
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
+2 -2
View File
@@ -185,8 +185,8 @@ function stopHistoryPolling() {
}
}
async function pollHistory() {
if (isStreaming) return;
async function pollHistory(force = false) {
if (isStreaming && !force) return;
try {
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId);
+67
View File
@@ -1,5 +1,50 @@
// ── Chat Page ──
let _streamingPollInterval = null;
let _recoveryChainId = null;
function startStreamingPoll(sessionId, sendBtn, chainId) {
_recoveryChainId = chainId;
if (_streamingPollInterval) clearInterval(_streamingPollInterval);
_streamingPollInterval = setInterval(async () => {
try {
// Neue Nachrichten laden (inkl. der noch streamenden)
await pollHistory(true);
const status = await apiIsStreaming(sessionId);
if (!status.streaming) {
clearInterval(_streamingPollInterval);
_streamingPollInterval = null;
isStreaming = false;
if (sendBtn) sendBtn.disabled = false;
// Finale Nachrichten nochmal laden
await pollHistory(true);
// Chain-Daten laden für Meta-Info
const cid = _recoveryChainId || status.chainId;
if (cid) {
const chain = await apiGetChain(cid);
if (chain) {
let meta = `Model: ${chain.model || '?'} | In: ${chain.inputTokens || 0} | Out: ${chain.outputTokens || 0}`;
if (chain.iterations > 1) meta += ` | Iterations: ${chain.iterations}`;
if (chain.durationMs) meta += ` | ${(chain.durationMs / 1000).toFixed(1)}s`;
if (chain.stopReason && chain.stopReason !== 'end_turn' && chain.stopReason !== 'tool_use') meta += ` | Stop: ${chain.stopReason}`;
setMeta(meta);
} else {
setMeta('Bereit');
}
} else {
setMeta('Bereit');
}
_recoveryChainId = null;
}
} catch (e) {
console.error('Streaming-Poll fehlgeschlagen:', e);
}
}, 1500);
}
registerPage('chat', {
async init(params) {
const sessionId = params.id;
@@ -152,6 +197,24 @@ registerPage('chat', {
isStreaming = false;
await loadHistory();
// Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern)
document.fonts.ready.then(() => {
requestAnimationFrame(() => scrollDown());
});
// Prüfen ob Backend gerade noch streamt (z.B. nach Page-Refresh)
try {
const status = await apiIsStreaming(sessionId);
if (status.streaming) {
isStreaming = true;
if (sendBtn) sendBtn.disabled = true;
setMetaHtml('<span>⏳ Streaming...</span>');
startStreamingPoll(sessionId, sendBtn, status.chainId);
}
} catch (e) {
console.error('Streaming-Status prüfen fehlgeschlagen:', e);
}
},
destroy() {
@@ -159,6 +222,10 @@ registerPage('chat', {
document.removeEventListener('click', this._closeMenu);
this._closeMenu = null;
}
if (_streamingPollInterval) {
clearInterval(_streamingPollInterval);
_streamingPollInterval = null;
}
stopHistoryPolling();
messages.length = 0;
lastKnownMessageId = 0;
+12 -2
View File
@@ -15,7 +15,12 @@ function renderMarkdown(text) {
function scrollDown() {
const chat = chatEl();
if (!chat) return;
chat.scrollTop = chat.scrollHeight;
// Nach Font-Swap kann sich die Höhe ändern → nochmal scrollen
document.fonts.ready.then(() => {
chat.scrollTop = chat.scrollHeight;
});
}
function esc(s) {
@@ -45,13 +50,18 @@ function renderUserContent(container, text) {
}
const ref = match[1]; // z.B. "src/foo.cs:10-25"
// Pfad ohne Zeilenangabe extrahieren
const filePath = ref.replace(/:\d+(-\d+)?$/, '');
const chip = document.createElement('span');
chip.className = 'file-ref-chip';
chip.textContent = match[0];
// Schöne Darstellung: Icon + Kurzpfad
const fileName = filePath.includes('/') ? filePath.substring(filePath.lastIndexOf('/') + 1) : filePath;
const lineInfo = ref.replace(filePath, '').trim(); // z.B. ":10-25"
chip.textContent = '📄 ' + fileName + lineInfo;
chip.title = ref;
chip.style.cursor = 'pointer';
chip.addEventListener('click', () => {
openFileExplorer({ sessionId: getCurrentSessionId() });
openFileExplorer({ sessionId: getCurrentSessionId(), initialFile: filePath });
});
frag.appendChild(chip);
last = match.index + match[0].length;