yaver-feedback-react-native 0.2.0
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 +690 -0
- package/app.plugin.js +70 -0
- package/package.json +39 -0
- package/src/BlackBox.ts +317 -0
- package/src/ConnectionScreen.tsx +400 -0
- package/src/Discovery.ts +223 -0
- package/src/FeedbackModal.tsx +678 -0
- package/src/FixReport.tsx +313 -0
- package/src/FloatingButton.tsx +860 -0
- package/src/P2PClient.ts +303 -0
- package/src/ShakeDetector.ts +57 -0
- package/src/YaverFeedback.ts +345 -0
- package/src/__tests__/Discovery.test.ts +187 -0
- package/src/__tests__/P2PClient.test.ts +218 -0
- package/src/__tests__/SDKToken.test.ts +268 -0
- package/src/__tests__/YaverFeedback.test.ts +189 -0
- package/src/__tests__/types.test.ts +247 -0
- package/src/capture.ts +84 -0
- package/src/expo.ts +62 -0
- package/src/index.ts +54 -0
- package/src/types.ts +251 -0
- package/src/upload.ts +74 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
DeviceEventEmitter,
|
|
5
|
+
FlatList,
|
|
6
|
+
Modal,
|
|
7
|
+
Platform,
|
|
8
|
+
ScrollView,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
Text,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
View,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { YaverFeedback } from './YaverFeedback';
|
|
15
|
+
import { BlackBox } from './BlackBox';
|
|
16
|
+
import { captureScreenshot, startAudioRecording, stopAudioRecording } from './capture';
|
|
17
|
+
import { uploadFeedback } from './upload';
|
|
18
|
+
import { TimelineEvent, DeviceInfo, FeedbackBundle, AgentCommentary } from './types';
|
|
19
|
+
|
|
20
|
+
type FeedbackMode = 'live' | 'narrated' | 'batch';
|
|
21
|
+
|
|
22
|
+
const MODE_LABELS: Record<FeedbackMode, string> = {
|
|
23
|
+
live: 'Live',
|
|
24
|
+
narrated: 'Narrated',
|
|
25
|
+
batch: 'Batch',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Full-screen modal for composing and sending a feedback report.
|
|
30
|
+
* Renders when triggered by shake, floating button, or manual call.
|
|
31
|
+
*
|
|
32
|
+
* Supports three feedback modes:
|
|
33
|
+
* - Live: stream events to the agent as they happen
|
|
34
|
+
* - Narrated: record everything, send on stop
|
|
35
|
+
* - Batch: dump everything at end (default)
|
|
36
|
+
*/
|
|
37
|
+
export const FeedbackModal: React.FC = () => {
|
|
38
|
+
const [visible, setVisible] = useState(false);
|
|
39
|
+
const [timeline, setTimeline] = useState<TimelineEvent[]>([]);
|
|
40
|
+
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
41
|
+
const [isSending, setIsSending] = useState(false);
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const [sent, setSent] = useState(false);
|
|
44
|
+
const [mode, setMode] = useState<FeedbackMode>('batch');
|
|
45
|
+
const [commentary, setCommentary] = useState<AgentCommentary[]>([]);
|
|
46
|
+
const [isVoiceCommand, setIsVoiceCommand] = useState(false);
|
|
47
|
+
const [isReloading, setIsReloading] = useState(false);
|
|
48
|
+
const mountedRef = useRef(true);
|
|
49
|
+
const commentaryListRef = useRef<FlatList>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
mountedRef.current = true;
|
|
53
|
+
const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
54
|
+
if (YaverFeedback.isEnabled()) {
|
|
55
|
+
setVisible(true);
|
|
56
|
+
setTimeline([]);
|
|
57
|
+
setError(null);
|
|
58
|
+
setSent(false);
|
|
59
|
+
setCommentary([]);
|
|
60
|
+
setMode(YaverFeedback.getFeedbackMode());
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Listen for agent commentary events
|
|
65
|
+
const commentarySub = DeviceEventEmitter.addListener(
|
|
66
|
+
'yaverFeedback:commentary',
|
|
67
|
+
(event: AgentCommentary) => {
|
|
68
|
+
if (mountedRef.current) {
|
|
69
|
+
setCommentary((prev) => [...prev, event]);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
mountedRef.current = false;
|
|
76
|
+
sub.remove();
|
|
77
|
+
commentarySub.remove();
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const handleScreenshot = useCallback(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const path = await captureScreenshot();
|
|
84
|
+
if (mountedRef.current) {
|
|
85
|
+
const event: TimelineEvent = {
|
|
86
|
+
type: 'screenshot',
|
|
87
|
+
path,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
setTimeline((prev) => [...prev, event]);
|
|
91
|
+
|
|
92
|
+
// In live mode, stream the event immediately
|
|
93
|
+
if (mode === 'live') {
|
|
94
|
+
const client = YaverFeedback.getP2PClient();
|
|
95
|
+
if (client) {
|
|
96
|
+
try {
|
|
97
|
+
await client.streamFeedback(
|
|
98
|
+
(async function* () {
|
|
99
|
+
yield {
|
|
100
|
+
type: 'screenshot',
|
|
101
|
+
timestamp: event.timestamp,
|
|
102
|
+
data: { path },
|
|
103
|
+
};
|
|
104
|
+
})(),
|
|
105
|
+
);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('[YaverFeedback] Live stream failed:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (mountedRef.current) {
|
|
114
|
+
setError(String(err));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}, [mode]);
|
|
118
|
+
|
|
119
|
+
const handleToggleAudio = useCallback(async () => {
|
|
120
|
+
if (isRecordingAudio) {
|
|
121
|
+
try {
|
|
122
|
+
const result = await stopAudioRecording();
|
|
123
|
+
if (mountedRef.current) {
|
|
124
|
+
setIsRecordingAudio(false);
|
|
125
|
+
const event: TimelineEvent = {
|
|
126
|
+
type: 'audio',
|
|
127
|
+
path: result.path,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
duration: result.duration,
|
|
130
|
+
};
|
|
131
|
+
setTimeline((prev) => [...prev, event]);
|
|
132
|
+
|
|
133
|
+
// In live mode, stream the audio event
|
|
134
|
+
if (mode === 'live') {
|
|
135
|
+
const client = YaverFeedback.getP2PClient();
|
|
136
|
+
if (client) {
|
|
137
|
+
try {
|
|
138
|
+
await client.streamFeedback(
|
|
139
|
+
(async function* () {
|
|
140
|
+
yield {
|
|
141
|
+
type: 'audio',
|
|
142
|
+
timestamp: event.timestamp,
|
|
143
|
+
data: { path: result.path, duration: result.duration },
|
|
144
|
+
};
|
|
145
|
+
})(),
|
|
146
|
+
);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.warn('[YaverFeedback] Live stream failed:', err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (mountedRef.current) {
|
|
155
|
+
setIsRecordingAudio(false);
|
|
156
|
+
setError(String(err));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
try {
|
|
161
|
+
await startAudioRecording();
|
|
162
|
+
if (mountedRef.current) {
|
|
163
|
+
setIsRecordingAudio(true);
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (mountedRef.current) {
|
|
167
|
+
setError(String(err));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}, [isRecordingAudio, mode]);
|
|
172
|
+
|
|
173
|
+
const handleVoiceCommand = useCallback(async () => {
|
|
174
|
+
if (isVoiceCommand) {
|
|
175
|
+
// Stop voice command and send as a task
|
|
176
|
+
try {
|
|
177
|
+
const result = await stopAudioRecording();
|
|
178
|
+
if (mountedRef.current) {
|
|
179
|
+
setIsVoiceCommand(false);
|
|
180
|
+
|
|
181
|
+
const client = YaverFeedback.getP2PClient();
|
|
182
|
+
if (client) {
|
|
183
|
+
try {
|
|
184
|
+
await client.streamFeedback(
|
|
185
|
+
(async function* () {
|
|
186
|
+
yield {
|
|
187
|
+
type: 'voice_command',
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
data: { path: result.path, duration: result.duration },
|
|
190
|
+
};
|
|
191
|
+
})(),
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.warn('[YaverFeedback] Voice command send failed:', err);
|
|
195
|
+
if (mountedRef.current) {
|
|
196
|
+
setError('Failed to send voice command.');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (mountedRef.current) {
|
|
203
|
+
setIsVoiceCommand(false);
|
|
204
|
+
setError(String(err));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
try {
|
|
209
|
+
await startAudioRecording();
|
|
210
|
+
if (mountedRef.current) {
|
|
211
|
+
setIsVoiceCommand(true);
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (mountedRef.current) {
|
|
215
|
+
setError(String(err));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}, [isVoiceCommand]);
|
|
220
|
+
|
|
221
|
+
const handleReload = useCallback(async () => {
|
|
222
|
+
const config = YaverFeedback.getConfig();
|
|
223
|
+
if (!config?.agentUrl) return;
|
|
224
|
+
|
|
225
|
+
setIsReloading(true);
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(`${config.agentUrl.replace(/\/$/, '')}/exec`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: {
|
|
230
|
+
Authorization: `Bearer ${config.authToken}`,
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify({ command: 'reload', type: 'hot-reload' }),
|
|
234
|
+
});
|
|
235
|
+
if (response.ok) {
|
|
236
|
+
BlackBox.lifecycle('Hot reload triggered from feedback SDK');
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (mountedRef.current) {
|
|
240
|
+
setError('Reload failed: ' + String(err));
|
|
241
|
+
}
|
|
242
|
+
} finally {
|
|
243
|
+
if (mountedRef.current) {
|
|
244
|
+
setIsReloading(false);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
const handleSend = useCallback(async () => {
|
|
250
|
+
const config = YaverFeedback.getConfig();
|
|
251
|
+
if (!config || !config.agentUrl) return;
|
|
252
|
+
|
|
253
|
+
setIsSending(true);
|
|
254
|
+
setError(null);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const { Dimensions } = require('react-native');
|
|
258
|
+
const { width, height } = Dimensions.get('window');
|
|
259
|
+
|
|
260
|
+
const deviceInfo: DeviceInfo = {
|
|
261
|
+
platform: Platform.OS,
|
|
262
|
+
osVersion: String(Platform.Version),
|
|
263
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
264
|
+
screenWidth: width,
|
|
265
|
+
screenHeight: height,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const screenshots = timeline
|
|
269
|
+
.filter((e) => e.type === 'screenshot')
|
|
270
|
+
.map((e) => e.path);
|
|
271
|
+
|
|
272
|
+
const audioEvent = timeline.find((e) => e.type === 'audio');
|
|
273
|
+
|
|
274
|
+
// Include captured errors from the error buffer
|
|
275
|
+
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
276
|
+
|
|
277
|
+
const bundle: FeedbackBundle = {
|
|
278
|
+
metadata: {
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
device: deviceInfo,
|
|
281
|
+
app: {},
|
|
282
|
+
},
|
|
283
|
+
screenshots,
|
|
284
|
+
audio: audioEvent?.path,
|
|
285
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
await uploadFeedback(config.agentUrl, config.authToken, bundle);
|
|
289
|
+
|
|
290
|
+
if (mountedRef.current) {
|
|
291
|
+
setSent(true);
|
|
292
|
+
// Auto-close after a short delay
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
if (mountedRef.current) {
|
|
295
|
+
setVisible(false);
|
|
296
|
+
}
|
|
297
|
+
}, 1500);
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (mountedRef.current) {
|
|
301
|
+
setError(String(err));
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
if (mountedRef.current) {
|
|
305
|
+
setIsSending(false);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, [timeline]);
|
|
309
|
+
|
|
310
|
+
const handleCancel = useCallback(() => {
|
|
311
|
+
setVisible(false);
|
|
312
|
+
setTimeline([]);
|
|
313
|
+
setError(null);
|
|
314
|
+
setSent(false);
|
|
315
|
+
setIsRecordingAudio(false);
|
|
316
|
+
setIsVoiceCommand(false);
|
|
317
|
+
setCommentary([]);
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
const renderCommentaryItem = useCallback(
|
|
321
|
+
({ item }: { item: AgentCommentary }) => (
|
|
322
|
+
<View style={styles.commentaryBubble}>
|
|
323
|
+
<Text style={styles.commentaryType}>{item.type}</Text>
|
|
324
|
+
<Text style={styles.commentaryMessage}>{item.message}</Text>
|
|
325
|
+
</View>
|
|
326
|
+
),
|
|
327
|
+
[],
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (!visible) return null;
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<Modal
|
|
334
|
+
visible={visible}
|
|
335
|
+
animationType="slide"
|
|
336
|
+
transparent
|
|
337
|
+
onRequestClose={handleCancel}
|
|
338
|
+
>
|
|
339
|
+
<View style={styles.overlay}>
|
|
340
|
+
<View style={styles.modal}>
|
|
341
|
+
<Text style={styles.title}>Send Feedback</Text>
|
|
342
|
+
|
|
343
|
+
{/* Mode selector */}
|
|
344
|
+
<View style={styles.modeSelector}>
|
|
345
|
+
{(['live', 'narrated', 'batch'] as FeedbackMode[]).map((m) => (
|
|
346
|
+
<TouchableOpacity
|
|
347
|
+
key={m}
|
|
348
|
+
style={[styles.modeButton, mode === m && styles.modeButtonActive]}
|
|
349
|
+
onPress={() => setMode(m)}
|
|
350
|
+
>
|
|
351
|
+
<Text
|
|
352
|
+
style={[styles.modeButtonText, mode === m && styles.modeButtonTextActive]}
|
|
353
|
+
>
|
|
354
|
+
{MODE_LABELS[m]}
|
|
355
|
+
</Text>
|
|
356
|
+
</TouchableOpacity>
|
|
357
|
+
))}
|
|
358
|
+
</View>
|
|
359
|
+
|
|
360
|
+
{/* Agent commentary (chat-like view) */}
|
|
361
|
+
{commentary.length > 0 && (
|
|
362
|
+
<FlatList
|
|
363
|
+
ref={commentaryListRef}
|
|
364
|
+
data={commentary}
|
|
365
|
+
renderItem={renderCommentaryItem}
|
|
366
|
+
keyExtractor={(item) => item.id}
|
|
367
|
+
style={styles.commentaryList}
|
|
368
|
+
onContentSizeChange={() =>
|
|
369
|
+
commentaryListRef.current?.scrollToEnd({ animated: true })
|
|
370
|
+
}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{/* Timeline of captured items */}
|
|
375
|
+
{timeline.length > 0 && (
|
|
376
|
+
<ScrollView style={styles.timeline} horizontal>
|
|
377
|
+
{timeline.map((event, idx) => (
|
|
378
|
+
<View key={idx} style={styles.timelineItem}>
|
|
379
|
+
<Text style={styles.timelineIcon}>
|
|
380
|
+
{event.type === 'screenshot'
|
|
381
|
+
? '[img]'
|
|
382
|
+
: event.type === 'audio'
|
|
383
|
+
? '[mic]'
|
|
384
|
+
: '[vid]'}
|
|
385
|
+
</Text>
|
|
386
|
+
<Text style={styles.timelineLabel}>{event.type}</Text>
|
|
387
|
+
{event.duration != null && (
|
|
388
|
+
<Text style={styles.timelineDuration}>
|
|
389
|
+
{Math.round(event.duration)}s
|
|
390
|
+
</Text>
|
|
391
|
+
)}
|
|
392
|
+
</View>
|
|
393
|
+
))}
|
|
394
|
+
</ScrollView>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{/* Action buttons */}
|
|
398
|
+
<View style={styles.actions}>
|
|
399
|
+
<TouchableOpacity style={styles.actionButton} onPress={handleScreenshot}>
|
|
400
|
+
<Text style={styles.actionText}>Take Screenshot</Text>
|
|
401
|
+
</TouchableOpacity>
|
|
402
|
+
|
|
403
|
+
<TouchableOpacity
|
|
404
|
+
style={[styles.actionButton, isRecordingAudio && styles.actionButtonActive]}
|
|
405
|
+
onPress={handleToggleAudio}
|
|
406
|
+
disabled={isVoiceCommand}
|
|
407
|
+
>
|
|
408
|
+
<Text style={styles.actionText}>
|
|
409
|
+
{isRecordingAudio ? 'Stop Recording' : 'Voice Note'}
|
|
410
|
+
</Text>
|
|
411
|
+
</TouchableOpacity>
|
|
412
|
+
</View>
|
|
413
|
+
|
|
414
|
+
{/* Hot Reload + Streaming status */}
|
|
415
|
+
<View style={styles.actions}>
|
|
416
|
+
<TouchableOpacity
|
|
417
|
+
style={[styles.actionButton, styles.reloadButton]}
|
|
418
|
+
onPress={handleReload}
|
|
419
|
+
disabled={isReloading}
|
|
420
|
+
>
|
|
421
|
+
<Text style={styles.actionText}>
|
|
422
|
+
{isReloading ? 'Reloading...' : 'Hot Reload'}
|
|
423
|
+
</Text>
|
|
424
|
+
</TouchableOpacity>
|
|
425
|
+
|
|
426
|
+
<View style={[styles.actionButton, styles.streamingIndicator]}>
|
|
427
|
+
<View style={[styles.streamingDot, BlackBox.isStreaming && styles.streamingDotActive]} />
|
|
428
|
+
<Text style={styles.streamingText}>
|
|
429
|
+
{BlackBox.isStreaming ? 'Streaming' : 'Not streaming'}
|
|
430
|
+
</Text>
|
|
431
|
+
</View>
|
|
432
|
+
</View>
|
|
433
|
+
|
|
434
|
+
{/* Voice command button */}
|
|
435
|
+
{mode === 'live' && (
|
|
436
|
+
<TouchableOpacity
|
|
437
|
+
style={[styles.voiceCommandButton, isVoiceCommand && styles.voiceCommandActive]}
|
|
438
|
+
onPress={handleVoiceCommand}
|
|
439
|
+
disabled={isRecordingAudio}
|
|
440
|
+
>
|
|
441
|
+
<Text style={styles.voiceCommandText}>
|
|
442
|
+
{isVoiceCommand ? 'Stop & Send Command' : 'Speak to Fix'}
|
|
443
|
+
</Text>
|
|
444
|
+
</TouchableOpacity>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* Error display */}
|
|
448
|
+
{error && <Text style={styles.error}>{error}</Text>}
|
|
449
|
+
|
|
450
|
+
{/* Send / Cancel */}
|
|
451
|
+
<View style={styles.footer}>
|
|
452
|
+
<TouchableOpacity style={styles.cancelButton} onPress={handleCancel}>
|
|
453
|
+
<Text style={styles.cancelText}>Cancel</Text>
|
|
454
|
+
</TouchableOpacity>
|
|
455
|
+
|
|
456
|
+
{sent ? (
|
|
457
|
+
<View style={styles.sendButton}>
|
|
458
|
+
<Text style={styles.sendText}>Sent!</Text>
|
|
459
|
+
</View>
|
|
460
|
+
) : (
|
|
461
|
+
<TouchableOpacity
|
|
462
|
+
style={[styles.sendButton, isSending && styles.sendButtonDisabled]}
|
|
463
|
+
onPress={handleSend}
|
|
464
|
+
disabled={isSending || timeline.length === 0}
|
|
465
|
+
>
|
|
466
|
+
{isSending ? (
|
|
467
|
+
<ActivityIndicator color="#fff" size="small" />
|
|
468
|
+
) : (
|
|
469
|
+
<Text style={styles.sendText}>Send Report</Text>
|
|
470
|
+
)}
|
|
471
|
+
</TouchableOpacity>
|
|
472
|
+
)}
|
|
473
|
+
</View>
|
|
474
|
+
</View>
|
|
475
|
+
</View>
|
|
476
|
+
</Modal>
|
|
477
|
+
);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const styles = StyleSheet.create({
|
|
481
|
+
overlay: {
|
|
482
|
+
flex: 1,
|
|
483
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
484
|
+
justifyContent: 'flex-end',
|
|
485
|
+
},
|
|
486
|
+
modal: {
|
|
487
|
+
backgroundColor: '#1a1a2e',
|
|
488
|
+
borderTopLeftRadius: 20,
|
|
489
|
+
borderTopRightRadius: 20,
|
|
490
|
+
padding: 24,
|
|
491
|
+
paddingBottom: 40,
|
|
492
|
+
maxHeight: '90%',
|
|
493
|
+
},
|
|
494
|
+
title: {
|
|
495
|
+
fontSize: 20,
|
|
496
|
+
fontWeight: '700',
|
|
497
|
+
color: '#fff',
|
|
498
|
+
marginBottom: 12,
|
|
499
|
+
},
|
|
500
|
+
modeSelector: {
|
|
501
|
+
flexDirection: 'row',
|
|
502
|
+
gap: 8,
|
|
503
|
+
marginBottom: 16,
|
|
504
|
+
},
|
|
505
|
+
modeButton: {
|
|
506
|
+
flex: 1,
|
|
507
|
+
paddingVertical: 8,
|
|
508
|
+
borderRadius: 8,
|
|
509
|
+
alignItems: 'center',
|
|
510
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
511
|
+
borderWidth: 1,
|
|
512
|
+
borderColor: 'rgba(255,255,255,0.1)',
|
|
513
|
+
},
|
|
514
|
+
modeButtonActive: {
|
|
515
|
+
backgroundColor: 'rgba(99,102,241,0.3)',
|
|
516
|
+
borderColor: '#6366f1',
|
|
517
|
+
},
|
|
518
|
+
modeButtonText: {
|
|
519
|
+
color: '#999',
|
|
520
|
+
fontSize: 13,
|
|
521
|
+
fontWeight: '600',
|
|
522
|
+
},
|
|
523
|
+
modeButtonTextActive: {
|
|
524
|
+
color: '#c7c8ff',
|
|
525
|
+
},
|
|
526
|
+
commentaryList: {
|
|
527
|
+
maxHeight: 140,
|
|
528
|
+
marginBottom: 12,
|
|
529
|
+
},
|
|
530
|
+
commentaryBubble: {
|
|
531
|
+
backgroundColor: 'rgba(99,102,241,0.15)',
|
|
532
|
+
borderRadius: 10,
|
|
533
|
+
padding: 10,
|
|
534
|
+
marginBottom: 6,
|
|
535
|
+
borderLeftWidth: 3,
|
|
536
|
+
borderLeftColor: '#6366f1',
|
|
537
|
+
},
|
|
538
|
+
commentaryType: {
|
|
539
|
+
color: '#8b8bf5',
|
|
540
|
+
fontSize: 10,
|
|
541
|
+
fontWeight: '700',
|
|
542
|
+
textTransform: 'uppercase',
|
|
543
|
+
marginBottom: 2,
|
|
544
|
+
},
|
|
545
|
+
commentaryMessage: {
|
|
546
|
+
color: '#d0d0e0',
|
|
547
|
+
fontSize: 13,
|
|
548
|
+
lineHeight: 18,
|
|
549
|
+
},
|
|
550
|
+
timeline: {
|
|
551
|
+
maxHeight: 80,
|
|
552
|
+
marginBottom: 16,
|
|
553
|
+
},
|
|
554
|
+
timelineItem: {
|
|
555
|
+
alignItems: 'center',
|
|
556
|
+
marginRight: 16,
|
|
557
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
558
|
+
borderRadius: 12,
|
|
559
|
+
padding: 10,
|
|
560
|
+
minWidth: 70,
|
|
561
|
+
},
|
|
562
|
+
timelineIcon: {
|
|
563
|
+
fontSize: 14,
|
|
564
|
+
color: '#ccc',
|
|
565
|
+
fontWeight: '600',
|
|
566
|
+
},
|
|
567
|
+
timelineLabel: {
|
|
568
|
+
color: '#ccc',
|
|
569
|
+
fontSize: 11,
|
|
570
|
+
marginTop: 4,
|
|
571
|
+
},
|
|
572
|
+
timelineDuration: {
|
|
573
|
+
color: '#999',
|
|
574
|
+
fontSize: 10,
|
|
575
|
+
},
|
|
576
|
+
actions: {
|
|
577
|
+
flexDirection: 'row',
|
|
578
|
+
gap: 12,
|
|
579
|
+
marginBottom: 12,
|
|
580
|
+
},
|
|
581
|
+
actionButton: {
|
|
582
|
+
flex: 1,
|
|
583
|
+
backgroundColor: 'rgba(99,102,241,0.2)',
|
|
584
|
+
borderWidth: 1,
|
|
585
|
+
borderColor: 'rgba(99,102,241,0.4)',
|
|
586
|
+
borderRadius: 12,
|
|
587
|
+
paddingVertical: 14,
|
|
588
|
+
alignItems: 'center',
|
|
589
|
+
},
|
|
590
|
+
actionButtonActive: {
|
|
591
|
+
backgroundColor: 'rgba(239,68,68,0.3)',
|
|
592
|
+
borderColor: 'rgba(239,68,68,0.6)',
|
|
593
|
+
},
|
|
594
|
+
actionText: {
|
|
595
|
+
color: '#fff',
|
|
596
|
+
fontSize: 14,
|
|
597
|
+
fontWeight: '600',
|
|
598
|
+
},
|
|
599
|
+
voiceCommandButton: {
|
|
600
|
+
backgroundColor: 'rgba(34,197,94,0.2)',
|
|
601
|
+
borderWidth: 1,
|
|
602
|
+
borderColor: 'rgba(34,197,94,0.4)',
|
|
603
|
+
borderRadius: 12,
|
|
604
|
+
paddingVertical: 14,
|
|
605
|
+
alignItems: 'center',
|
|
606
|
+
marginBottom: 12,
|
|
607
|
+
},
|
|
608
|
+
voiceCommandActive: {
|
|
609
|
+
backgroundColor: 'rgba(34,197,94,0.4)',
|
|
610
|
+
borderColor: '#22c55e',
|
|
611
|
+
},
|
|
612
|
+
voiceCommandText: {
|
|
613
|
+
color: '#22c55e',
|
|
614
|
+
fontSize: 14,
|
|
615
|
+
fontWeight: '600',
|
|
616
|
+
},
|
|
617
|
+
error: {
|
|
618
|
+
color: '#ef4444',
|
|
619
|
+
fontSize: 12,
|
|
620
|
+
marginBottom: 12,
|
|
621
|
+
},
|
|
622
|
+
footer: {
|
|
623
|
+
flexDirection: 'row',
|
|
624
|
+
gap: 12,
|
|
625
|
+
},
|
|
626
|
+
cancelButton: {
|
|
627
|
+
flex: 1,
|
|
628
|
+
paddingVertical: 14,
|
|
629
|
+
alignItems: 'center',
|
|
630
|
+
borderRadius: 12,
|
|
631
|
+
borderWidth: 1,
|
|
632
|
+
borderColor: 'rgba(255,255,255,0.2)',
|
|
633
|
+
},
|
|
634
|
+
cancelText: {
|
|
635
|
+
color: '#999',
|
|
636
|
+
fontSize: 16,
|
|
637
|
+
fontWeight: '600',
|
|
638
|
+
},
|
|
639
|
+
sendButton: {
|
|
640
|
+
flex: 2,
|
|
641
|
+
backgroundColor: '#6366f1',
|
|
642
|
+
paddingVertical: 14,
|
|
643
|
+
alignItems: 'center',
|
|
644
|
+
borderRadius: 12,
|
|
645
|
+
},
|
|
646
|
+
sendButtonDisabled: {
|
|
647
|
+
opacity: 0.5,
|
|
648
|
+
},
|
|
649
|
+
sendText: {
|
|
650
|
+
color: '#fff',
|
|
651
|
+
fontSize: 16,
|
|
652
|
+
fontWeight: '700',
|
|
653
|
+
},
|
|
654
|
+
reloadButton: {
|
|
655
|
+
backgroundColor: 'rgba(251,191,36,0.2)',
|
|
656
|
+
borderColor: 'rgba(251,191,36,0.4)',
|
|
657
|
+
},
|
|
658
|
+
streamingIndicator: {
|
|
659
|
+
flexDirection: 'row',
|
|
660
|
+
justifyContent: 'center',
|
|
661
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
662
|
+
borderColor: 'rgba(255,255,255,0.1)',
|
|
663
|
+
},
|
|
664
|
+
streamingDot: {
|
|
665
|
+
width: 8,
|
|
666
|
+
height: 8,
|
|
667
|
+
borderRadius: 4,
|
|
668
|
+
backgroundColor: '#555',
|
|
669
|
+
marginRight: 6,
|
|
670
|
+
},
|
|
671
|
+
streamingDotActive: {
|
|
672
|
+
backgroundColor: '#22c55e',
|
|
673
|
+
},
|
|
674
|
+
streamingText: {
|
|
675
|
+
color: '#999',
|
|
676
|
+
fontSize: 12,
|
|
677
|
+
},
|
|
678
|
+
});
|