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:
Meleki committed 2026-03-02 06:52:26 +01:00
1 parent 190a0d0a8b
commit 4bf500ad47
8 files changed
+73 -22

No files matched your search

+10 -3
View File
@@ -1,13 +1,15 @@
# TODOs
## Features
- [ ] Skills: In Datenbank anlegen können, mit Pfad zu Skill-Dateien; im Frontend am Dashboard dargestellt. Zuweisung an Agent und/oder Session.
- [x] Skills: In Datenbank anlegen können, mit Pfad zu Skill-Dateien; im Frontend am Dashboard dargestellt. Zuweisung an Agent und/oder Session.
- [x] System-Prompt: Nachrichten einer Session als Referenz und somit in den System-Prompt aufnehmen
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
- [ ] Subagents: Möglichkeit Subagents zu starten und zu orchestrieren
- [x] Im Chat: Menüpunkt "Datenbank sichern" entfernen und stattdessen in den Einstellungen platzieren. Außerdem soll nun nicht mehr nur die Datenbank gesichert werden, sondern der gesamte data-Ordner (der Unterordner backup muss natürlich exkludiert werden)
- [x] Agents-Formular (nur beim Bearbeiten von vorhandenen Agent): Button der das File-Explorer Modal öffnet bei dem ich den Agent-Ordner sehe.
- [x] Chat: Maximal 500 Nachrichten laden. Dauert sonst zu lange und der Browser wird langsam
- [ ] User-Management: Tabelle in Datenbank mit username / password. Wenn kein User vorhanden: dann admin mit password "admin" anlegen
- [ ] Neuer Message-Tag: `[canvas:<title>:<height (optional)>]...[/canvas]`
- [ ] Websearch Tool?
- [ ] Anfrage/Stream abbrechen
## Bugs
- [x] Wenn gerade gestreamt wird und man refreshed die Seite, dann verliert das Frontend den Status "Streaming..." und ich kann eine Nachricht schreiben, obwohl das nicht möglich sein sollte
@@ -21,3 +23,8 @@
- [x] Chat: User-Nachrichten mit dem Tag [Datei: pfat/zur/datei.xyz:1-10] wird nicht als HTML geparsed
- [x] Chat: Wird zwar erkannt und ist klickbar, aber es öffnet sich einfach nur der File Explorer, ohne dass sich die Datei öffnet und die Darstellung in Monospace ist auch nicht schön
- [x] Chat: Es werden momentan alle Tool-Calls angezeigt - die Anzeige soll individuell in den Einstellungen gemacht werden können. Auswahl aus drei Stufen: "Alle anzeigen" / "Nur Modifikationen" (file änderungen, memory änderungen, etc.) / "Verstecken"
- [ ] Browser: "Zurück"-Taste soll auf den vorherigen location.hash wechseln
## Ideas / Backlog
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
- [ ] Subagents: Möglichkeit Subagents zu starten und zu orchestrieren
+8
View File
@@ -240,6 +240,14 @@
.stop-btn {
color: var(--dangerous);
}
.stop-btn:hover {
background: rgb(from var(--dangerous) r g b / 0.15);
}
.input-area textarea {
flex: 1;
min-height: 0;
+7 -1
View File
@@ -8,8 +8,13 @@
gap: var(--page-spacing);
}
.settings-section {
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.settings-section h2 {
margin-bottom: var(--box-spacing);
padding-bottom: var(--badge-spacing);
border-bottom: var(--border-width) solid var(--f2-border);
}
@@ -73,4 +78,5 @@
.settings-reset {
border-top: var(--border-width) solid var(--f2-border);
padding-top: var(--box-spacing);
flex-direction: row;
}
+1 -1
View File
@@ -22,7 +22,7 @@
--page-padding: 2em;
--page-spacing: 2em;
--box-padding: .75em 1em;
--box-spacing: 1em;
--box-spacing: 1.5em;
--input-padding: .45em .7em;
--button-padding: .45em .8em;
--badge-padding: .2em .6em;
+1
View File
@@ -186,6 +186,7 @@
<button id="explorerBtn" class="icon-btn" title="Workspace durchsuchen"><i class="fa fa-folder"></i></button>
<textarea id="input" placeholder="Nachricht..." rows="1"></textarea>
<button id="send" class="icon-btn"><i class="fa fa-paper-plane"></i></button>
<button id="stop" class="icon-btn stop-btn" hidden title="Antwort abbrechen"><i class="fa fa-stop"></i></button>
</div>
</div>
</div>
+3 -2
View File
@@ -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
View File
@@ -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();
+10 -3
View File
@@ -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);
}