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',
+];