import { StatusBar } from 'expo-status-bar'; import { useState } from 'react'; import { StyleSheet, Text, View, TextInput, TouchableOpacity, ScrollView, Switch, FlatList, KeyboardAvoidingView, Platform, } from 'react-native'; import { DEFAULT_GATEWAY_CONFIG } from '@yuzu-gca/shared'; /** Android Client: Chat + Settings + Connection */ type Msg = { role: 'user' | 'assistant'; text: string; ts: number }; export default function App() { const [tab, setTab] = useState<'chat' | 'terminal' | 'settings' | 'device' | 'connection'>('chat'); // Chat state const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); // Settings state const [gatewayUrl, setGatewayUrl] = useState(DEFAULT_GATEWAY_CONFIG.url); const [deviceName, setDeviceName] = useState('Android Device'); const [autoConnect, setAutoConnect] = useState(true); const [heartbeatInterval, setHeartbeatInterval] = useState(String(DEFAULT_GATEWAY_CONFIG.heartbeatInterval / 1000)); const [logLevel, setLogLevel] = useState('info'); // Connection state const [connStatus, setConnStatus] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected'); /** Send chat message to Gateway */ const sendMessage = async () => { const text = input.trim(); if (!text || sending) return; const userMsg: Msg = { role: 'user', text, ts: Date.now() }; setMessages(prev => [...prev, userMsg]); setInput(''); setSending(true); try { const resp = await fetch(`${gatewayUrl}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }), signal: AbortSignal.timeout(30000), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json() as { reply?: string; error?: string }; const aiMsg: Msg = { role: 'assistant', text: data.reply ?? data.error ?? 'No response', ts: Date.now(), }; setMessages(prev => [...prev, aiMsg]); } catch (e: any) { setMessages(prev => [...prev, { role: 'assistant', text: `Error: ${e.message}`, ts: Date.now(), }]); } finally { setSending(false); } }; /** Connect/disconnect toggle */ const toggleConnection = () => { setConnStatus(prev => { if (prev === 'disconnected' || prev === 'error') return 'connecting'; return 'disconnected'; }); }; // ── Render ────────────────────────────────────────────── return ( {/* Header */} Yuzu GCA {/* Tabs */} {(['chat', 'terminal', 'settings', 'device', 'connection'] as const).map(t => ( setTab(t)} style={[styles.tab, tab === t && styles.tabActive]}> {t === 'chat' ? 'Chat' : t === 'terminal' ? 'Term' : t === 'settings' ? 'Gateway' : t === 'device' ? 'Device' : (connStatus === 'connected' ? '🟢' : '⚫')} ))} {/* ── Chat Tab ── */} {tab === 'chat' && ( String(i)} style={styles.chatList} renderItem={({ item }) => ( {item.role === 'user' ? 'You' : 'GCA'} {item.text} )} ListEmptyComponent={ Send a message to control your devices via AI } /> {sending ? '...' : 'Send'} )} {/* ── Terminal Tab ── */} {tab === 'terminal' && ( (() => { const [cmd, setCmd] = useState(''); const [out, setOut] = useState('Execute remote commands via Gateway MCP.\nType a shell command and press Execute.\n'); const [running, setRunning] = useState(false); const runCmd = async () => { if (!cmd.trim() || running) return; const c = cmd.trim(); setCmd(''); setRunning(true); setOut(o => o + `$ ${c}\n`); try { const r = await fetch(`${gatewayUrl}/tools/exec`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({command:c}), signal:AbortSignal.timeout(30000) }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const d = await r.json() as any; setOut(o => o + (d.stdout||d.stderr||JSON.stringify(d)) + '\n'); } catch(e:any) { setOut(o => o + `Error: ${e.message}\n`); } finally { setRunning(false); } }; return ( {out} {running ? '...' : 'Run'} ); })() )} {/* ── Settings Tab ── */} {tab === 'settings' && ( Gateway URL Heartbeat (seconds) Auto-connect on start Log Level {(['debug', 'info', 'warn', 'error'] as const).map(lv => ( setLogLevel(lv)}> {lv} ))} )} {/* ── Device Tab ── */} {tab === 'device' && ( Device Name Device Info Platform: Android Gateway: {gatewayUrl} Capabilities: exec, file_list, sysinfo... MCP Tools: 36 defined, 7 implemented )} {/* ── Connection Tab ── */} {tab === 'connection' && ( {connStatus === 'connected' ? '🟢 Connected' : connStatus === 'connecting' ? '🟡 Connecting...' : connStatus === 'error' ? '🔴 Error' : '⚫ Disconnected'} {connStatus === 'disconnected' ? 'Connect' : 'Disconnect'} )} ); } // ── Styles ──────────────────────────────────────────────── const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: '#0f172a' }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 48, paddingBottom: 8 }, title: { fontSize: 22, fontWeight: '800', color: '#e2e8f0' }, statusDot: { width: 10, height: 10, borderRadius: 5 }, dot_connected: { backgroundColor: '#22c55e' }, dot_connecting: { backgroundColor: '#eab308' }, dot_error: { backgroundColor: '#ef4444' }, dot_disconnected: { backgroundColor: '#475569' }, // Tabs tabBar: { flexDirection: 'row', marginHorizontal: 16, marginBottom: 8 }, tab: { flex: 1, padding: 10, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: '#1e293b' }, tabActive: { borderBottomColor: '#3b82f6' }, tabText: { fontSize: 13, color: '#64748b' }, tabTextActive: { color: '#3b82f6', fontWeight: '600' }, // Chat chatContainer: { flex: 1 }, chatList: { flex: 1, paddingHorizontal: 12 }, bubble: { marginVertical: 4, padding: 10, borderRadius: 10, maxWidth: '85%' }, bubbleUser: { alignSelf: 'flex-end', backgroundColor: '#1e40af' }, bubbleAI: { alignSelf: 'flex-start', backgroundColor: '#1e293b', borderWidth: 1, borderColor: '#334155' }, bubbleRole: { fontSize: 10, color: '#94a3b8', marginBottom: 2 }, bubbleText: { fontSize: 14, color: '#e2e8f0' }, emptyText: { textAlign: 'center', color: '#475569', marginTop: 40, fontSize: 14 }, inputBar: { flexDirection: 'row', padding: 8, paddingBottom: 16, gap: 8 }, chatInput: { flex: 1, backgroundColor: '#1e293b', color: '#e2e8f0', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 }, sendBtn: { backgroundColor: '#1e40af', borderRadius: 8, paddingHorizontal: 16, justifyContent: 'center' }, sendBtnDisabled: { opacity: 0.5 }, sendBtnText: { color: '#fff', fontWeight: '600', fontSize: 14 }, // Settings body: { flex: 1, paddingHorizontal: 16 }, label: { fontSize: 12, color: '#94a3b8', marginBottom: 4, marginTop: 12 }, input: { backgroundColor: '#1e293b', color: '#e2e8f0', borderRadius: 8, padding: 12, fontSize: 14 }, switchRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 12 }, chipRow: { flexDirection: 'row', gap: 8, marginTop: 4 }, chip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: '#1e293b' }, chipActive: { backgroundColor: '#1e40af' }, chipText: { fontSize: 12, color: '#94a3b8' }, chipTextActive: { color: '#fff' }, infoCard: { backgroundColor: '#1e293b', borderRadius: 8, padding: 16, marginTop: 16 }, infoTitle: { fontSize: 14, color: '#38bdf8', marginBottom: 8 }, infoText: { fontSize: 12, color: '#94a3b8', marginTop: 4 }, // Connection connIndicator: { borderRadius: 8, padding: 16, marginTop: 16, alignItems: 'center' }, conn_connected: { backgroundColor: '#064e3b' }, conn_connecting: { backgroundColor: '#78350f' }, conn_error: { backgroundColor: '#7f1d1d' }, conn_disconnected: { backgroundColor: '#1e293b' }, connText: { fontSize: 16, color: '#e2e8f0' }, termOutput: { flex: 1, backgroundColor: '#0a0f1a', margin: 8, borderRadius: 8, padding: 12 }, termText: { fontFamily: 'monospace', fontSize: 12, color: '#22c55e', lineHeight: 18 }, btn: { borderRadius: 8, padding: 14, marginTop: 16, alignItems: 'center' }, btnPrimary: { backgroundColor: '#1e40af' }, btnDanger: { backgroundColor: '#991b1b' }, btnText: { fontSize: 16, color: '#fff', fontWeight: '600' }, });