@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.
- package/README.md +21 -2
- package/lib/module/components/AIAgent.js +216 -5
- package/lib/module/components/AIAgent.js.map +1 -1
- package/lib/module/components/AgentChatBar.js +358 -36
- package/lib/module/components/AgentChatBar.js.map +1 -1
- package/lib/module/core/AgentRuntime.js +122 -6
- package/lib/module/core/AgentRuntime.js.map +1 -1
- package/lib/module/core/systemPrompt.js +57 -0
- package/lib/module/core/systemPrompt.js.map +1 -1
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/GeminiProvider.js +108 -85
- package/lib/module/providers/GeminiProvider.js.map +1 -1
- package/lib/module/services/AudioInputService.js +128 -0
- package/lib/module/services/AudioInputService.js.map +1 -0
- package/lib/module/services/AudioOutputService.js +154 -0
- package/lib/module/services/AudioOutputService.js.map +1 -0
- package/lib/module/services/VoiceService.js +362 -0
- package/lib/module/services/VoiceService.js.map +1 -0
- package/lib/module/utils/audioUtils.js +49 -0
- package/lib/module/utils/audioUtils.js.map +1 -0
- package/lib/module/utils/logger.js +21 -4
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/babel.config.d.ts +10 -0
- package/lib/typescript/babel.config.d.ts.map +1 -0
- package/lib/typescript/eslint.config.d.mts +3 -0
- package/lib/typescript/eslint.config.d.mts.map +1 -0
- package/lib/typescript/fetch-models.d.mts +2 -0
- package/lib/typescript/fetch-models.d.mts.map +1 -0
- package/lib/typescript/list-all-models.d.mts +2 -0
- package/lib/typescript/list-all-models.d.mts.map +1 -0
- package/lib/typescript/list-models.d.mts +2 -0
- package/lib/typescript/list-models.d.mts.map +1 -0
- package/lib/typescript/src/components/AIAgent.d.ts +8 -2
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts +19 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
- package/lib/typescript/src/core/systemPrompt.d.ts +8 -0
- package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +24 -1
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/providers/GeminiProvider.d.ts +22 -18
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -1
- package/lib/typescript/src/services/AudioInputService.d.ts +31 -0
- package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -0
- package/lib/typescript/src/services/AudioOutputService.d.ts +34 -0
- package/lib/typescript/src/services/AudioOutputService.d.ts.map +1 -0
- package/lib/typescript/src/services/VoiceService.d.ts +73 -0
- package/lib/typescript/src/services/VoiceService.d.ts.map +1 -0
- package/lib/typescript/src/utils/audioUtils.d.ts +17 -0
- package/lib/typescript/src/utils/audioUtils.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +4 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -1
- package/package.json +24 -8
- package/src/components/AIAgent.tsx +222 -3
- package/src/components/AgentChatBar.tsx +487 -42
- package/src/core/AgentRuntime.ts +131 -2
- package/src/core/systemPrompt.ts +62 -0
- package/src/core/types.ts +30 -0
- package/src/index.ts +16 -0
- package/src/providers/GeminiProvider.ts +105 -89
- package/src/services/AudioInputService.ts +141 -0
- package/src/services/AudioOutputService.ts +167 -0
- package/src/services/VoiceService.ts +409 -0
- package/src/utils/audioUtils.ts +54 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
// ───
|
|
402
|
+
// ─── FAB (Compressed) ──────────────────────────────────────
|
|
403
|
+
|
|
69
404
|
if (!isExpanded) {
|
|
405
|
+
const fabIcon = isThinking ? '⏳' : '🤖';
|
|
70
406
|
return (
|
|
71
|
-
<Animated.View
|
|
72
|
-
|
|
73
|
-
|
|
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}>{
|
|
416
|
+
<Text style={styles.fabIcon}>{fabIcon}</Text>
|
|
78
417
|
</Pressable>
|
|
79
418
|
</Animated.View>
|
|
80
419
|
);
|
|
81
420
|
}
|
|
82
421
|
|
|
83
|
-
// ─── Expanded
|
|
422
|
+
// ─── Expanded Widget ───────────────────────────────────────
|
|
423
|
+
|
|
84
424
|
return (
|
|
85
425
|
<Animated.View style={[styles.expandedContainer, pan.getLayout()]}>
|
|
86
|
-
{/* Drag Handle
|
|
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
|
-
{/*
|
|
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
|
-
{/*
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
});
|