import { useState } from 'react'; import { useKeyboard, useRenderer } from '@opentui/react'; import { Theme } from './theme'; import { AppState, LogEntry, QRInfo } from './types'; import { RelayManager, AISettings, AddReplacement, DeleteReplacement } from './dialogs'; interface AppProps { theme: Theme; config: any; appState: AppState; logs: LogEntry[]; liveText: string; liveFinal: boolean; qrInfo: QRInfo | null; showQR: boolean; onCommand?: (cmd: string) => void; onClearLogs?: () => void; } type Tab = 'status' | 'devices' | 'settings' | 'logs'; export function App({ theme, config, appState, logs, liveText, liveFinal, qrInfo, showQR, onCommand, onClearLogs }: AppProps) { const renderer = useRenderer(); const [startTime] = useState(Date.now()); const [activeTab, setActiveTab] = useState('status'); const [activeDialog, setActiveDialog] = useState(null); // Keyboard shortcuts useKeyboard((key) => { if (key.ctrl && key.name === 'q') { renderer.destroy(); } else if (key.name === 'tab' && !key.shift) { const tabs: Tab[] = ['status', 'devices', 'settings', 'logs']; const idx = tabs.indexOf(activeTab); setActiveTab(tabs[(idx + 1) % tabs.length]); } else if (key.name === 'tab' && key.shift) { const tabs: Tab[] = ['status', 'devices', 'settings', 'logs']; const idx = tabs.indexOf(activeTab); setActiveTab(tabs[(idx - 1 + tabs.length) % tabs.length]); } else if (key.name === '1') { setActiveTab('status'); } else if (key.name === '2') { setActiveTab('devices'); } else if (key.name === '3') { setActiveTab('settings'); } else if (key.name === '4') { setActiveTab('logs'); } else if (key.ctrl && key.name === 'p') { onCommand?.('pause'); } else if (key.ctrl && key.name === 'l') { onClearLogs?.(); } else if (key.name === 'escape' && activeDialog) { setActiveDialog(null); } else if (activeTab === 'settings') { // Settings tab shortcuts if (key.name === 'a') { setActiveDialog('ai'); } else if (key.name === 'r') { setActiveDialog('relay'); } else if (key.name === '+' || key.name === '=') { setActiveDialog('add-replacement'); } else if (key.name === '-' || key.name === '_') { setActiveDialog('delete-replacement'); } } }); // Format uptime const formatUptime = () => { const s = Math.floor((Date.now() - startTime) / 1000); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const ss = s % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(ss).padStart(2, '0')}`; }; const renderTabBar = () => { const tabs: { id: Tab; label: string; key: string }[] = [ { id: 'status', label: 'Status', key: '1' }, { id: 'devices', label: 'Devices', key: '2' }, { id: 'settings', label: 'Settings', key: '3' }, { id: 'logs', label: 'Logs', key: '4' }, ]; return ( {' '} {tabs.map((tab, i) => ( <> {i > 0 && } {activeTab === tab.id && '▸ '} {tab.label} ({tab.key}) ))} ); }; const renderStatusTab = () => ( {/* Connection Status */} Connection {' '} {appState.connectedCount > 0 ? ( <> {' '} {appState.connectedCount} device{appState.connectedCount > 1 ? 's' : ''} connected ) : ( <> {' '} Waiting for devices... )} {appState.relayStatus !== 'disabled' && ( {' '} {appState.relayStatus === 'connected' ? '▲' : '◐'} {' '} Relay: {appState.relayStatus} )} {/* QR Code */} {showQR && qrInfo && ( Scan to Connect URL: {qrInfo.displayUrl} Mode: {qrInfo.mode} {qrInfo.qrCode && ( {qrInfo.qrCode} )} )} {/* Live Preview */} Live Transcript {liveFinal ? {liveText} : {liveText}} {/* Stats */} Uptime: {formatUptime()} {' '} Words: {appState.totalWords} {' '} Phrases: {appState.totalPhrases} {appState.paused && ( <> {' '} ■ PAUSED )} ); const renderDevicesTab = () => ( Connected Devices {appState.connectedCount > 0 ? ( {' '} {' '} {appState.connectedCount} device{appState.connectedCount > 1 ? 's' : ''} active ) : ( {' '} No devices connected. Scan the QR code to connect. )} Press 1 to view Status with QR code ); const renderSettingsTab = () => ( Settings {/* AI Settings */} {' '} AI Cleanup {' '} {config.aiEnabled ? 'Enabled' : 'Disabled'} {config.aiEnabled && ( {' '} Provider: {config.aiProvider} {' '} Model: {config.aiModel || 'default'} )} {' '} Press A to configure AI settings {/* Relay Settings */} {' '} Relay Server {' '} {appState.relayStatus !== 'disabled' ? appState.relayStatus : 'Not configured'} {config.relayUrl && ( {' '} URL: {config.relayUrl} )} {' '} Press R to manage relay servers {/* Word Replacements */} {' '} Word Replacements {' '} {Object.keys(config.wordReplacements || {}).length} active {' '} Press + to add, - to remove {/* Other Settings */} {' '} General {' '} Language: {config.language || 'en-US'} {' '} Clipboard mode: {config.clipboardMode ? 'Yes' : 'No'} {' '} Port: {config.port || 4000} ); const renderLogsTab = () => ( Activity Log Ctrl+L to clear {logs.length === 0 ? ( No activity yet... ) : ( logs.slice(-30).map((log, i) => ( {log.time} {' '} {getLogIcon(log.type)} {' '} {log.text} )) )} ); return ( {/* Header */} {' '} ◉ AirMic {' '} {formatUptime()} {appState.paused && ( <> {' '} ■ PAUSED )} {/* Tab Bar */} {renderTabBar()} {/* Tab Content */} {activeTab === 'status' && renderStatusTab()} {activeTab === 'devices' && renderDevicesTab()} {activeTab === 'settings' && renderSettingsTab()} {activeTab === 'logs' && renderLogsTab()} {/* Footer */} {' '} Tab: switch • Ctrl+P: pause • Ctrl+Q: quit {activeTab === 'settings' && ' • A: AI • R: relay • +/-: replacements'} {/* Dialogs */} {activeDialog === 'relay' && ( setActiveDialog(null)} onSave={(cfg) => { setActiveDialog(null); onCommand?.('save-config'); }} /> )} {activeDialog === 'ai' && ( setActiveDialog(null)} onSave={(cfg) => { setActiveDialog(null); onCommand?.('save-config'); }} /> )} {activeDialog === 'add-replacement' && ( setActiveDialog(null)} onSave={(from, to) => { setActiveDialog(null); onCommand?.('add-replacement'); }} /> )} {activeDialog === 'delete-replacement' && ( setActiveDialog(null)} onDelete={(key) => { setActiveDialog(null); onCommand?.('delete-replacement'); }} /> )} ); } function getLogColor(type: string, theme: Theme): string { const colors: Record = { phrase: theme.text, command: theme.primary, connect: theme.green, disconnect: theme.red, warn: theme.red, auth: theme.purple, info: theme.textMuted, }; return colors[type] || theme.textMuted; } function getLogIcon(type: string): string { const icons: Record = { phrase: ' ', command: '› ', connect: '+ ', disconnect: '- ', warn: '! ', auth: '# ', info: ' ', }; return icons[type] || ' '; }