@mobileai/react-native 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +21 -2
  2. package/lib/module/components/AIAgent.js +216 -5
  3. package/lib/module/components/AIAgent.js.map +1 -1
  4. package/lib/module/components/AgentChatBar.js +358 -36
  5. package/lib/module/components/AgentChatBar.js.map +1 -1
  6. package/lib/module/core/AgentRuntime.js +122 -6
  7. package/lib/module/core/AgentRuntime.js.map +1 -1
  8. package/lib/module/core/systemPrompt.js +57 -0
  9. package/lib/module/core/systemPrompt.js.map +1 -1
  10. package/lib/module/index.js +8 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/providers/GeminiProvider.js +108 -85
  13. package/lib/module/providers/GeminiProvider.js.map +1 -1
  14. package/lib/module/services/AudioInputService.js +128 -0
  15. package/lib/module/services/AudioInputService.js.map +1 -0
  16. package/lib/module/services/AudioOutputService.js +154 -0
  17. package/lib/module/services/AudioOutputService.js.map +1 -0
  18. package/lib/module/services/VoiceService.js +362 -0
  19. package/lib/module/services/VoiceService.js.map +1 -0
  20. package/lib/module/utils/audioUtils.js +49 -0
  21. package/lib/module/utils/audioUtils.js.map +1 -0
  22. package/lib/module/utils/logger.js +21 -4
  23. package/lib/module/utils/logger.js.map +1 -1
  24. package/lib/typescript/babel.config.d.ts +10 -0
  25. package/lib/typescript/babel.config.d.ts.map +1 -0
  26. package/lib/typescript/eslint.config.d.mts +3 -0
  27. package/lib/typescript/eslint.config.d.mts.map +1 -0
  28. package/lib/typescript/fetch-models.d.mts +2 -0
  29. package/lib/typescript/fetch-models.d.mts.map +1 -0
  30. package/lib/typescript/list-all-models.d.mts +2 -0
  31. package/lib/typescript/list-all-models.d.mts.map +1 -0
  32. package/lib/typescript/list-models.d.mts +2 -0
  33. package/lib/typescript/list-models.d.mts.map +1 -0
  34. package/lib/typescript/src/components/AIAgent.d.ts +8 -2
  35. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  36. package/lib/typescript/src/components/AgentChatBar.d.ts +19 -2
  37. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
  38. package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
  39. package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
  40. package/lib/typescript/src/core/systemPrompt.d.ts +8 -0
  41. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
  42. package/lib/typescript/src/core/types.d.ts +24 -1
  43. package/lib/typescript/src/core/types.d.ts.map +1 -1
  44. package/lib/typescript/src/index.d.ts +6 -1
  45. package/lib/typescript/src/index.d.ts.map +1 -1
  46. package/lib/typescript/src/providers/GeminiProvider.d.ts +22 -18
  47. package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -1
  48. package/lib/typescript/src/services/AudioInputService.d.ts +31 -0
  49. package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -0
  50. package/lib/typescript/src/services/AudioOutputService.d.ts +34 -0
  51. package/lib/typescript/src/services/AudioOutputService.d.ts.map +1 -0
  52. package/lib/typescript/src/services/VoiceService.d.ts +73 -0
  53. package/lib/typescript/src/services/VoiceService.d.ts.map +1 -0
  54. package/lib/typescript/src/utils/audioUtils.d.ts +17 -0
  55. package/lib/typescript/src/utils/audioUtils.d.ts.map +1 -0
  56. package/lib/typescript/src/utils/logger.d.ts +4 -0
  57. package/lib/typescript/src/utils/logger.d.ts.map +1 -1
  58. package/package.json +24 -8
  59. package/src/components/AIAgent.tsx +222 -3
  60. package/src/components/AgentChatBar.tsx +487 -42
  61. package/src/core/AgentRuntime.ts +131 -2
  62. package/src/core/systemPrompt.ts +62 -0
  63. package/src/core/types.ts +30 -0
  64. package/src/index.ts +16 -0
  65. package/src/providers/GeminiProvider.ts +105 -89
  66. package/src/services/AudioInputService.ts +141 -0
  67. package/src/services/AudioOutputService.ts +167 -0
  68. package/src/services/VoiceService.ts +409 -0
  69. package/src/utils/audioUtils.ts +54 -0
  70. package/src/utils/logger.ts +24 -7
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * AgentChatBar — Floating, draggable, compressible chat widget.
3
+ * Supports two modes: Text and Voice.
3
4
  * Does not block underlying UI natively.
