feat(app): optionaler Quell-Badge an ARIA-Bubbles (Einstellung, pro Geraet, default aus)

Zeigt wie in Diagnostic, ob eine Antwort vom lokalen Modell / Claude / Fast-Path
kam — aber in der App als Opt-in, damit die App "Mama-tauglich" bleibt.

- ChatScreen: ChatMessage.answeredBy; aus dem chat-Payload UND der History-Sync
  eingefangen; kleiner farbiger Badge im statusRow der ARIA-Bubble, nur wenn
  showSource an. State aus AsyncStorage aria_show_source (default false, per
  2s-Reload synchron mit den Settings).
- SettingsScreen: Toggle "Antwort-Quelle anzeigen" in der Chat-Bubbles-Card,
  schreibt aria_show_source (pro Geraet — Stefan an, Mama aus).
- Bridge liefert answeredBy schon in chat_backup/History mit (kein Change noetig).

Server-Teil in 9cc1aec. APK-Rebuild noetig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 13:46:12 +02:00
co-authored by Claude Opus 4.8
parent 9cc1aec5ec
commit 7ae61701ad
2 changed files with 44 additions and 0 deletions
+19
View File
@@ -73,6 +73,9 @@ interface ChatMessage {
/** Projekt-Zuordnung — leer = Hauptchat. Wird genutzt um Bubbles zu /** Projekt-Zuordnung — leer = Hauptchat. Wird genutzt um Bubbles zu
* Projekt-Bloecken zu gruppieren (auf/einklappbar). */ * Projekt-Bloecken zu gruppieren (auf/einklappbar). */
projectId?: string; projectId?: string;
/** Welcher Backend die Antwort erzeugt hat: 'local' | 'claude' | 'fast-path'.
* Fuer den optionalen Quell-Badge (Einstellung aria_show_source, default aus). */
answeredBy?: string;
/** Bridge-Message-ID zur Zuordnung von TTS-Audio */ /** Bridge-Message-ID zur Zuordnung von TTS-Audio */
messageId?: string; messageId?: string;
/** Lokaler Pfad zur gecachten TTS-Audio-Datei (file://...) */ /** Lokaler Pfad zur gecachten TTS-Audio-Datei (file://...) */
@@ -324,6 +327,8 @@ const ChatScreen: React.FC = () => {
// App soll sie standardmaessig NICHT anzeigen — Stefan sieht sonst // App soll sie standardmaessig NICHT anzeigen — Stefan sieht sonst
// jeden Hint mit. Toggle in Settings. // jeden Hint mit. Toggle in Settings.
const [showSystemHints, setShowSystemHints] = useState(false); const [showSystemHints, setShowSystemHints] = useState(false);
// Quell-Badge (local/claude/fast-path) an ARIA-Bubbles — pro Geraet, default AUS.
const [showSource, setShowSource] = useState(false);
// Gerätelokale XTTS-Voice-Wahl (bevorzugt gegenueber dem globalen Default) // Gerätelokale XTTS-Voice-Wahl (bevorzugt gegenueber dem globalen Default)
const localXttsVoiceRef = useRef<string>(''); const localXttsVoiceRef = useRef<string>('');
// Geraetelokale TTS-Wiedergabegeschwindigkeit (speed-Param an F5-TTS) // Geraetelokale TTS-Wiedergabegeschwindigkeit (speed-Param an F5-TTS)
@@ -546,6 +551,8 @@ const ChatScreen: React.FC = () => {
setGpsEnabled(gps === 'true'); setGpsEnabled(gps === 'true');
const hints = await AsyncStorage.getItem('aria_show_hints'); const hints = await AsyncStorage.getItem('aria_show_hints');
setShowSystemHints(hints === 'true'); // default false setShowSystemHints(hints === 'true'); // default false
const src = await AsyncStorage.getItem('aria_show_source');
setShowSource(src === 'true'); // default false (Mama sieht keine Badges)
}; };
loadSettings(); loadSettings();
const interval = setInterval(loadSettings, 2000); const interval = setInterval(loadSettings, 2000);
@@ -815,6 +822,7 @@ const ChatScreen: React.FC = () => {
attachments: attachments.length ? attachments : undefined, attachments: attachments.length ? attachments : undefined,
backupTs: typeof m.ts === 'number' ? m.ts : undefined, backupTs: typeof m.ts === 'number' ? m.ts : undefined,
projectId: typeof m.project_id === 'string' ? m.project_id : '', projectId: typeof m.project_id === 'string' ? m.project_id : '',
answeredBy: typeof m.answeredBy === 'string' ? m.answeredBy : '',
...(cmid && { clientMsgId: cmid }), ...(cmid && { clientMsgId: cmid }),
// Server-Bubble = vom Brain verarbeitet → 'delivered' (✓✓) // Server-Bubble = vom Brain verarbeitet → 'delivered' (✓✓)
...(role === 'user' && cmid && { deliveryStatus: 'delivered' as const }), ...(role === 'user' && cmid && { deliveryStatus: 'delivered' as const }),
@@ -1151,6 +1159,7 @@ const ChatScreen: React.FC = () => {
messageId: (message.payload.messageId as string) || undefined, messageId: (message.payload.messageId as string) || undefined,
backupTs: (message.payload.backupTs as number) || undefined, backupTs: (message.payload.backupTs as number) || undefined,
projectId: ((message.payload as any).projectId as string) || '', projectId: ((message.payload as any).projectId as string) || '',
answeredBy: ((message.payload as any).answeredBy as string) || '',
}; };
// ARIA hat geantwortet → alle User-Bubbles davor als 'delivered' // ARIA hat geantwortet → alle User-Bubbles davor als 'delivered'
// markieren (WhatsApp-Doppelhaken ✓✓). Brain hat sie verarbeitet. // markieren (WhatsApp-Doppelhaken ✓✓). Brain hat sie verarbeitet.
@@ -2411,6 +2420,16 @@ const ChatScreen: React.FC = () => {
) : null} ) : null}
<View style={styles.statusRow}> <View style={styles.statusRow}>
<Text style={styles.timestamp}>{time}</Text> <Text style={styles.timestamp}>{time}</Text>
{!isUser && showSource && item.answeredBy ? (() => {
const ab = item.answeredBy as string;
const map: { [k: string]: { t: string; c: string } } = {
local: { t: '⚡ lokal', c: '#34C759' },
claude: { t: 'Claude', c: '#0096FF' },
'fast-path': { t: '⚡ fast', c: '#AF7BFF' },
};
const b = map[ab] || { t: ab, c: '#8A8AA0' };
return <Text style={{ fontSize: 9, fontWeight: 'bold', color: b.c, marginLeft: 6 }}>{b.t}</Text>;
})() : null}
{item.text.length > 0 ? ( {item.text.length > 0 ? (
<TouchableOpacity <TouchableOpacity
hitSlop={{top:6,bottom:6,left:6,right:6}} hitSlop={{top:6,bottom:6,left:6,right:6}}
+25
View File
@@ -175,6 +175,7 @@ const SettingsScreen: React.FC = () => {
const [bgGpsEnabled, setBgGpsEnabled] = useState(false); const [bgGpsEnabled, setBgGpsEnabled] = useState(false);
const [backgroundMode, setBackgroundMode] = useState(true); // Default an const [backgroundMode, setBackgroundMode] = useState(true); // Default an
const [showSystemHints, setShowSystemHints] = useState(false); // Default aus const [showSystemHints, setShowSystemHints] = useState(false); // Default aus
const [showSource, setShowSource] = useState(false); // Quell-Badge, Default aus
const [scannerVisible, setScannerVisible] = useState(false); const [scannerVisible, setScannerVisible] = useState(false);
const [logTab, setLogTab] = useState<LogTab>('live'); const [logTab, setLogTab] = useState<LogTab>('live');
const [logs, setLogs] = useState<LogEntry[]>([]); const [logs, setLogs] = useState<LogEntry[]>([]);
@@ -261,6 +262,9 @@ const SettingsScreen: React.FC = () => {
// Default ist aus — nur explicit 'true' aktiviert // Default ist aus — nur explicit 'true' aktiviert
setShowSystemHints(saved === 'true'); setShowSystemHints(saved === 'true');
}); });
AsyncStorage.getItem('aria_show_source').then(saved => {
setShowSource(saved === 'true'); // Default aus
});
// gpsTrackingService status syncen + auf Aenderungen lauschen // gpsTrackingService status syncen + auf Aenderungen lauschen
setGpsTracking(gpsTrackingService.isActive()); setGpsTracking(gpsTrackingService.isActive());
const offGps = gpsTrackingService.onChange(setGpsTracking); const offGps = gpsTrackingService.onChange(setGpsTracking);
@@ -857,6 +861,11 @@ const SettingsScreen: React.FC = () => {
AsyncStorage.setItem('aria_show_hints', String(value)).catch(() => {}); AsyncStorage.setItem('aria_show_hints', String(value)).catch(() => {});
}, []); }, []);
const handleShowSourceToggle = useCallback((value: boolean) => {
setShowSource(value);
AsyncStorage.setItem('aria_show_source', String(value)).catch(() => {});
}, []);
// --- XTTS Voice --- // --- XTTS Voice ---
const selectVoice = useCallback((voiceName: string) => { const selectVoice = useCallback((voiceName: string) => {
@@ -1580,6 +1589,22 @@ const SettingsScreen: React.FC = () => {
thumbColor={showSystemHints ? '#FFFFFF' : '#666680'} thumbColor={showSystemHints ? '#FFFFFF' : '#666680'}
/> />
</View> </View>
<View style={styles.toggleRow}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>Antwort-Quelle anzeigen</Text>
<Text style={styles.toggleHint}>
Kleiner Badge an ARIAs Bubbles: ob die Antwort vom schnellen
lokalen Modell, von Claude oder per Direkt-Befehl kam. Nur fuer
dich interessant (Technik) — standardmaessig aus.
</Text>
</View>
<Switch
value={showSource}
onValueChange={handleShowSourceToggle}
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
thumbColor={showSource ? '#FFFFFF' : '#666680'}
/>
</View>
</View> </View>
{/* === Hintergrund-Modus === */} {/* === Hintergrund-Modus === */}