From 856701fb6fdec108009f47d97f67f513a1533f9b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 15 May 2026 08:31:55 +0200 Subject: [PATCH] feat(chat): Gedanken-Stream (App + Diagnostic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persistentes chronologisches Log was ARIA intern macht β€” gefuettert aus agent_activity-Events (thinking/tool/assistant/idle). Bleibt zwischen Denk-Phasen stehen, neue Eintraege kommen unten dran, lange Pausen werden mit Trennlinie + Minuten-Hint sichtbar gemacht. App (ChatScreen.tsx): - πŸ’­-Icon in der Statusleiste neben πŸ—‚οΈ und πŸ”, zeigt Eintrags-Anzahl - Bottom-Sheet (60% Hoehe) mit chronologischer Liste, Tap auf Hintergrund schliesst, πŸ—‘-Confirm zum Leeren - Persistierung in AsyncStorage (aria_thought_stream, capped 500) - Dedup gegen direkt aufeinanderfolgende identische Events Diagnostic (index.html): - πŸ’­ Gedanken-Button im Chat-Test-Header neben β€žVollbild" - Zentrales Modal (720px x 70vh), Live-Update wenn neue Eintraege kommen (autoscroll ans Ende), πŸ—‘ Leeren-Button mit Confirm - Persistierung in localStorage, gleiche cap/dedup-Logik wie App Co-Authored-By: Claude Opus 4.7 (1M context) --- android/src/screens/ChatScreen.tsx | 165 ++++++++++++++++++++++++++++- diagnostic/index.html | 127 ++++++++++++++++++++++ 2 files changed, 291 insertions(+), 1 deletion(-) diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 5c0a444..b227484 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -126,11 +126,24 @@ interface ChatMessage { sendAttempts?: number; } +/** Ein Eintrag im Gedanken-Stream β€” chronologisches Log dessen was ARIA + * intern macht (Brain-`agent_activity`-Events). Bleibt zwischen Denk- + * Phasen stehen, wird in AsyncStorage persistiert. */ +interface ThoughtEntry { + ts: number; + /** Roh-Activity vom Brain: thinking, tool, assistant, idle (= βœ“ fertig). */ + activity: string; + /** Bei activity='tool' der Tool-Name, sonst leer. */ + tool?: string; +} + // --- Konstanten --- const CHAT_STORAGE_KEY = 'aria_chat_messages'; +const THOUGHT_STORAGE_KEY = 'aria_thought_stream'; const MAX_STORED_MESSAGES = 500; const MAX_MEMORY_MESSAGES = 500; +const MAX_THOUGHTS = 500; // Hilfe: Messages-Array auf Max kappen (aelteste raus) β€” verhindert OOM // im Gespraechsmodus bei sehr vielen Nachrichten. @@ -252,6 +265,14 @@ const ChatScreen: React.FC = () => { const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]); const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''}); + // Gedanken-Stream: chronologisches Log dessen was ARIA intern macht. + // Wird aus agent_activity-Events gefuettert und in AsyncStorage persistiert. + const [thoughts, setThoughts] = useState([]); + const [thoughtsVisible, setThoughtsVisible] = useState(false); + // Spiegel der letzten Activity in einer Ref β€” verhindert dass aufeinander- + // folgende identische Events (z.B. zwei 'thinking' hintereinander) den + // Stream zumuellen. Eigentlich seltener Fall, aber billig zu pruefen. + const lastThoughtKeyRef = useRef(''); // Service-Status (Gamebox: F5-TTS / Whisper Lade-Status) + Banner-Sichtbarkeit const [serviceStatus, setServiceStatus] = useState>({}); const [serviceBannerDismissed, setServiceBannerDismissed] = useState(false); @@ -1002,6 +1023,16 @@ const ChatScreen: React.FC = () => { const activity = (message.payload.activity as string) || 'idle'; const tool = (message.payload.tool as string) || ''; setAgentActivity({ activity, tool }); + // In den Gedanken-Stream einfuegen. Dedup gegen identische Folge- + // Events (z.B. zwei mal 'thinking' direkt hintereinander). + const key = `${activity}|${tool}`; + if (key !== lastThoughtKeyRef.current) { + lastThoughtKeyRef.current = key; + setThoughts(prev => { + const next = [...prev, { ts: Date.now(), activity, tool }]; + return next.length > MAX_THOUGHTS ? next.slice(-MAX_THOUGHTS) : next; + }); + } // Spotify darf waehrend "ARIA denkt/schreibt" weiterspielen β€” pausiert // nur wenn TTS startet (dann acquired _firePlaybackStarted den Focus). // Watchdog: solange Brain noch Lebenszeichen sendet (jedes neue @@ -1266,6 +1297,36 @@ const ChatScreen: React.FC = () => { return () => { if (saveTimer.current) clearTimeout(saveTimer.current); }; }, [messages]); + // Gedanken-Stream beim Mount aus AsyncStorage laden + useEffect(() => { + AsyncStorage.getItem(THOUGHT_STORAGE_KEY) + .then(raw => { + if (!raw) return; + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) setThoughts(parsed.slice(-MAX_THOUGHTS)); + } catch {} + }) + .catch(() => {}); + }, []); + + // Gedanken-Stream persistieren (debounced) + const thoughtSaveTimer = useRef | null>(null); + useEffect(() => { + if (thoughts.length === 0) { + AsyncStorage.removeItem(THOUGHT_STORAGE_KEY).catch(() => {}); + return; + } + if (thoughtSaveTimer.current) clearTimeout(thoughtSaveTimer.current); + thoughtSaveTimer.current = setTimeout(() => { + AsyncStorage.setItem( + THOUGHT_STORAGE_KEY, + JSON.stringify(thoughts.slice(-MAX_THOUGHTS)), + ).catch(() => {}); + }, 500); + return () => { if (thoughtSaveTimer.current) clearTimeout(thoughtSaveTimer.current); }; + }, [thoughts]); + // messagesRef immer aktuell halten β€” wird von dispatchWithAck/Retry gelesen // damit Retries auf den aktuellen deliveryStatus reagieren koennen. useEffect(() => { messagesRef.current = messages; }, [messages]); @@ -1953,7 +2014,13 @@ const ChatScreen: React.FC = () => { {connectionState === 'connected' ? 'Verbunden' : connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'} - setInboxVisible(true)} style={{marginLeft: 'auto', paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}> + setThoughtsVisible(true)} style={{marginLeft: 'auto', paddingHorizontal: 6, flexDirection: 'row', alignItems: 'center'}} hitSlop={{top:8,bottom:8,left:6,right:6}}> + {'\uD83D\uDCAD'} + {thoughts.length > 0 ? ( + {thoughts.length} + ) : null} + + setInboxVisible(true)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}> {'\uD83D\uDDC2\uFE0F'} setSearchVisible(!searchVisible)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}> @@ -2228,6 +2295,102 @@ const ChatScreen: React.FC = () => { ) : null} + {/* Gedanken-Stream β€” chronologisches Log von ARIAs interner Aktivitaet. + Bottom-Sheet (slide-up), 60% Bildschirmhoehe. MΓΌlltonne zum Leeren. */} + setThoughtsVisible(false)} + > + setThoughtsVisible(false)} + > + + {/* Drag-Indicator */} + + + + + + {'πŸ’­'} Gedanken-Stream {thoughts.length > 0 ? `(${thoughts.length})` : ''} + + {thoughts.length > 0 ? ( + { + Alert.alert('Gedanken-Stream leeren?', `Alle ${thoughts.length} Eintraege werden geloescht.`, [ + { text: 'Abbrechen', style: 'cancel' }, + { text: 'Leeren', style: 'destructive', onPress: () => { + setThoughts([]); + lastThoughtKeyRef.current = ''; + } }, + ]); + }} + hitSlop={{top:8,bottom:8,left:8,right:8}} + style={{paddingHorizontal:8}} + > + {'πŸ—‘'} + + ) : null} + setThoughtsVisible(false)} hitSlop={{top:8,bottom:8,left:8,right:8}}> + Γ— + + + {thoughts.length === 0 ? ( + + + Noch keine Gedanken aufgezeichnet.{'\n'}Sobald ARIA was tut, taucht's hier auf. + + + ) : ( + `t_${i}`} + contentContainerStyle={{paddingVertical:8}} + renderItem={({ item, index }) => { + const prev = index > 0 ? thoughts[index - 1] : null; + // Lange Pause? β†’ Trenn-Linie mit Minuten-Hint + const gapMin = prev ? Math.floor((item.ts - prev.ts) / 60000) : 0; + const showGap = gapMin >= 1; + const time = new Date(item.ts).toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit', second:'2-digit'}); + const icon = + item.activity === 'idle' ? 'βœ“' : + item.activity === 'tool' ? 'πŸ”§' : + item.activity === 'assistant' ? '✍️' : + item.activity === 'thinking' ? 'πŸ’­' : 'β€’'; + const label = + item.activity === 'idle' ? 'fertig' : + item.activity === 'tool' ? (item.tool || 'tool') : + item.activity === 'assistant' ? 'schreibt' : + item.activity === 'thinking' ? 'denkt' : item.activity; + const isIdle = item.activity === 'idle'; + return ( + + {showGap ? ( + + + + {gapMin < 60 ? `${gapMin} Min` : `${Math.floor(gapMin/60)}h ${gapMin%60}m`} + + + + ) : null} + + {time} + {icon} + {label} + + + ); + }} + /> + )} + + + + {/* Notizen-Inbox β€” Listet alle Memories aus dem aktuellen Chat (Special-Bubbles). Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-Bubbles des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */} diff --git a/diagnostic/index.html b/diagnostic/index.html index 6fa6bfd..a24d9e1 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -301,6 +301,7 @@ GPS-Position einblenden + @@ -342,6 +343,22 @@ + + + @@ -2166,6 +2183,9 @@ } function updateThinkingIndicator(msg) { + // Gedanken-Stream fuettern β€” JEDES Event (auch idle als βœ“ fertig) + pushThought(msg.activity || '', msg.tool || ''); + const indicators = [ document.getElementById('thinking-indicator'), document.getElementById('thinking-indicator-fs'), @@ -2202,6 +2222,112 @@ }, 120000); } + // ── Gedanken-Stream ───────────────────────────── + // Chronologisches Log von agent_activity-Events. Wird in localStorage + // persistiert (ueberlebt Page-Reload), capped auf MAX_THOUGHTS. + const THOUGHT_STORAGE_KEY = 'aria_thought_stream'; + const MAX_THOUGHTS = 500; + let thoughtStream = []; + let lastThoughtKey = ''; + let _thoughtSaveTimer = null; + + function loadThoughtStream() { + try { + const raw = localStorage.getItem(THOUGHT_STORAGE_KEY); + if (!raw) return; + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) thoughtStream = parsed.slice(-MAX_THOUGHTS); + } catch {} + updateThoughtsBadge(); + } + + function persistThoughtStream() { + if (_thoughtSaveTimer) clearTimeout(_thoughtSaveTimer); + _thoughtSaveTimer = setTimeout(() => { + try { + if (thoughtStream.length === 0) localStorage.removeItem(THOUGHT_STORAGE_KEY); + else localStorage.setItem(THOUGHT_STORAGE_KEY, JSON.stringify(thoughtStream.slice(-MAX_THOUGHTS))); + } catch {} + }, 500); + } + + function pushThought(activity, tool) { + // Dedup gegen direkt aufeinanderfolgende identische Events + const key = `${activity}|${tool || ''}`; + if (key === lastThoughtKey) return; + lastThoughtKey = key; + thoughtStream.push({ ts: Date.now(), activity, tool: tool || '' }); + if (thoughtStream.length > MAX_THOUGHTS) thoughtStream = thoughtStream.slice(-MAX_THOUGHTS); + updateThoughtsBadge(); + // Wenn das Modal offen ist: live nachrendern + ans Ende scrollen + const modal = document.getElementById('thought-stream-modal'); + if (modal && modal.style.display !== 'none') renderThoughtStream(true); + persistThoughtStream(); + } + + function updateThoughtsBadge() { + const a = document.getElementById('thoughts-count'); + if (a) a.textContent = thoughtStream.length ? `(${thoughtStream.length})` : ''; + const b = document.getElementById('thoughts-count-modal'); + if (b) b.textContent = thoughtStream.length ? `(${thoughtStream.length})` : ''; + } + + function openThoughtStream() { + const modal = document.getElementById('thought-stream-modal'); + if (!modal) return; + modal.style.display = 'flex'; + renderThoughtStream(true); + } + + function closeThoughtStream() { + const modal = document.getElementById('thought-stream-modal'); + if (modal) modal.style.display = 'none'; + } + + function clearThoughtStream() { + if (thoughtStream.length === 0) return; + if (!confirm(`Gedanken-Stream leeren? ${thoughtStream.length} Eintraege werden geloescht.`)) return; + thoughtStream = []; + lastThoughtKey = ''; + updateThoughtsBadge(); + renderThoughtStream(false); + persistThoughtStream(); + } + + function _escapeHtml(s) { + return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + } + + function renderThoughtStream(autoscroll) { + const list = document.getElementById('thought-stream-list'); + if (!list) return; + if (thoughtStream.length === 0) { + list.innerHTML = '
Noch keine Gedanken aufgezeichnet.
Sobald ARIA was tut, taucht\'s hier auf.
'; + return; + } + const rows = []; + let prevTs = 0; + for (const t of thoughtStream) { + const gapMin = prevTs ? Math.floor((t.ts - prevTs) / 60000) : 0; + if (gapMin >= 1) { + const label = gapMin < 60 ? `${gapMin} Min` : `${Math.floor(gapMin/60)}h ${gapMin%60}m`; + rows.push(`
${label}
`); + } + prevTs = t.ts; + const d = new Date(t.ts); + const time = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; + let icon, label, color; + if (t.activity === 'idle') { icon = 'βœ“'; label = 'fertig'; color = '#34C759'; } + else if (t.activity === 'tool') { icon = 'πŸ”§'; label = t.tool || 'tool'; color = '#E0E0F0'; } + else if (t.activity === 'assistant'){ icon = '✍️'; label = 'schreibt'; color = '#E0E0F0'; } + else if (t.activity === 'thinking'){ icon = 'πŸ’­'; label = 'denkt'; color = '#E0E0F0'; } + else { icon = 'β€’'; label = t.activity; color = '#E0E0F0'; } + rows.push(`
${time}${icon}${_escapeHtml(label)}
`); + } + list.innerHTML = rows.join(''); + if (autoscroll) list.scrollTop = list.scrollHeight; + } + // ── XTTS Panel ───────────────────────────── function renderVoiceList(voices) { const box = document.getElementById('xtts-voice-list'); @@ -4696,6 +4822,7 @@ }); } + loadThoughtStream(); connectWS();