4
5
  */
5
6
 
@@ -14,7 +15,9 @@ import {
14
15
  PanResponder,
15
16
  useWindowDimensions,
16
17
  } from 'react-native';
17
- import type { ExecutionResult } from '../core/types';
18
+ import type { ExecutionResult, AgentMode } from '../core/types';
19
+
20
+ // ─── Props ─────────────────────────────────────────────────────
18
21
 
19
22
  interface AgentChatBarProps {
20
23
  onSend: (message: string) => void;
@@ -22,23 +25,354 @@ interface AgentChatBarProps {
22
25
  lastResult: ExecutionResult | null;
23
26
  language: 'en' | 'ar';
24
27
  onDismiss?: () => void;
28
+ /** Available modes (default: ['text']) */
29
+ availableModes?: AgentMode[];
30
+ /** Current active mode */
31
+ mode?: AgentMode;
32
+ onModeChange?: (mode: AgentMode) => void;
33
+ /** Voice controls */
34
+ onMicToggle?: (active: boolean) => void;
35
+ onSpeakerToggle?: (muted: boolean) => void;
36
+ isMicActive?: boolean;
37
+ isSpeakerMuted?: boolean;
38
+ /** AI is currently speaking */
39
+ isAISpeaking?: boolean;
40
+ /** Voice WebSocket is connected */
41
+ isVoiceConnected?: boolean;
42
+ /** Full session cleanup (stop mic, audio, WebSocket, live mode) */
43
+ onStopSession?: () => void;
44
+ }
45
+
46
+ // ─── Mode Selector ─────────────────────────────────────────────
47
+
48
+ function ModeSelector({
49
+ modes,
50
+ activeMode,
51
+ onSelect,
52
+ }: {
53
+ modes: AgentMode[];
54
+ activeMode: AgentMode;
55
+ onSelect: (mode: AgentMode) => void;
56
+ }) {
57
+ if (modes.length <= 1) return null;
58
+
59
+ const labels: Record<AgentMode, { icon: string; label: string }> = {
60
+ text: { icon: '💬', label: 'Text' },
61
+ voice: { icon: '🎙️', label: 'Live Agent' },
62
+ };
63
+
64
+ return (
65
+ <View style={modeStyles.container}>
66
+ {modes.map((mode) => (
67
+ <Pressable
68
+ key={mode}
69
+ style={[
70
+ modeStyles.tab,
71
+ activeMode === mode && modeStyles.tabActive,
72
+ ]}
73
+ onPress={() => onSelect(mode)}
74
+ accessibilityLabel={`Switch to ${labels[mode].label} mode`}
75
+ >
76
+ <Text style={modeStyles.tabIcon}>{labels[mode].icon}</Text>
77
+ <Text
78
+ style={[
79
+ modeStyles.tabLabel,
80
+ activeMode === mode && modeStyles.tabLabelActive,
81
+ ]}
82
+ >
83
+ {labels[mode].label}
84
+ </Text>
85
+ </Pressable>
86
+ ))}
87
+ </View>
88
+ );
89
+ }
90
+
91
+ // ─── Audio Control Button ──────────────────────────────────────
92
+
93
+ function AudioControlButton({
94
+ icon,
95
+ activeIcon,
96
+ isActive,
97
+ onPress,
98
+ label,
99
+ size = 36,
100
+ }: {
101
+ icon: string;
102
+ activeIcon: string;
103
+ isActive: boolean;
104
+ onPress: () => void;
105
+ label: string;
106
+ size?: number;
107
+ }) {
108
+ return (
109
+ <Pressable
110
+ style={[
111
+ audioStyles.controlBtn,
112
+ { width: size, height: size, borderRadius: size / 2 },
113
+ isActive && audioStyles.controlBtnActive,
114
+ ]}
115
+ onPress={onPress}
116
+ accessibilityLabel={label}
117
+ hitSlop={8}
118
+ >
119
+ <Text style={audioStyles.controlIcon}>{isActive ? activeIcon : icon}</Text>
120
+ </Pressable>
121
+ );
122
+ }
123
+
124
+ // ─── Dictation Button (optional expo-speech-recognition) ──────
125
+
126
+ /**
127
+ * Try to load expo-speech-recognition as an optional peer dependency.
128
+ * If not installed, returns null and the mic button won't render.
129
+ * Same pattern as react-native-view-shot for screenshots.
130
+ */
131
+ let SpeechModule: any = null;
132
+ try {
133
+ SpeechModule = require('expo-speech-recognition');
134
+ } catch {
135
+ // Not installed — dictation button won't appear
136
+ }
137
+
138
+ function DictationButton({
139
+ language,
140
+ onTranscript,
141
+ disabled,
142
+ }: {
143
+ language: string;
144
+ onTranscript: (text: string) => void;
145
+ disabled: boolean;
146
+ }) {
147
+ const [isListening, setIsListening] = useState(false);
148
+
149
+ // Don't render if expo-speech-recognition isn't installed
150
+ if (!SpeechModule) return null;
151
+
152
+ const { ExpoSpeechRecognitionModule } = SpeechModule;
153
+ if (!ExpoSpeechRecognitionModule) return null;
154
+
155
+ const toggle = async () => {
156
+ if (isListening) {
157
+ ExpoSpeechRecognitionModule.stop();
158
+ return;
159
+ }
160
+
161
+ try {
162
+ const perms = await ExpoSpeechRecognitionModule.requestPermissionsAsync();
163
+ if (!perms.granted) return;
164
+
165
+ // Register one-shot listeners for this recording session
166
+ const resultListener = ExpoSpeechRecognitionModule.addListener(
167
+ 'result',
168
+ (event: any) => {
169
+ const transcript = event.results?.[0]?.transcript;
170
+ if (transcript && event.isFinal) {
171
+ onTranscript(transcript);
172
+ }
173
+ },
174
+ );
175
+
176
+ const endListener = ExpoSpeechRecognitionModule.addListener(
177
+ 'end',
178
+ () => {
179
+ setIsListening(false);
180
+ resultListener.remove();
181
+ endListener.remove();
182
+ },
183
+ );
184
+
185
+ ExpoSpeechRecognitionModule.start({
186
+ lang: language === 'ar' ? 'ar-SA' : 'en-US',
187
+ interimResults: false,
188
+ continuous: false,
189
+ addsPunctuation: true,
190
+ });
191
+
192
+ setIsListening(true);
193
+ } catch {
194
+ setIsListening(false);
195
+ }
196
+ };
197
+
198
+ return (
199
+ <Pressable
200
+ style={[
201
+ styles.dictationButton,
202
+ isListening && styles.dictationButtonActive,
203
+ disabled && styles.sendButtonDisabled,
204
+ ]}
205
+ onPress={toggle}
206
+ disabled={disabled}
207
+ accessibilityLabel={isListening ? 'Stop dictation' : 'Start dictation'}
208
+ hitSlop={8}
209
+ >
210
+ <Text style={styles.sendButtonText}>
211
+ {isListening ? '⏹️' : '🎤'}
212
+ </Text>
213
+ </Pressable>
214
+ );
215
+ }
216
+
217
+ // ─── Text Input Row ────────────────────────────────────────────
218
+
219
+ function TextInputRow({
220
+ text,
221
+ setText,
222
+ onSend,
223
+ isThinking,
224
+ isArabic,
225
+ }: {
226
+ text: string;
227
+ setText: (t: string) => void;
228
+ onSend: () => void;
229
+ isThinking: boolean;
230
+ isArabic: boolean;
231
+ }) {
232
+ return (
233
+ <View style={styles.inputRow}>
234
+ <TextInput
235
+ style={[styles.input, isArabic && styles.inputRTL]}
236
+ placeholder={isArabic ? 'اكتب طلبك...' : 'Ask AI...'}
237
+ placeholderTextColor="#999"
238
+ value={text}
239
+ onChangeText={setText}
240
+ onSubmitEditing={onSend}
241
+ returnKeyType="send"
242
+ editable={!isThinking}
243
+ multiline={false}
244
+ />
245
+ <DictationButton
246
+ language={isArabic ? 'ar' : 'en'}
247
+ onTranscript={(t: string) => setText(t)}
248
+ disabled={isThinking}
249
+ />
250
+ <Pressable
251
+ style={[styles.sendButton, isThinking && styles.sendButtonDisabled]}
252
+ onPress={onSend}
253
+ disabled={isThinking || !text.trim()}
254
+ accessibilityLabel="Send request to AI Agent"
255
+ >
256
+ <Text style={styles.sendButtonText}>
257
+ {isThinking ? '⏳' : '🚀'}
258
+ </Text>
259
+ </Pressable>
260
+ </View>
261
+ );
262
+ }
263
+
264
+ // ─── Voice Controls Row ────────────────────────────────────────
265
+
266
+ function VoiceControlsRow({
267
+ isMicActive,
268
+ isSpeakerMuted,
269
+ onMicToggle,
270
+ onSpeakerToggle,
271
+ isAISpeaking,
272
+ isVoiceConnected = false,
273
+ isArabic,
274
+ onStopSession,
275
+ }: {
276
+ isMicActive: boolean;
277
+ isSpeakerMuted: boolean;
278
+ onMicToggle: (active: boolean) => void;
279
+ onSpeakerToggle: (muted: boolean) => void;
280
+ isAISpeaking?: boolean;
281
+ isVoiceConnected?: boolean;
282
+ isArabic: boolean;
283
+ onStopSession?: () => void;
284
+ }) {
285
+ const isConnecting = !isVoiceConnected;
286
+
287
+ return (
288
+ <View style={styles.inputRow}>
289
+ {/* Speaker mute/unmute */}
290
+ <AudioControlButton
291
+ icon="🔊"
292
+ activeIcon="🔇"
293
+ isActive={isSpeakerMuted}
294
+ onPress={() => onSpeakerToggle(!isSpeakerMuted)}
295
+ label={isSpeakerMuted ? 'Unmute speaker' : 'Mute speaker'}
296
+ />
297
+
298
+ {/* Mic button — large center */}
299
+ <Pressable
300
+ style={[
301
+ audioStyles.micButton,
302
+ isConnecting && audioStyles.micButtonConnecting,
303
+ isMicActive && audioStyles.micButtonActive,
304
+ isAISpeaking && audioStyles.micButtonSpeaking,
305
+ ]}
306
+ onPress={() => {
307
+ if (isMicActive) {
308
+ // Stop button: full session cleanup
309
+ onStopSession?.();
310
+ } else if (!isConnecting) {
311
+ // Talk button: start mic
312
+ onMicToggle(true);
313
+ }
314
+ }}
315
+ disabled={isConnecting}
316
+ accessibilityLabel={
317
+ isConnecting ? 'Connecting...' :
318
+ isMicActive ? 'Stop recording' : 'Start recording'
319
+ }
320
+ >
321
+ <Text style={audioStyles.micIcon}>
322
+ {isConnecting ? '🔄' : isAISpeaking ? '🔊' : isMicActive ? '⏹️' : '🎙️'}
323
+ </Text>
324
+ <Text style={audioStyles.micLabel}>
325
+ {isConnecting
326
+ ? (isArabic ? 'جاري الاتصال...' : 'Connecting...')
327
+ : isAISpeaking
328
+ ? (isArabic ? 'يتحدث...' : 'Speaking...')
329
+ : isMicActive
330
+ ? (isArabic ? 'إيقاف' : 'Stop')
331
+ : (isArabic ? 'تحدث' : 'Talk')}
332
+ </Text>
333
+ </Pressable>
334
+
335
+ {/* Connection status indicator */}
336
+ <View style={[
337
+ audioStyles.statusDot,
338
+ isVoiceConnected ? audioStyles.statusDotConnected : audioStyles.statusDotConnecting,
339
+ ]} />
340
+ </View>
341
+ );
25
342
  }
26
343
 
27
- export function AgentChatBar({ onSend, isThinking, lastResult, language, onDismiss }: AgentChatBarProps) {
344
+
345
+ // ─── Main Component ────────────────────────────────────────────
346
+
347
+ export function AgentChatBar({
348
+ onSend,
349
+ isThinking,
350
+ lastResult,
351
+ language,
352
+ onDismiss,
353
+ availableModes = ['text'],
354
+ mode = 'text',
355
+ onModeChange,
356
+ onMicToggle,
357
+ onSpeakerToggle,
358
+ isMicActive = false,
359
+ isSpeakerMuted = false,
360
+ isAISpeaking,
361
+ isVoiceConnected,
362
+ onStopSession,
363
+ }: AgentChatBarProps) {
28
364
  const [text, setText] = useState('');
29
365
  const [isExpanded, setIsExpanded] = useState(false);
30
366
  const { height } = useWindowDimensions();
31
367
  const isArabic = language === 'ar';
32
368
 
33
- // Initial position: Bottom right for FAB, Bottom center for Expanded
34
- // For simplicity, we just initialize to a safe generic spot on screen.
35
369
  const pan = useRef(new Animated.ValueXY({ x: 10, y: height - 200 })).current;
36
370
 
37
- // PanResponder for dragging the widget
371
+
372
+
38
373
  const panResponder = useRef(
39
374
  PanResponder.create({
40
375
  onMoveShouldSetPanResponder: (_, gestureState) => {
41
- // Only trigger drag if moving more than 5px (allows taps to register inside)
42
376
  return Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5;
43
377
  },
44
378
  onPanResponderGrant: () => {
@@ -65,25 +399,31 @@ export function AgentChatBar({ onSend, isThinking, lastResult, language, onDismi
65
399
  }
66
400
  };
67
401
 
68
- // ─── Compressed State (FAB) ───
402
+ // ─── FAB (Compressed) ──────────────────────────────────────
403
+
69
404
  if (!isExpanded) {
405
+ const fabIcon = isThinking ? '⏳' : '🤖';
70
406
  return (
71
- <Animated.View style={[styles.fabContainer, pan.getLayout()]} {...panResponder.panHandlers}>
72
- <Pressable
73
- style={styles.fab}
407
+ <Animated.View
408
+ style={[styles.fabContainer, pan.getLayout()]}
409
+ {...panResponder.panHandlers}
410
+ >
411
+ <Pressable
412
+ style={styles.fab}
74
413
  onPress={() => setIsExpanded(true)}
75
414
  accessibilityLabel="Open AI Agent Chat"
76
415
  >
77
- <Text style={styles.fabIcon}>{isThinking ? '⏳' : '🤖'}</Text>
416
+ <Text style={styles.fabIcon}>{fabIcon}</Text>
78
417
  </Pressable>
79
418
  </Animated.View>
80
419
  );
81
420
  }
82
421
 
83
- // ─── Expanded State (Widget) ───
422
+ // ─── Expanded Widget ───────────────────────────────────────
423
+
84
424
  return (
85
425
  <Animated.View style={[styles.expandedContainer, pan.getLayout()]}>
86
- {/* Drag Handle Area */}
426
+ {/* Drag Handle */}
87
427
  <View {...panResponder.panHandlers} style={styles.dragHandleArea} accessibilityLabel="Drag AI Agent">
88
428
  <View style={styles.dragGrip} />
89
429
  <Pressable onPress={() => setIsExpanded(false)} style={styles.minimizeBtn} accessibilityLabel="Minimize AI Agent">
@@ -91,7 +431,14 @@ export function AgentChatBar({ onSend, isThinking, lastResult, language, onDismi
91
431
  </Pressable>
92
432
  </View>
93
433
 
94
- {/* Result message */}
434
+ {/* Mode Selector */}
435
+ <ModeSelector
436
+ modes={availableModes}
437
+ activeMode={mode}
438
+ onSelect={(m) => onModeChange?.(m)}
439
+ />
440
+
441
+ {/* Result Bubble */}
95
442
  {lastResult && (
96
443
  <View style={[styles.resultBubble, lastResult.success ? styles.resultSuccess : styles.resultError]}>
97
444
  <Text style={styles.resultText}>{lastResult.message}</Text>
@@ -103,36 +450,38 @@ export function AgentChatBar({ onSend, isThinking, lastResult, language, onDismi
103
450
  </View>
104
451
  )}
105
452
 
106
- {/* Input row */}
107
- <View style={styles.inputRow}>
108
- <TextInput
109
- style={[styles.input, isArabic && styles.inputRTL]}
110
- placeholder={isArabic ? 'اكتب طلبك...' : 'Ask AI...'}
111
- placeholderTextColor="#999"
112
- value={text}
113
- onChangeText={setText}
114
- onSubmitEditing={handleSend}
115
- returnKeyType="send"
116
- editable={!isThinking}
117
- multiline={false}
453
+ {/* Mode-specific input */}
454
+ {mode === 'text' && (
455
+ <TextInputRow
456
+ text={text}
457
+ setText={setText}
458
+ onSend={handleSend}
459
+ isThinking={isThinking}
460
+ isArabic={isArabic}
118
461
  />
119
- <Pressable
120
- style={[styles.sendButton, isThinking && styles.sendButtonDisabled]}
121
- onPress={handleSend}
122
- disabled={isThinking || !text.trim()}
123
- accessibilityLabel="Send request to AI Agent"
124
- >
125
- <Text style={styles.sendButtonText}>
126
- {isThinking ? '⏳' : '🚀'}
127
- </Text>
128
- </Pressable>
129
- </View>
462
+ )}
463
+
464
+ {mode === 'voice' && (
465
+ <VoiceControlsRow
466
+ isMicActive={isMicActive}
467
+ isSpeakerMuted={isSpeakerMuted}
468
+ onMicToggle={onMicToggle || (() => {})}
469
+ onSpeakerToggle={onSpeakerToggle || (() => {})}
470
+ isAISpeaking={isAISpeaking}
471
+ isVoiceConnected={isVoiceConnected}
472
+ isArabic={isArabic}
473
+ onStopSession={onStopSession}
474
+ />
475
+ )}
476
+
477
+
130
478
  </Animated.View>
131
479
  );
132
480
  }
133
481
 
482
+ // ─── Styles ────────────────────────────────────────────────────
483
+
134
484
  const styles = StyleSheet.create({
135
- // FAB Styles
136
485
  fabContainer: {
137
486
  position: 'absolute',
138
487
  zIndex: 9999,
@@ -150,11 +499,10 @@ const styles = StyleSheet.create({
150
499
  shadowOpacity: 0.3,
151
500
  shadowRadius: 6,
152
501
  },
502
+
153
503
  fabIcon: {
154
504
  fontSize: 28,
155
505
  },
156
-
157
- // Expanded Styles
158
506
  expandedContainer: {
159
507
  position: 'absolute',
160
508
  zIndex: 9999,
@@ -193,8 +541,6 @@ const styles = StyleSheet.create({
193
541
  fontSize: 18,
194
542
  fontWeight: 'bold',
195
543
  },
196
-
197
- // Results & Input
198
544
  resultBubble: {
199
545
  borderRadius: 12,
200
546
  padding: 12,
@@ -227,6 +573,7 @@ const styles = StyleSheet.create({
227
573
  flexDirection: 'row',
228
574
  alignItems: 'center',
229
575
  gap: 8,
576
+ justifyContent: 'center',
230
577
  },
231
578
  input: {
232
579
  flex: 1,
@@ -255,4 +602,102 @@ const styles = StyleSheet.create({
255
602
  sendButtonText: {
256
603
  fontSize: 18,
257
604
  },
605
+ dictationButton: {
606
+ width: 40,
607
+ height: 40,
608
+ borderRadius: 20,
609
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
610
+ justifyContent: 'center' as const,
611
+ alignItems: 'center' as const,
612
+ },
613
+ dictationButtonActive: {
614
+ backgroundColor: 'rgba(255, 59, 48, 0.3)',
615
+ },
616
+ });
617
+
618
+ const modeStyles = StyleSheet.create({
619
+ container: {
620
+ flexDirection: 'row',
621
+ marginBottom: 12,
622
+ borderRadius: 12,
623
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
624
+ padding: 3,
625
+ },
626
+ tab: {
627
+ flex: 1,
628
+ flexDirection: 'row',
629
+ alignItems: 'center',
630
+ justifyContent: 'center',
631
+ paddingVertical: 8,
632
+ borderRadius: 10,
633
+ gap: 4,
634
+ },
635
+ tabActive: {
636
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
637
+ },
638
+ tabIcon: {
639
+ fontSize: 14,
640
+ },
641
+ tabLabel: {
642
+ color: 'rgba(255, 255, 255, 0.5)',
643
+ fontSize: 12,
644
+ fontWeight: '600',
645
+ },
646
+ tabLabelActive: {
647
+ color: '#fff',
648
+ },
649
+ });
650
+
651
+ const audioStyles = StyleSheet.create({
652
+ controlBtn: {
653
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
654
+ justifyContent: 'center',
655
+ alignItems: 'center',
656
+ },
657
+ controlBtnActive: {
658
+ backgroundColor: 'rgba(255, 100, 100, 0.2)',
659
+ },
660
+ controlIcon: {
661
+ fontSize: 16,
662
+ },
663
+ micButton: {
664
+ flex: 1,
665
+ flexDirection: 'row',
666
+ alignItems: 'center',
667
+ justifyContent: 'center',
668
+ paddingVertical: 12,
669
+ borderRadius: 24,
670
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
671
+ gap: 8,
672
+ },
673
+ micButtonActive: {
674
+ backgroundColor: 'rgba(255, 59, 48, 0.3)',
675
+ },
676
+ micButtonSpeaking: {
677
+ backgroundColor: 'rgba(52, 199, 89, 0.3)',
678
+ },
679
+ micIcon: {
680
+ fontSize: 20,
681
+ },
682
+ micLabel: {
683
+ color: '#fff',
684
+ fontSize: 14,
685
+ fontWeight: '600',
686
+ },
687
+
688
+ micButtonConnecting: {
689
+ backgroundColor: 'rgba(255, 200, 50, 0.2)',
690
+ opacity: 0.7,
691
+ },
692
+ statusDot: {
693
+ width: 10,
694
+ height: 10,
695
+ borderRadius: 5,
696
+ },
697
+ statusDotConnected: {
698
+ backgroundColor: '#34C759',
699
+ },
700
+ statusDotConnecting: {
701
+ backgroundColor: '#FFCC00',
702
+ },
258
703
  });