feat(chat): Gedanken-Stream (App + Diagnostic)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -126,11 +126,24 @@ interface ChatMessage {
|
|||||||
sendAttempts?: number;
|
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 ---
|
// --- Konstanten ---
|
||||||
|
|
||||||
const CHAT_STORAGE_KEY = 'aria_chat_messages';
|
const CHAT_STORAGE_KEY = 'aria_chat_messages';
|
||||||
|
const THOUGHT_STORAGE_KEY = 'aria_thought_stream';
|
||||||
const MAX_STORED_MESSAGES = 500;
|
const MAX_STORED_MESSAGES = 500;
|
||||||
const MAX_MEMORY_MESSAGES = 500;
|
const MAX_MEMORY_MESSAGES = 500;
|
||||||
|
const MAX_THOUGHTS = 500;
|
||||||
|
|
||||||
// Hilfe: Messages-Array auf Max kappen (aelteste raus) — verhindert OOM
|
// Hilfe: Messages-Array auf Max kappen (aelteste raus) — verhindert OOM
|
||||||
// im Gespraechsmodus bei sehr vielen Nachrichten.
|
// im Gespraechsmodus bei sehr vielen Nachrichten.
|
||||||
@@ -252,6 +265,14 @@ const ChatScreen: React.FC = () => {
|
|||||||
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
||||||
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
|
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<ThoughtEntry[]>([]);
|
||||||
|
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<string>('');
|
||||||
// Service-Status (Gamebox: F5-TTS / Whisper Lade-Status) + Banner-Sichtbarkeit
|
// Service-Status (Gamebox: F5-TTS / Whisper Lade-Status) + Banner-Sichtbarkeit
|
||||||
const [serviceStatus, setServiceStatus] = useState<Record<string, {state: string, model?: string, loadSeconds?: number, error?: string}>>({});
|
const [serviceStatus, setServiceStatus] = useState<Record<string, {state: string, model?: string, loadSeconds?: number, error?: string}>>({});
|
||||||
const [serviceBannerDismissed, setServiceBannerDismissed] = useState(false);
|
const [serviceBannerDismissed, setServiceBannerDismissed] = useState(false);
|
||||||
@@ -1002,6 +1023,16 @@ const ChatScreen: React.FC = () => {
|
|||||||
const activity = (message.payload.activity as string) || 'idle';
|
const activity = (message.payload.activity as string) || 'idle';
|
||||||
const tool = (message.payload.tool as string) || '';
|
const tool = (message.payload.tool as string) || '';
|
||||||
setAgentActivity({ activity, tool });
|
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
|
// Spotify darf waehrend "ARIA denkt/schreibt" weiterspielen — pausiert
|
||||||
// nur wenn TTS startet (dann acquired _firePlaybackStarted den Focus).
|
// nur wenn TTS startet (dann acquired _firePlaybackStarted den Focus).
|
||||||
// Watchdog: solange Brain noch Lebenszeichen sendet (jedes neue
|
// Watchdog: solange Brain noch Lebenszeichen sendet (jedes neue
|
||||||
@@ -1266,6 +1297,36 @@ const ChatScreen: React.FC = () => {
|
|||||||
return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
|
return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
|
||||||
}, [messages]);
|
}, [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<ReturnType<typeof setTimeout> | 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
|
// messagesRef immer aktuell halten — wird von dispatchWithAck/Retry gelesen
|
||||||
// damit Retries auf den aktuellen deliveryStatus reagieren koennen.
|
// damit Retries auf den aktuellen deliveryStatus reagieren koennen.
|
||||||
useEffect(() => { messagesRef.current = messages; }, [messages]);
|
useEffect(() => { messagesRef.current = messages; }, [messages]);
|
||||||
@@ -1953,7 +2014,13 @@ const ChatScreen: React.FC = () => {
|
|||||||
{connectionState === 'connected' ? 'Verbunden' :
|
{connectionState === 'connected' ? 'Verbunden' :
|
||||||
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'}
|
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={() => setInboxVisible(true)} style={{marginLeft: 'auto', paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
<TouchableOpacity onPress={() => setThoughtsVisible(true)} style={{marginLeft: 'auto', paddingHorizontal: 6, flexDirection: 'row', alignItems: 'center'}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
||||||
|
<Text style={{fontSize: 16}}>{'\uD83D\uDCAD'}</Text>
|
||||||
|
{thoughts.length > 0 ? (
|
||||||
|
<Text style={{color: '#8888AA', fontSize: 11, marginLeft: 3}}>{thoughts.length}</Text>
|
||||||
|
) : null}
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => setInboxVisible(true)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
||||||
<Text style={{fontSize: 18}}>{'\uD83D\uDDC2\uFE0F'}</Text>
|
<Text style={{fontSize: 18}}>{'\uD83D\uDDC2\uFE0F'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => setSearchVisible(!searchVisible)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
<TouchableOpacity onPress={() => setSearchVisible(!searchVisible)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
||||||
@@ -2228,6 +2295,102 @@ const ChatScreen: React.FC = () => {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* Gedanken-Stream — chronologisches Log von ARIAs interner Aktivitaet.
|
||||||
|
Bottom-Sheet (slide-up), 60% Bildschirmhoehe. Mülltonne zum Leeren. */}
|
||||||
|
<Modal
|
||||||
|
visible={thoughtsVisible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setThoughtsVisible(false)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setThoughtsVisible(false)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity activeOpacity={1} style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}}>
|
||||||
|
{/* Drag-Indicator */}
|
||||||
|
<View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}>
|
||||||
|
<View style={{width:40, height:4, borderRadius:2, backgroundColor:'#2A2A3E'}} />
|
||||||
|
</View>
|
||||||
|
<View style={{flexDirection:'row', alignItems:'center', padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
|
||||||
|
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:16, flex:1}}>
|
||||||
|
{'💭'} Gedanken-Stream {thoughts.length > 0 ? `(${thoughts.length})` : ''}
|
||||||
|
</Text>
|
||||||
|
{thoughts.length > 0 ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
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}}
|
||||||
|
>
|
||||||
|
<Text style={{fontSize:18}}>{'🗑'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
<TouchableOpacity onPress={() => setThoughtsVisible(false)} hitSlop={{top:8,bottom:8,left:8,right:8}}>
|
||||||
|
<Text style={{color:'#8888AA', fontSize:24}}>×</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
{thoughts.length === 0 ? (
|
||||||
|
<View style={{flex:1, alignItems:'center', justifyContent:'center', padding:24}}>
|
||||||
|
<Text style={{color:'#555570', fontSize:13, fontStyle:'italic', textAlign:'center'}}>
|
||||||
|
Noch keine Gedanken aufgezeichnet.{'\n'}Sobald ARIA was tut, taucht's hier auf.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={thoughts}
|
||||||
|
keyExtractor={(_, i) => `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 (
|
||||||
|
<View>
|
||||||
|
{showGap ? (
|
||||||
|
<View style={{flexDirection:'row', alignItems:'center', paddingHorizontal:16, paddingVertical:6}}>
|
||||||
|
<View style={{flex:1, height:1, backgroundColor:'#1E1E2E'}} />
|
||||||
|
<Text style={{color:'#555570', fontSize:10, paddingHorizontal:8}}>
|
||||||
|
{gapMin < 60 ? `${gapMin} Min` : `${Math.floor(gapMin/60)}h ${gapMin%60}m`}
|
||||||
|
</Text>
|
||||||
|
<View style={{flex:1, height:1, backgroundColor:'#1E1E2E'}} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<View style={{flexDirection:'row', paddingHorizontal:16, paddingVertical:5}}>
|
||||||
|
<Text style={{color:'#555570', fontSize:11, width:78}}>{time}</Text>
|
||||||
|
<Text style={{fontSize:13, width:24}}>{icon}</Text>
|
||||||
|
<Text style={{color: isIdle ? '#34C759' : '#E0E0F0', fontSize:13, flex:1}}>{label}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
||||||
Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-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. */}
|
des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */}
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
<input type="checkbox" id="gps-debug-toggle" onchange="toggleGpsDebug()" style="margin-right:4px;vertical-align:middle;">
|
<input type="checkbox" id="gps-debug-toggle" onchange="toggleGpsDebug()" style="margin-right:4px;vertical-align:middle;">
|
||||||
GPS-Position einblenden
|
GPS-Position einblenden
|
||||||
</label>
|
</label>
|
||||||
|
<button class="btn secondary" onclick="openThoughtStream()" id="btn-thoughts" title="Gedanken-Stream — was ARIA intern tut" style="padding:4px 10px;font-size:11px;">💭 Gedanken <span id="thoughts-count" style="color:#8888AA;"></span></button>
|
||||||
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
|
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,6 +343,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Gedanken-Stream Modal — chronologisches Log was ARIA intern tut.
|
||||||
|
Zentrales Modal (max 720px breit), Liste mit Auto-Scroll ans Ende
|
||||||
|
wenn neue Eintraege reinkommen. -->
|
||||||
|
<div id="thought-stream-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.7);z-index:1100;align-items:center;justify-content:center;padding:24px;" onclick="if(event.target===this) closeThoughtStream();">
|
||||||
|
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:12px;width:100%;max-width:720px;height:70vh;display:flex;flex-direction:column;">
|
||||||
|
<div style="display:flex;align-items:center;padding:14px;border-bottom:1px solid #1E1E2E;">
|
||||||
|
<h2 style="margin:0;color:#FFD60A;flex:1;font-size:16px;">💭 Gedanken-Stream <span id="thoughts-count-modal" style="color:#8888AA;font-weight:normal;"></span></h2>
|
||||||
|
<button class="btn secondary" onclick="clearThoughtStream()" id="btn-clear-thoughts" title="Stream leeren" style="padding:4px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;margin-right:6px;">🗑 Leeren</button>
|
||||||
|
<button class="btn secondary" onclick="closeThoughtStream()" style="padding:4px 12px;">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
<div id="thought-stream-list" style="flex:1;overflow-y:auto;padding:8px 0;font-size:13px;font-family:monospace;">
|
||||||
|
<!-- gefuellt durch renderThoughtStream() -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
|
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
|
||||||
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
|
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
|
||||||
|
|
||||||
@@ -2166,6 +2183,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateThinkingIndicator(msg) {
|
function updateThinkingIndicator(msg) {
|
||||||
|
// Gedanken-Stream fuettern — JEDES Event (auch idle als ✓ fertig)
|
||||||
|
pushThought(msg.activity || '', msg.tool || '');
|
||||||
|
|
||||||
const indicators = [
|
const indicators = [
|
||||||
document.getElementById('thinking-indicator'),
|
document.getElementById('thinking-indicator'),
|
||||||
document.getElementById('thinking-indicator-fs'),
|
document.getElementById('thinking-indicator-fs'),
|
||||||
@@ -2202,6 +2222,112 @@
|
|||||||
}, 120000);
|
}, 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 = '<div style="padding:24px;text-align:center;color:#555570;font-style:italic;">Noch keine Gedanken aufgezeichnet.<br>Sobald ARIA was tut, taucht\'s hier auf.</div>';
|
||||||
|
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(`<div style="display:flex;align-items:center;padding:6px 16px;gap:8px;"><div style="flex:1;height:1px;background:#1E1E2E;"></div><span style="color:#555570;font-size:10px;">${label}</span><div style="flex:1;height:1px;background:#1E1E2E;"></div></div>`);
|
||||||
|
}
|
||||||
|
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(`<div style="display:flex;padding:4px 16px;align-items:baseline;"><span style="color:#555570;width:78px;font-size:11px;">${time}</span><span style="width:24px;">${icon}</span><span style="color:${color};flex:1;">${_escapeHtml(label)}</span></div>`);
|
||||||
|
}
|
||||||
|
list.innerHTML = rows.join('');
|
||||||
|
if (autoscroll) list.scrollTop = list.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// ── XTTS Panel ─────────────────────────────
|
// ── XTTS Panel ─────────────────────────────
|
||||||
function renderVoiceList(voices) {
|
function renderVoiceList(voices) {
|
||||||
const box = document.getElementById('xtts-voice-list');
|
const box = document.getElementById('xtts-voice-list');
|
||||||
@@ -4696,6 +4822,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadThoughtStream();
|
||||||
connectWS();
|
connectWS();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user