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
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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