diff --git a/packages/client-android/App.tsx b/packages/client-android/App.tsx index 8144162..e0849f6 100644 --- a/packages/client-android/App.tsx +++ b/packages/client-android/App.tsx @@ -2,14 +2,21 @@ import { StatusBar } from 'expo-status-bar'; import { useState } from 'react'; import { StyleSheet, Text, View, TextInput, TouchableOpacity, - ScrollView, Switch, + ScrollView, Switch, FlatList, KeyboardAvoidingView, Platform, } from 'react-native'; import { DEFAULT_GATEWAY_CONFIG } from '@yuzu-gca/shared'; -/** U-001: Settings Page for Yuzu GCA Android Client */ +/** Android Client: Chat + Settings + Connection */ + +type Msg = { role: 'user' | 'assistant'; text: string; ts: number }; export default function App() { - const [tab, setTab] = useState<'settings' | 'device' | 'connection'>('settings'); + const [tab, setTab] = useState<'chat' | '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); @@ -21,134 +28,206 @@ export default function App() { // 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 - Settings + + Yuzu GCA + + {/* Tabs */} - {(['settings', 'device', 'connection'] as const).map(t => ( + {(['chat', 'settings', 'device', 'connection'] as const).map(t => ( setTab(t)} style={[styles.tab, tab === t && styles.tabActive]}> - {t === 'settings' ? 'Gateway' : t === 'device' ? 'Device' : 'Connection'} + {t === 'chat' ? 'Chat' : t === 'settings' ? 'Gateway' : t === 'device' ? 'Device' : (connStatus === 'connected' ? '🟢' : '⚫')} ))} - - {tab === 'settings' && ( - <> - {/* Gateway URL */} - Gateway URL + {/* ── 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 + } + /> + - - {/* Heartbeat Interval */} - Heartbeat (seconds) - - - {/* Auto-connect */} - - Auto-connect on start - - - - {/* Log Level */} - Log Level - - {(['debug', 'info', 'warn', 'error'] as const).map(lv => ( - setLogLevel(lv)} - > - {lv} - - ))} - - - )} - - {tab === 'device' && ( - <> - Device Name - - - Device Info - Platform: Android - MCP Server: {gatewayUrl ? 'Configured' : 'Not configured'} - Capabilities: 36 tools (7 implemented) - - - )} - - {tab === 'connection' && ( - <> - - - {connStatus === 'connected' ? '🟢 Connected' : - connStatus === 'connecting' ? '🟡 Connecting...' : - connStatus === 'error' ? '🔴 Error' : '⚫ Disconnected'} - - - setConnStatus(connStatus === 'disconnected' ? 'connecting' : 'disconnected')} - > - - {connStatus === 'disconnected' ? 'Connect' : 'Disconnect'} - + + {sending ? '...' : 'Send'} - - )} - - + + + )} + + {/* ── 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({ - container: { flex: 1, backgroundColor: '#0f172a', paddingTop: 48 }, - title: { fontSize: 28, fontWeight: '800', color: '#e2e8f0', textAlign: 'center' }, - subtitle: { fontSize: 12, color: '#64748b', textAlign: 'center', marginBottom: 16 }, - tabBar: { flexDirection: 'row', marginHorizontal: 16, marginBottom: 16 }, - tab: { - flex: 1, padding: 10, alignItems: 'center', - borderBottomWidth: 2, borderBottomColor: '#1e293b', - }, + 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: 14, color: '#64748b' }, + 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, marginBottom: 4, - }, + 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' }, @@ -158,6 +237,8 @@ const styles = StyleSheet.create({ 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' },