diff --git a/TODO.md b/TODO.md
index d39eb9d..16fb6b7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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:
:]...[/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
diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css
index dafc8c3..8eef8e8 100644
--- a/wwwroot/css/chat.css
+++ b/wwwroot/css/chat.css
@@ -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;
diff --git a/wwwroot/css/placeholder.css b/wwwroot/css/placeholder.css
index c508de1..7894262 100644
--- a/wwwroot/css/placeholder.css
+++ b/wwwroot/css/placeholder.css
@@ -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;
}
diff --git a/wwwroot/css/theme.css b/wwwroot/css/theme.css
index 521906f..f0ec73c 100644
--- a/wwwroot/css/theme.css
+++ b/wwwroot/css/theme.css
@@ -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;
diff --git a/wwwroot/index.html b/wwwroot/index.html
index 87bcd5e..747cb67 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -186,6 +186,7 @@
+
diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js
index 13bbed2..d28d1e7 100644
--- a/wwwroot/js/api.js
+++ b/wwwroot/js/api.js
@@ -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
});
}
diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js
index 3cf74f1..12ec9db 100644
--- a/wwwroot/js/chat.js
+++ b/wwwroot/js/chat.js
@@ -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(' Streaming...');
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(' Streaming... (Verbindung wiederhergestellt)');
- 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(' Streaming... (Verbindung wiederhergestellt)');
+ 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();
diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js
index beff8a1..57128bb 100644
--- a/wwwroot/js/pages/chat-page.js
+++ b/wwwroot/js/pages/chat-page.js
@@ -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(' Streaming...');
startStreamingPoll(sessionId, sendBtn, status.chainId);
}