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:
1 parent
3cb468f4e8
commit
76341dc42b
14 files changed
+349
-121
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user