feat: Stream-Abbruch per Stop-Button
- Neuer Stop-Button (#stop) neben Send-Button, erscheint nur während Stream - AbortController in chat.js: abort() schließt SSE-Verbindung → Backend CancellationToken triggered - apiChat() akzeptiert optionales AbortSignal - AbortError-Handling: zeigt 'Abgebrochen' statt Fehlermeldung - Button-Toggle in sendMessage(), stopStreaming(), startStreamingPoll(), Page-Load-Recovery - CSS: .stop-btn mit --dangerous Farbe - Diverse kleine UI-Fixes (TODO.md, theme.css box-spacing, settings-section Layout)
This commit is contained in:
1 parent
190a0d0a8b
commit
4bf500ad47
8 files changed
+73
-22
No files matched your search
+3
-2
@@ -1,10 +1,11 @@
|
||||
// ── API Calls ──
|
||||
|
||||
async function apiChat(messages, sessionId) {
|
||||
async function apiChat(messages, sessionId, signal) {
|
||||
return fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages, sessionId })
|
||||
body: JSON.stringify({ messages, sessionId }),
|
||||
signal
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+33
-12
@@ -53,6 +53,7 @@ let pendingRefs = []; // [{ path, from, to }] - Workspace-Dateireferenzen
|
||||
let pendingReply = null; // { index, role, text } - Zitierte Nachricht
|
||||
let _lastDoneChainData = null; // Chain-Daten vom letzten done-Event
|
||||
let _chatAbandoned = false; // true wenn User weg-navigiert während Stream läuft
|
||||
let _abortController = null; // AbortController für laufenden Stream
|
||||
|
||||
async function clearChat() {
|
||||
const archive = await apiArchive(getCurrentSessionId());
|
||||
@@ -70,6 +71,12 @@ async function clearChat() {
|
||||
showArchiveLoadButton();
|
||||
}
|
||||
|
||||
function stopStreaming() {
|
||||
if (_abortController) {
|
||||
_abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Send Message ──
|
||||
|
||||
async function sendMessage() {
|
||||
@@ -129,7 +136,11 @@ async function sendMessage() {
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('input'))
|
||||
btn.disabled = true;
|
||||
btn.hidden = true;
|
||||
const stopBtn = $('#stop');
|
||||
stopBtn.hidden = false;
|
||||
isStreaming = true;
|
||||
_abortController = new AbortController();
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
|
||||
|
||||
let fullText = '';
|
||||
@@ -138,7 +149,7 @@ async function sendMessage() {
|
||||
_chatAbandoned = false;
|
||||
|
||||
try {
|
||||
const resp = await apiChat(messages, getCurrentSessionId());
|
||||
const resp = await apiChat(messages, getCurrentSessionId(), _abortController.signal);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
@@ -228,23 +239,33 @@ async function sendMessage() {
|
||||
// Bei Navigation nichts tun – der neue Page-Init übernimmt
|
||||
if (_chatAbandoned) return;
|
||||
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><i class="fa fa-hourglass-half"></i> Streaming... (Verbindung wiederhergestellt)</span>');
|
||||
startStreamingPoll(getCurrentSessionId(), btn, status.chainId);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
addErrorBubble('Netzwerk-Fehler: ' + e.message);
|
||||
// User hat Stream abgebrochen
|
||||
if (e.name === 'AbortError') {
|
||||
setMeta('Abgebrochen');
|
||||
// Kurz warten damit Backend den Disconnect merkt
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
} else {
|
||||
// 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><i class="fa fa-hourglass-half"></i> Streaming... (Verbindung wiederhergestellt)</span>');
|
||||
startStreamingPoll(getCurrentSessionId(), btn, status.chainId);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
addErrorBubble('Netzwerk-Fehler: ' + e.message);
|
||||
}
|
||||
} finally {
|
||||
// Bei Navigation nichts aufräumen – der neue Page-Init übernimmt
|
||||
if (_chatAbandoned) return;
|
||||
if (!isStreaming || !_streamingPollInterval) {
|
||||
btn.disabled = false;
|
||||
btn.hidden = false;
|
||||
stopBtn.hidden = true;
|
||||
isStreaming = false;
|
||||
_abortController = null;
|
||||
}
|
||||
endStreamBubble();
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ function startStreamingPoll(sessionId, sendBtn, chainId) {
|
||||
clearInterval(_streamingPollInterval);
|
||||
_streamingPollInterval = null;
|
||||
isStreaming = false;
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
_abortController = null;
|
||||
if (sendBtn) { sendBtn.disabled = false; sendBtn.hidden = false; }
|
||||
const stopEl = document.getElementById('stop');
|
||||
if (stopEl) stopEl.hidden = true;
|
||||
|
||||
// Finale Nachrichten nochmal laden
|
||||
await pollHistory(true);
|
||||
@@ -166,9 +169,11 @@ registerPage('chat', {
|
||||
backBtn.addEventListener('click', () => navigateTo('dashboard'));
|
||||
}
|
||||
|
||||
// Send
|
||||
// Send / Stop
|
||||
const sendBtn = document.getElementById('send');
|
||||
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
|
||||
const stopBtn = document.getElementById('stop');
|
||||
if (stopBtn) stopBtn.addEventListener('click', stopStreaming);
|
||||
|
||||
const inputEl = document.getElementById('input');
|
||||
if (inputEl) {
|
||||
@@ -255,7 +260,9 @@ registerPage('chat', {
|
||||
const status = await apiIsStreaming(sessionId);
|
||||
if (status.streaming) {
|
||||
isStreaming = true;
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
if (sendBtn) { sendBtn.disabled = true; sendBtn.hidden = true; }
|
||||
const stopEl = document.getElementById('stop');
|
||||
if (stopEl) stopEl.hidden = false;
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
|
||||
startStreamingPoll(sessionId, sendBtn, status.chainId);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user