feat(app): service_status Banner oben in ChatScreen

App-Pendant zum Diagnostic-Banner. Wenn die Gamebox-Bridges (F5-TTS /
Whisper) ihren Lade-Status broadcasten, zeigt die App oben unter der
Verbindungs-Statusleiste ein farbiges Banner:
  Gelb  = irgendwas laedt   (NICHT wegtippbar)
  Gruen = alles bereit      (tippbar zum Schliessen)
  Rot   = Fehler

Banner aggregiert beide Services in einer Kachel. Dismiss-State wird
zurueckgesetzt sobald irgendein Service wieder in 'loading' geht
(z.B. Modell-Wechsel via Diagnostic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-24 16:24:47 +02:00
parent 2f625572fc
commit 8ba6a71a49
1 changed files with 83 additions and 0 deletions

View File

@ -108,6 +108,9 @@ const ChatScreen: React.FC = () => {
const [searchVisible, setSearchVisible] = useState(false);
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
// 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 [serviceBannerDismissed, setServiceBannerDismissed] = useState(false);
// Gerätelokale TTS-Config: globaler Toggle (aus Settings) + temporäres Muten (Mund-Button)
const [ttsDeviceEnabled, setTtsDeviceEnabled] = useState(true);
const [ttsMuted, setTtsMuted] = useState(false);
@ -351,6 +354,24 @@ const ChatScreen: React.FC = () => {
ToastAndroid.show(`Stimme "${v || 'Standard'}" bereit`, ToastAndroid.SHORT);
}
}
// Gamebox-Bridges (f5tts/whisper) melden Lade-Status — Banner oben
if (message.type === ('service_status' as any)) {
const p = message.payload as any;
const svc = (p?.service as string) || '';
if (!svc) return;
setServiceStatus(prev => ({
...prev,
[svc]: {
state: (p?.state as string) || 'unknown',
model: p?.model as string | undefined,
loadSeconds: p?.loadSeconds as number | undefined,
error: p?.error as string | undefined,
},
}));
// Bei neuer Loading-Phase Banner wieder aktivieren
if (p?.state === 'loading') setServiceBannerDismissed(false);
}
});
const unsubState = rvs.onStateChange((state) => {
@ -764,6 +785,49 @@ const ChatScreen: React.FC = () => {
</TouchableOpacity>
</View>
{/* Service-Status Banner (Gamebox: F5-TTS / Whisper Lade-Status) */}
{(() => {
const entries = Object.entries(serviceStatus);
if (entries.length === 0 || serviceBannerDismissed) return null;
const anyLoading = entries.some(([, v]) => v.state === 'loading');
const anyError = entries.some(([, v]) => v.state === 'error');
const allReady = !anyLoading && !anyError && entries.every(([, v]) => v.state === 'ready');
const bg = anyError ? '#3A1F1F' : anyLoading ? '#3A331F' : '#1F3A2A';
const border = anyError ? '#FF3B30' : anyLoading ? '#FFD60A' : '#34C759';
const labels: Record<string, string> = { f5tts: 'F5-TTS', whisper: 'Whisper STT' };
return (
<TouchableOpacity
activeOpacity={allReady ? 0.6 : 1.0}
onPress={() => { if (allReady) setServiceBannerDismissed(true); }}
style={[styles.serviceBanner, { backgroundColor: bg, borderColor: border }]}
>
{entries.map(([svc, info]) => {
let icon = '\u23F3', text = '';
if (info.state === 'loading') {
text = `${labels[svc] || svc}: laedt${info.model ? ' ' + info.model : ''}...`;
} else if (info.state === 'ready') {
icon = '\u2705';
const sec = info.loadSeconds ? ` (${info.loadSeconds.toFixed(1)}s)` : '';
text = `${labels[svc] || svc}: bereit${info.model ? ' ' + info.model : ''}${sec}`;
} else if (info.state === 'error') {
icon = '\u274C';
text = `${labels[svc] || svc}: Fehler ${info.error || ''}`;
} else {
text = `${labels[svc] || svc}: ${info.state}`;
}
return (
<Text key={svc} style={styles.serviceBannerLine}>
{icon} {text}
</Text>
);
})}
<Text style={styles.serviceBannerHint}>
{allReady ? 'Tippen zum Schliessen' : 'Bitte warten...'}
</Text>
</TouchableOpacity>
);
})()}
{/* Suchleiste */}
{searchVisible && (
<View style={styles.searchBar}>
@ -978,6 +1042,25 @@ const styles = StyleSheet.create({
color: '#8888AA',
fontSize: 12,
},
serviceBanner: {
paddingVertical: 8,
paddingHorizontal: 12,
borderTopWidth: 0,
borderBottomWidth: 1,
borderLeftWidth: 0,
borderRightWidth: 0,
},
serviceBannerLine: {
color: '#FFFFFF',
fontSize: 12,
lineHeight: 18,
},
serviceBannerHint: {
color: '#AAAACC',
fontSize: 10,
marginTop: 2,
fontStyle: 'italic',
},
messageList: {
padding: 12,
paddingBottom: 8,