diff --git a/TODO.md b/TODO.md index d75015d..8529c68 100644 --- a/TODO.md +++ b/TODO.md @@ -46,7 +46,7 @@ - [x] ~~Claude (Sub-)Agents~~ – Nicht umsetzbar: Claude Code Sub-Agents sind ein CLI-Feature, nicht über die Messages API verfügbar. Eigenes Sub-Agent-System (Preparer/Review/Finalizer) deckt den Use-Case bereits ab. ## Features aus Devy übernehmen -- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "denkt nach..." +- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "Streaming..." - [ ] 🎨 Chat-Hintergründe - Benutzerdefinierte Hintergründe pro Session - [x] 👤 User-Management - Admin-only: Benutzer CRUD, Login-as-User, Default-Pfade - [x] 🖥️ Windows Desktop App - WPF + WebView2 Wrapper mit konfigurierbarer URL diff --git a/wwwroot/index.html b/wwwroot/index.html index 93861c4..5af79c2 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -616,6 +616,7 @@ + diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 3878945..706279d 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -161,6 +161,38 @@ let _currentToolBox = null; // Aktuell laufende Tool-Box (für Stream-Events) let _chatAbandoned = false; // true wenn User weg-navigiert während Stream läuft let _abortController = null; // AbortController für laufenden Stream +// ── Typing Indicator ── + +let _currentAgentName = null; +let _typingInterval = null; + +function setCurrentAgentName(name) { + _currentAgentName = name; +} + +function _randomTypingPhrase() { + return TYPING_PHRASES[Math.floor(Math.random() * TYPING_PHRASES.length)]; +} + +function _buildTypingHtml(phrase) { + var name = _currentAgentName || 'Agent'; + return ' ' + esc(name) + ' ' + esc(phrase) + '...'; +} + +function startTypingIndicator() { + setMetaHtml(_buildTypingHtml(_randomTypingPhrase())); + _typingInterval = setInterval(function() { + setMetaHtml(_buildTypingHtml(_randomTypingPhrase())); + }, 3000); +} + +function stopTypingIndicator() { + if (_typingInterval) { + clearInterval(_typingInterval); + _typingInterval = null; + } +} + async function clearChat() { const archive = await apiArchive(getCurrentSessionId()); if (!archive) return; @@ -213,7 +245,7 @@ async function _runStream(fetchPromise) { _currentToolBox = null; _chatAbandoned = false; _stopClickCount = 0; - setMetaHtml(' Streaming...'); + startTypingIndicator(); try { const resp = await fetchPromise(_abortController.signal); @@ -250,7 +282,7 @@ async function _runStream(fetchPromise) { try { const status = await apiIsStreaming(sid); if (status.streaming) { - setMetaHtml(' Streaming... (Verbindung wiederhergestellt)'); + startTypingIndicator(); startStreamingPoll(sid, sendBtn, status.chainId); return; } @@ -259,6 +291,7 @@ async function _runStream(fetchPromise) { } } finally { if (_chatAbandoned) return; + stopTypingIndicator(); if (!isStreaming || !_streamingPollInterval) { sendBtn.disabled = false; sendBtn.hidden = false; @@ -269,7 +302,7 @@ async function _runStream(fetchPromise) { _abortController = null; // Meta zurücksetzen falls kein done-Event kam (z.B. nach Stop) var metaEl = $('#meta'); - if (metaEl && metaEl.textContent.includes('Streaming')) { + if (metaEl && (metaEl.textContent.includes('Streaming') || metaEl.textContent.includes('...'))) { setMeta(_stopClickCount > 0 ? 'Gestoppt' : 'Bereit'); } } diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index c2316a4..4d8d519 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -202,8 +202,11 @@ registerPage('chat', { if (session.agentId) { try { const agent = await apiGetAgent(session.agentId); - if (agent && agent.customCommands) { - agentCmds = JSON.parse(agent.customCommands) || []; + if (agent) { + setCurrentAgentName(agent.name || null); + if (agent.customCommands) { + agentCmds = JSON.parse(agent.customCommands) || []; + } } } catch(e) {} } @@ -390,6 +393,8 @@ registerPage('chat', { // Stream nicht abbrechen (Backend soll weiterlaufen), // aber Frontend-Processing stoppen _chatAbandoned = true; + stopTypingIndicator(); + setCurrentAgentName(null); closeEmojiPicker(); if (this._closeMenu) { document.removeEventListener('click', this._closeMenu); diff --git a/wwwroot/js/typing-phrases.js b/wwwroot/js/typing-phrases.js new file mode 100644 index 0000000..d44502d --- /dev/null +++ b/wwwroot/js/typing-phrases.js @@ -0,0 +1,29 @@ +const TYPING_PHRASES = [ + 'denkt si wos', + 'grübelt', + 'schaut deppat drein', + 'brunzt', + 'scheißt an Kropfn', + 'krotzt si am Hirn', + 'hoit sei Goschn', + 'is am Schmäh fian', + 'raunzt leise vor si hi', + 'fladert a Idee', + 'schleicht si boid', + 'red mit si söba', + 'brettlt durch die Daten', + 'sudert', + 'bockts nimma', + 'schaut obs eh passt', + 'mocht a Hetz', + 'wuzelt si durch', + 'hod an Bickn', + 'zwickt si', + 'draht Dahma', + 'schnackselt mit der API', + 'is gonz narrisch', + 'hackelt', + 'sudert übers Wetter', + 'fetzt si mitn Code', + 'is am Wurschteln', +];