yaver-feedback-react-native 0.6.1 → 0.7.1
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/dist/Discovery.d.ts +40 -27
- package/dist/Discovery.js +164 -70
- package/dist/FeedbackModal.d.ts +0 -9
- package/dist/FeedbackModal.js +292 -473
- package/dist/MachinePickerScreen.js +5 -1
- package/dist/P2PClient.d.ts +24 -0
- package/dist/P2PClient.js +65 -10
- package/dist/YaverFeedback.d.ts +10 -4
- package/dist/YaverFeedback.js +30 -10
- package/dist/__tests__/YaverFeedback.test.js +2 -35
- package/dist/__tests__/types.test.js +3 -35
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +36 -3
- package/dist/capture.d.ts +25 -12
- package/dist/capture.js +73 -34
- package/dist/deviceDedup.d.ts +44 -0
- package/dist/deviceDedup.js +184 -0
- package/dist/expo.d.ts +0 -1
- package/dist/expo.js +0 -2
- package/dist/index.d.ts +15 -10
- package/dist/index.js +17 -11
- package/dist/types.d.ts +2 -32
- package/dist/upload.d.ts +8 -3
- package/dist/upload.js +5 -12
- package/package.json +1 -1
- package/src/Discovery.ts +188 -101
- package/src/FeedbackModal.tsx +409 -564
- package/src/MachinePickerScreen.tsx +6 -1
- package/src/P2PClient.ts +66 -12
- package/src/YaverFeedback.ts +27 -12
- package/src/__tests__/YaverFeedback.test.ts +2 -41
- package/src/__tests__/types.test.ts +3 -39
- package/src/auth.ts +46 -3
- package/src/capture.ts +73 -37
- package/src/deviceDedup.ts +184 -0
- package/src/expo.ts +0 -2
- package/src/index.ts +19 -10
- package/src/types.ts +2 -33
- package/src/upload.ts +6 -15
package/src/FeedbackModal.tsx
CHANGED
|
@@ -2,262 +2,304 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
4
|
DeviceEventEmitter,
|
|
5
|
-
FlatList,
|
|
6
5
|
Modal,
|
|
7
6
|
Platform,
|
|
8
|
-
|
|
7
|
+
Pressable,
|
|
9
8
|
StyleSheet,
|
|
10
9
|
Text,
|
|
11
|
-
TouchableOpacity,
|
|
12
10
|
View,
|
|
13
11
|
} from 'react-native';
|
|
14
12
|
import { YaverFeedback } from './YaverFeedback';
|
|
15
|
-
import {
|
|
16
|
-
|
|
13
|
+
import {
|
|
14
|
+
captureScreenshot,
|
|
15
|
+
startVideoRecording,
|
|
16
|
+
stopVideoRecording,
|
|
17
|
+
} from './capture';
|
|
17
18
|
import { uploadFeedback } from './upload';
|
|
18
|
-
import {
|
|
19
|
+
import { DeviceInfo, FeedbackBundle } from './types';
|
|
19
20
|
import { AuthOverlay } from './AuthOverlay';
|
|
20
21
|
|
|
21
|
-
type FeedbackMode = 'live' | 'narrated' | 'batch';
|
|
22
|
-
|
|
23
|
-
const MODE_LABELS: Record<FeedbackMode, string> = {
|
|
24
|
-
live: 'Live',
|
|
25
|
-
narrated: 'Narrated',
|
|
26
|
-
batch: 'Batch',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
22
|
/**
|
|
30
|
-
*
|
|
31
|
-
* Renders when triggered by shake, floating button, or manual call.
|
|
23
|
+
* Simplified feedback modal — 5 actions:
|
|
32
24
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
25
|
+
* 1. Hot Reload — instant JS reload (most common use case)
|
|
26
|
+
* 2. Screenshot + Fix — capture the underlying app (modal hidden
|
|
27
|
+
* during capture), attach errors, trigger
|
|
28
|
+
* a fix task on the agent
|
|
29
|
+
* 3. Vibing — open a vibing session on the agent
|
|
30
|
+
* 4. Start / Stop Recording — screen-recording toggle
|
|
31
|
+
* 5. Send Video — submit the last recorded video
|
|
32
|
+
*
|
|
33
|
+
* The header has an explicit X close icon on the right.
|
|
34
|
+
* Live / Narrated / Batch modes, voice notes, and the streaming indicator
|
|
35
|
+
* were removed in 0.7.0 — those flows never worked end-to-end against
|
|
36
|
+
* the Go agent (see MISSINGS_FEEDBACK_SDK.md).
|
|
37
37
|
*/
|
|
38
|
+
|
|
39
|
+
interface LastVideo {
|
|
40
|
+
path: string;
|
|
41
|
+
duration: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ActionState =
|
|
45
|
+
| 'idle'
|
|
46
|
+
| 'hot-reloading'
|
|
47
|
+
| 'capturing'
|
|
48
|
+
| 'vibing'
|
|
49
|
+
| 'sending-video';
|
|
50
|
+
|
|
38
51
|
export const FeedbackModal: React.FC = () => {
|
|
39
52
|
const [visible, setVisible] = useState(false);
|
|
40
|
-
const [
|
|
41
|
-
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
42
|
-
const [isSending, setIsSending] = useState(false);
|
|
53
|
+
const [action, setAction] = useState<ActionState>('idle');
|
|
43
54
|
const [error, setError] = useState<string | null>(null);
|
|
44
|
-
const [
|
|
45
|
-
const [
|
|
46
|
-
const [
|
|
47
|
-
const [isVoiceCommand, setIsVoiceCommand] = useState(false);
|
|
48
|
-
const [isReloading, setIsReloading] = useState(false);
|
|
55
|
+
const [toast, setToast] = useState<string | null>(null);
|
|
56
|
+
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
|
|
57
|
+
const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
|
|
49
58
|
const mountedRef = useRef(true);
|
|
50
|
-
const commentaryListRef = useRef<FlatList>(null);
|
|
51
59
|
|
|
52
60
|
useEffect(() => {
|
|
53
61
|
mountedRef.current = true;
|
|
54
62
|
const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
55
63
|
if (YaverFeedback.isEnabled()) {
|
|
56
64
|
setVisible(true);
|
|
57
|
-
setTimeline([]);
|
|
58
65
|
setError(null);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
setMode(YaverFeedback.getFeedbackMode());
|
|
66
|
+
setToast(null);
|
|
67
|
+
setAction('idle');
|
|
62
68
|
}
|
|
63
69
|
});
|
|
64
|
-
|
|
65
|
-
// Listen for agent commentary events
|
|
66
|
-
const commentarySub = DeviceEventEmitter.addListener(
|
|
67
|
-
'yaverFeedback:commentary',
|
|
68
|
-
(event: AgentCommentary) => {
|
|
69
|
-
if (mountedRef.current) {
|
|
70
|
-
setCommentary((prev) => [...prev, event]);
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
|
|
75
70
|
return () => {
|
|
76
71
|
mountedRef.current = false;
|
|
77
72
|
sub.remove();
|
|
78
|
-
commentarySub.remove();
|
|
79
73
|
};
|
|
80
74
|
}, []);
|
|
81
75
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
type: 'screenshot',
|
|
88
|
-
path,
|
|
89
|
-
timestamp: new Date().toISOString(),
|
|
90
|
-
};
|
|
91
|
-
setTimeline((prev) => [...prev, event]);
|
|
76
|
+
const closeSoon = useCallback((delayMs = 1200) => {
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
if (mountedRef.current) setVisible(false);
|
|
79
|
+
}, delayMs);
|
|
80
|
+
}, []);
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
82
|
+
const handleClose = useCallback(() => {
|
|
83
|
+
setVisible(false);
|
|
84
|
+
setError(null);
|
|
85
|
+
setToast(null);
|
|
86
|
+
setAction('idle');
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
90
|
+
// re-query Convex for the fresh IP and retry once. Solves the common
|
|
91
|
+
// case where the Mac's LAN IP rotated while the SDK held a stale URL.
|
|
92
|
+
const runWithReconnect = useCallback(
|
|
93
|
+
async (fn: (client: NonNullable<ReturnType<typeof YaverFeedback.getP2PClient>>) => Promise<void>) => {
|
|
94
|
+
let client = YaverFeedback.getP2PClient();
|
|
95
|
+
if (!client) {
|
|
96
|
+
const ok = await YaverFeedback.reconnect();
|
|
97
|
+
if (ok) client = YaverFeedback.getP2PClient();
|
|
112
98
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
setError(String(err));
|
|
99
|
+
if (!client) {
|
|
100
|
+
throw new Error('Not connected to the agent yet.');
|
|
116
101
|
}
|
|
117
|
-
}
|
|
118
|
-
}, [mode]);
|
|
119
|
-
|
|
120
|
-
const handleToggleAudio = useCallback(async () => {
|
|
121
|
-
if (isRecordingAudio) {
|
|
122
102
|
try {
|
|
123
|
-
|
|
124
|
-
if (mountedRef.current) {
|
|
125
|
-
setIsRecordingAudio(false);
|
|
126
|
-
const event: TimelineEvent = {
|
|
127
|
-
type: 'audio',
|
|
128
|
-
path: result.path,
|
|
129
|
-
timestamp: new Date().toISOString(),
|
|
130
|
-
duration: result.duration,
|
|
131
|
-
};
|
|
132
|
-
setTimeline((prev) => [...prev, event]);
|
|
133
|
-
|
|
134
|
-
// In live mode, stream the audio event
|
|
135
|
-
if (mode === 'live') {
|
|
136
|
-
const client = YaverFeedback.getP2PClient();
|
|
137
|
-
if (client) {
|
|
138
|
-
try {
|
|
139
|
-
await client.streamFeedback(
|
|
140
|
-
(async function* () {
|
|
141
|
-
yield {
|
|
142
|
-
type: 'audio',
|
|
143
|
-
timestamp: event.timestamp,
|
|
144
|
-
data: { path: result.path, duration: result.duration },
|
|
145
|
-
};
|
|
146
|
-
})(),
|
|
147
|
-
);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
console.warn('[YaverFeedback] Live stream failed:', err);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
103
|
+
await fn(client);
|
|
154
104
|
} catch (err) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
105
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
+
const transient = /Network request failed|timeout|ECONNREFUSED|Failed to fetch|fetch failed|aborted/i.test(msg);
|
|
107
|
+
if (!transient) throw err;
|
|
108
|
+
const ok = await YaverFeedback.reconnect();
|
|
109
|
+
if (!ok) throw err;
|
|
110
|
+
const fresh = YaverFeedback.getP2PClient();
|
|
111
|
+
if (!fresh) throw err;
|
|
112
|
+
await fn(fresh);
|
|
159
113
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
114
|
+
},
|
|
115
|
+
[],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// ─── 1. Hot reload ─────────────────────────────────────────────────
|
|
119
|
+
const handleHotReload = useCallback(async () => {
|
|
120
|
+
setAction('hot-reloading');
|
|
121
|
+
setError(null);
|
|
122
|
+
try {
|
|
123
|
+
await runWithReconnect(async (client) => {
|
|
124
|
+
await client.reloadApp('dev');
|
|
125
|
+
});
|
|
126
|
+
setToast('Reload sent');
|
|
127
|
+
closeSoon(800);
|
|
128
|
+
} catch (err: unknown) {
|
|
129
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
130
|
+
} finally {
|
|
131
|
+
if (mountedRef.current) setAction('idle');
|
|
132
|
+
}
|
|
133
|
+
}, [closeSoon, runWithReconnect]);
|
|
134
|
+
|
|
135
|
+
// ─── 2. Screenshot + Fix ───────────────────────────────────────────
|
|
136
|
+
//
|
|
137
|
+
// Hide the modal first so the screenshot captures the actual screen
|
|
138
|
+
// (the bug) — not the modal card. Await a short animation delay,
|
|
139
|
+
// snapshot, upload the feedback bundle with any buffered errors, then
|
|
140
|
+
// kick `/feedback/{id}/fix` to create the repair task.
|
|
141
|
+
const handleScreenshotAndFix = useCallback(async () => {
|
|
142
|
+
const client = YaverFeedback.getP2PClient();
|
|
143
|
+
const config = YaverFeedback.getConfig();
|
|
144
|
+
if (!client || !config?.agentUrl) {
|
|
145
|
+
setError('Not connected to the agent yet.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setAction('capturing');
|
|
149
|
+
setError(null);
|
|
150
|
+
|
|
151
|
+
// Step 1: Hide the modal so the screenshot contains the real screen.
|
|
152
|
+
setVisible(false);
|
|
153
|
+
// Wait out the slide-down animation on both platforms.
|
|
154
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
155
|
+
|
|
156
|
+
let path: string | null = null;
|
|
157
|
+
try {
|
|
158
|
+
path = await captureScreenshot();
|
|
159
|
+
} catch (err: unknown) {
|
|
160
|
+
setVisible(true);
|
|
161
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
162
|
+
setAction('idle');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Step 2: Re-show the modal for progress + ack.
|
|
167
|
+
setVisible(true);
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const { Dimensions } = require('react-native');
|
|
172
|
+
const { width, height } = Dimensions.get('window');
|
|
173
|
+
const deviceInfo: DeviceInfo = {
|
|
174
|
+
platform: Platform.OS,
|
|
175
|
+
osVersion: String(Platform.Version),
|
|
176
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
177
|
+
screenWidth: width,
|
|
178
|
+
screenHeight: height,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
182
|
+
const bundle: FeedbackBundle = {
|
|
183
|
+
metadata: {
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
device: deviceInfo,
|
|
186
|
+
app: {},
|
|
187
|
+
userNote: '[Screenshot + Fix]',
|
|
188
|
+
},
|
|
189
|
+
screenshots: [path],
|
|
190
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const uploaded = await uploadFeedback(
|
|
194
|
+
config.agentUrl,
|
|
195
|
+
config.authToken ?? '',
|
|
196
|
+
bundle,
|
|
197
|
+
);
|
|
198
|
+
// The agent returns the new report id as `id` (see
|
|
199
|
+
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
200
|
+
// one back; otherwise just ack the upload.
|
|
201
|
+
const reportId =
|
|
202
|
+
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
203
|
+
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
204
|
+
if (reportId) {
|
|
205
|
+
try {
|
|
206
|
+
await client.triggerFix(reportId);
|
|
207
|
+
setToast('Fix task started');
|
|
208
|
+
} catch (err: unknown) {
|
|
209
|
+
setToast('Report uploaded — fix trigger failed');
|
|
210
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
169
211
|
}
|
|
212
|
+
} else {
|
|
213
|
+
setToast('Report uploaded');
|
|
170
214
|
}
|
|
215
|
+
closeSoon(1400);
|
|
216
|
+
} catch (err: unknown) {
|
|
217
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
218
|
+
} finally {
|
|
219
|
+
if (mountedRef.current) setAction('idle');
|
|
220
|
+
}
|
|
221
|
+
}, [closeSoon]);
|
|
222
|
+
|
|
223
|
+
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
224
|
+
const handleVibing = useCallback(async () => {
|
|
225
|
+
const client = YaverFeedback.getP2PClient();
|
|
226
|
+
if (!client) {
|
|
227
|
+
setError('Not connected to the agent yet.');
|
|
228
|
+
return;
|
|
171
229
|
}
|
|
172
|
-
|
|
230
|
+
setAction('vibing');
|
|
231
|
+
setError(null);
|
|
232
|
+
try {
|
|
233
|
+
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
234
|
+
const errNote =
|
|
235
|
+
capturedErrors.length > 0
|
|
236
|
+
? `\n\nRecent captured errors:\n` +
|
|
237
|
+
capturedErrors
|
|
238
|
+
.slice(-3)
|
|
239
|
+
.map((e) => `- ${e.message}`)
|
|
240
|
+
.join('\n')
|
|
241
|
+
: '';
|
|
242
|
+
const prompt =
|
|
243
|
+
'The user opened the feedback modal on their phone and tapped Vibing. ' +
|
|
244
|
+
'Investigate whatever they are likely to be asking about — pick the ' +
|
|
245
|
+
'next small improvement or fix based on recent activity and the ' +
|
|
246
|
+
'current screen.' +
|
|
247
|
+
errNote;
|
|
248
|
+
await client.vibing(prompt);
|
|
249
|
+
setToast('Vibing task created');
|
|
250
|
+
closeSoon(1200);
|
|
251
|
+
} catch (err: unknown) {
|
|
252
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
253
|
+
} finally {
|
|
254
|
+
if (mountedRef.current) setAction('idle');
|
|
255
|
+
}
|
|
256
|
+
}, [closeSoon]);
|
|
173
257
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
258
|
+
// ─── 4. Toggle screen recording ────────────────────────────────────
|
|
259
|
+
const handleToggleRecording = useCallback(async () => {
|
|
260
|
+
setError(null);
|
|
261
|
+
if (isRecordingVideo) {
|
|
177
262
|
try {
|
|
178
|
-
const result = await
|
|
263
|
+
const result = await stopVideoRecording();
|
|
179
264
|
if (mountedRef.current) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (client) {
|
|
184
|
-
try {
|
|
185
|
-
await client.streamFeedback(
|
|
186
|
-
(async function* () {
|
|
187
|
-
yield {
|
|
188
|
-
type: 'voice_command',
|
|
189
|
-
timestamp: new Date().toISOString(),
|
|
190
|
-
data: { path: result.path, duration: result.duration },
|
|
191
|
-
};
|
|
192
|
-
})(),
|
|
193
|
-
);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.warn('[YaverFeedback] Voice command send failed:', err);
|
|
196
|
-
if (mountedRef.current) {
|
|
197
|
-
setError('Failed to send voice command.');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
if (mountedRef.current) {
|
|
204
|
-
setIsVoiceCommand(false);
|
|
205
|
-
setError(String(err));
|
|
265
|
+
setIsRecordingVideo(false);
|
|
266
|
+
setLastVideo(result);
|
|
267
|
+
setToast(`Recording stopped — ${Math.round(result.duration)}s`);
|
|
206
268
|
}
|
|
269
|
+
} catch (err: unknown) {
|
|
270
|
+
setIsRecordingVideo(false);
|
|
271
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
207
272
|
}
|
|
208
273
|
} else {
|
|
209
274
|
try {
|
|
210
|
-
await
|
|
211
|
-
if (mountedRef.current) {
|
|
212
|
-
setIsVoiceCommand(true);
|
|
213
|
-
}
|
|
214
|
-
} catch (err) {
|
|
275
|
+
await startVideoRecording();
|
|
215
276
|
if (mountedRef.current) {
|
|
216
|
-
|
|
277
|
+
setIsRecordingVideo(true);
|
|
278
|
+
setToast('Recording…');
|
|
279
|
+
setLastVideo(null);
|
|
217
280
|
}
|
|
281
|
+
} catch (err: unknown) {
|
|
282
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
218
283
|
}
|
|
219
284
|
}
|
|
220
|
-
}, [
|
|
285
|
+
}, [isRecordingVideo]);
|
|
221
286
|
|
|
222
|
-
|
|
287
|
+
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
288
|
+
const handleSendVideo = useCallback(async () => {
|
|
223
289
|
const config = YaverFeedback.getConfig();
|
|
224
|
-
if (!config?.agentUrl)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const response = await fetch(`${config.agentUrl.replace(/\/$/, '')}/dev/reload-app`, {
|
|
229
|
-
method: 'POST',
|
|
230
|
-
headers: {
|
|
231
|
-
Authorization: `Bearer ${config.authToken}`,
|
|
232
|
-
'Content-Type': 'application/json',
|
|
233
|
-
},
|
|
234
|
-
body: JSON.stringify({ mode: 'dev' }),
|
|
235
|
-
});
|
|
236
|
-
if (response.ok) {
|
|
237
|
-
BlackBox.lifecycle('Hot reload triggered from feedback SDK');
|
|
238
|
-
}
|
|
239
|
-
} catch (err) {
|
|
240
|
-
if (mountedRef.current) {
|
|
241
|
-
setError('Reload failed: ' + String(err));
|
|
242
|
-
}
|
|
243
|
-
} finally {
|
|
244
|
-
if (mountedRef.current) {
|
|
245
|
-
setIsReloading(false);
|
|
246
|
-
}
|
|
290
|
+
if (!config?.agentUrl) {
|
|
291
|
+
setError('Not connected to the agent yet.');
|
|
292
|
+
return;
|
|
247
293
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
setIsSending(true);
|
|
294
|
+
if (!lastVideo) {
|
|
295
|
+
setError('No video recorded yet.');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
setAction('sending-video');
|
|
255
299
|
setError(null);
|
|
256
|
-
|
|
257
300
|
try {
|
|
258
301
|
const { Dimensions } = require('react-native');
|
|
259
302
|
const { width, height } = Dimensions.get('window');
|
|
260
|
-
|
|
261
303
|
const deviceInfo: DeviceInfo = {
|
|
262
304
|
platform: Platform.OS,
|
|
263
305
|
osVersion: String(Platform.Version),
|
|
@@ -265,421 +307,224 @@ export const FeedbackModal: React.FC = () => {
|
|
|
265
307
|
screenWidth: width,
|
|
266
308
|
screenHeight: height,
|
|
267
309
|
};
|
|
268
|
-
|
|
269
|
-
const screenshots = timeline
|
|
270
|
-
.filter((e) => e.type === 'screenshot')
|
|
271
|
-
.map((e) => e.path);
|
|
272
|
-
|
|
273
|
-
const audioEvent = timeline.find((e) => e.type === 'audio');
|
|
274
|
-
|
|
275
|
-
// Include captured errors from the error buffer
|
|
276
|
-
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
277
|
-
|
|
278
310
|
const bundle: FeedbackBundle = {
|
|
279
311
|
metadata: {
|
|
280
312
|
timestamp: new Date().toISOString(),
|
|
281
313
|
device: deviceInfo,
|
|
282
314
|
app: {},
|
|
315
|
+
userNote: '[Screen recording]',
|
|
283
316
|
},
|
|
284
|
-
screenshots,
|
|
285
|
-
|
|
286
|
-
errors:
|
|
317
|
+
screenshots: [],
|
|
318
|
+
video: lastVideo.path,
|
|
319
|
+
errors: YaverFeedback.getCapturedErrors().length
|
|
320
|
+
? YaverFeedback.getCapturedErrors()
|
|
321
|
+
: undefined,
|
|
287
322
|
};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
setTimeout(() => {
|
|
295
|
-
if (mountedRef.current) {
|
|
296
|
-
setVisible(false);
|
|
297
|
-
}
|
|
298
|
-
}, 1500);
|
|
299
|
-
}
|
|
300
|
-
} catch (err) {
|
|
301
|
-
if (mountedRef.current) {
|
|
302
|
-
setError(String(err));
|
|
303
|
-
}
|
|
323
|
+
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
324
|
+
setToast('Video sent');
|
|
325
|
+
setLastVideo(null);
|
|
326
|
+
closeSoon(1200);
|
|
327
|
+
} catch (err: unknown) {
|
|
328
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
304
329
|
} finally {
|
|
305
|
-
if (mountedRef.current)
|
|
306
|
-
setIsSending(false);
|
|
307
|
-
}
|
|
330
|
+
if (mountedRef.current) setAction('idle');
|
|
308
331
|
}
|
|
309
|
-
}, [
|
|
310
|
-
|
|
311
|
-
const handleCancel = useCallback(() => {
|
|
312
|
-
setVisible(false);
|
|
313
|
-
setTimeline([]);
|
|
314
|
-
setError(null);
|
|
315
|
-
setSent(false);
|
|
316
|
-
setIsRecordingAudio(false);
|
|
317
|
-
setIsVoiceCommand(false);
|
|
318
|
-
setCommentary([]);
|
|
319
|
-
}, []);
|
|
332
|
+
}, [lastVideo, closeSoon]);
|
|
320
333
|
|
|
321
|
-
const
|
|
322
|
-
({ item }: { item: AgentCommentary }) => (
|
|
323
|
-
<View style={styles.commentaryBubble}>
|
|
324
|
-
<Text style={styles.commentaryType}>{item.type}</Text>
|
|
325
|
-
<Text style={styles.commentaryMessage}>{item.message}</Text>
|
|
326
|
-
</View>
|
|
327
|
-
),
|
|
328
|
-
[],
|
|
329
|
-
);
|
|
334
|
+
const busy = action !== 'idle';
|
|
330
335
|
|
|
331
|
-
// The AuthOverlay must stay mounted so it can respond to login / picker
|
|
332
|
-
// events even when the feedback modal itself isn't visible. The wrapping
|
|
333
|
-
// fragment keeps the original return shape intact for the modal branch.
|
|
334
336
|
return (
|
|
335
337
|
<>
|
|
336
338
|
<AuthOverlay />
|
|
337
339
|
{visible && (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
onPress={() => setMode(m)}
|
|
355
|
-
>
|
|
356
|
-
<Text
|
|
357
|
-
style={[styles.modeButtonText, mode === m && styles.modeButtonTextActive]}
|
|
340
|
+
<Modal
|
|
341
|
+
visible={visible}
|
|
342
|
+
animationType="slide"
|
|
343
|
+
transparent
|
|
344
|
+
onRequestClose={handleClose}
|
|
345
|
+
>
|
|
346
|
+
<Pressable style={styles.overlay} onPress={handleClose}>
|
|
347
|
+
<Pressable style={styles.modal} onPress={(e) => e.stopPropagation()}>
|
|
348
|
+
<View style={styles.header}>
|
|
349
|
+
<Text style={styles.title}>Send Feedback</Text>
|
|
350
|
+
<Pressable
|
|
351
|
+
onPress={handleClose}
|
|
352
|
+
hitSlop={12}
|
|
353
|
+
style={styles.closeBtn}
|
|
354
|
+
accessibilityRole="button"
|
|
355
|
+
accessibilityLabel="Close"
|
|
358
356
|
>
|
|
359
|
-
{
|
|
360
|
-
</
|
|
361
|
-
</TouchableOpacity>
|
|
362
|
-
))}
|
|
363
|
-
</View>
|
|
364
|
-
|
|
365
|
-
{/* Agent commentary (chat-like view) */}
|
|
366
|
-
{commentary.length > 0 && (
|
|
367
|
-
<FlatList
|
|
368
|
-
ref={commentaryListRef}
|
|
369
|
-
data={commentary}
|
|
370
|
-
renderItem={renderCommentaryItem}
|
|
371
|
-
keyExtractor={(item) => item.id}
|
|
372
|
-
style={styles.commentaryList}
|
|
373
|
-
onContentSizeChange={() =>
|
|
374
|
-
commentaryListRef.current?.scrollToEnd({ animated: true })
|
|
375
|
-
}
|
|
376
|
-
/>
|
|
377
|
-
)}
|
|
378
|
-
|
|
379
|
-
{/* Timeline of captured items */}
|
|
380
|
-
{timeline.length > 0 && (
|
|
381
|
-
<ScrollView style={styles.timeline} horizontal>
|
|
382
|
-
{timeline.map((event, idx) => (
|
|
383
|
-
<View key={idx} style={styles.timelineItem}>
|
|
384
|
-
<Text style={styles.timelineIcon}>
|
|
385
|
-
{event.type === 'screenshot'
|
|
386
|
-
? '[img]'
|
|
387
|
-
: event.type === 'audio'
|
|
388
|
-
? '[mic]'
|
|
389
|
-
: '[vid]'}
|
|
390
|
-
</Text>
|
|
391
|
-
<Text style={styles.timelineLabel}>{event.type}</Text>
|
|
392
|
-
{event.duration != null && (
|
|
393
|
-
<Text style={styles.timelineDuration}>
|
|
394
|
-
{Math.round(event.duration)}s
|
|
395
|
-
</Text>
|
|
396
|
-
)}
|
|
397
|
-
</View>
|
|
398
|
-
))}
|
|
399
|
-
</ScrollView>
|
|
400
|
-
)}
|
|
401
|
-
|
|
402
|
-
{/* Action buttons */}
|
|
403
|
-
<View style={styles.actions}>
|
|
404
|
-
<TouchableOpacity style={styles.actionButton} onPress={handleScreenshot}>
|
|
405
|
-
<Text style={styles.actionText}>Take Screenshot</Text>
|
|
406
|
-
</TouchableOpacity>
|
|
407
|
-
|
|
408
|
-
<TouchableOpacity
|
|
409
|
-
style={[styles.actionButton, isRecordingAudio && styles.actionButtonActive]}
|
|
410
|
-
onPress={handleToggleAudio}
|
|
411
|
-
disabled={isVoiceCommand}
|
|
412
|
-
>
|
|
413
|
-
<Text style={styles.actionText}>
|
|
414
|
-
{isRecordingAudio ? 'Stop Recording' : 'Voice Note'}
|
|
415
|
-
</Text>
|
|
416
|
-
</TouchableOpacity>
|
|
417
|
-
</View>
|
|
418
|
-
|
|
419
|
-
{/* Hot Reload + Streaming status */}
|
|
420
|
-
<View style={styles.actions}>
|
|
421
|
-
<TouchableOpacity
|
|
422
|
-
style={[styles.actionButton, styles.reloadButton]}
|
|
423
|
-
onPress={handleReload}
|
|
424
|
-
disabled={isReloading}
|
|
425
|
-
>
|
|
426
|
-
<Text style={styles.actionText}>
|
|
427
|
-
{isReloading ? 'Reloading...' : 'Hot Reload'}
|
|
428
|
-
</Text>
|
|
429
|
-
</TouchableOpacity>
|
|
430
|
-
|
|
431
|
-
<View style={[styles.actionButton, styles.streamingIndicator]}>
|
|
432
|
-
<View style={[styles.streamingDot, BlackBox.isStreaming && styles.streamingDotActive]} />
|
|
433
|
-
<Text style={styles.streamingText}>
|
|
434
|
-
{BlackBox.isStreaming ? 'Streaming' : 'Not streaming'}
|
|
435
|
-
</Text>
|
|
436
|
-
</View>
|
|
437
|
-
</View>
|
|
438
|
-
|
|
439
|
-
{/* Voice command button */}
|
|
440
|
-
{mode === 'live' && (
|
|
441
|
-
<TouchableOpacity
|
|
442
|
-
style={[styles.voiceCommandButton, isVoiceCommand && styles.voiceCommandActive]}
|
|
443
|
-
onPress={handleVoiceCommand}
|
|
444
|
-
disabled={isRecordingAudio}
|
|
445
|
-
>
|
|
446
|
-
<Text style={styles.voiceCommandText}>
|
|
447
|
-
{isVoiceCommand ? 'Stop & Send Command' : 'Speak to Fix'}
|
|
448
|
-
</Text>
|
|
449
|
-
</TouchableOpacity>
|
|
450
|
-
)}
|
|
451
|
-
|
|
452
|
-
{/* Error display */}
|
|
453
|
-
{error && <Text style={styles.error}>{error}</Text>}
|
|
454
|
-
|
|
455
|
-
{/* Send / Cancel */}
|
|
456
|
-
<View style={styles.footer}>
|
|
457
|
-
<TouchableOpacity style={styles.cancelButton} onPress={handleCancel}>
|
|
458
|
-
<Text style={styles.cancelText}>Cancel</Text>
|
|
459
|
-
</TouchableOpacity>
|
|
460
|
-
|
|
461
|
-
{sent ? (
|
|
462
|
-
<View style={styles.sendButton}>
|
|
463
|
-
<Text style={styles.sendText}>Sent!</Text>
|
|
357
|
+
<Text style={styles.closeIcon}>×</Text>
|
|
358
|
+
</Pressable>
|
|
464
359
|
</View>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
360
|
+
|
|
361
|
+
{/* 1. Hot Reload — the common path */}
|
|
362
|
+
<ActionRow
|
|
363
|
+
label={
|
|
364
|
+
action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'
|
|
365
|
+
}
|
|
366
|
+
tint="#fbbf24"
|
|
367
|
+
onPress={handleHotReload}
|
|
368
|
+
disabled={busy}
|
|
369
|
+
busy={action === 'hot-reloading'}
|
|
370
|
+
/>
|
|
371
|
+
|
|
372
|
+
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
373
|
+
<ActionRow
|
|
374
|
+
label={
|
|
375
|
+
action === 'capturing'
|
|
376
|
+
? 'Capturing…'
|
|
377
|
+
: 'Screenshot & Fix'
|
|
378
|
+
}
|
|
379
|
+
tint="#22c55e"
|
|
380
|
+
onPress={handleScreenshotAndFix}
|
|
381
|
+
disabled={busy}
|
|
382
|
+
busy={action === 'capturing'}
|
|
383
|
+
/>
|
|
384
|
+
|
|
385
|
+
{/* 3. Vibing */}
|
|
386
|
+
<ActionRow
|
|
387
|
+
label={action === 'vibing' ? 'Starting…' : 'Vibing'}
|
|
388
|
+
tint="#818cf8"
|
|
389
|
+
onPress={handleVibing}
|
|
390
|
+
disabled={busy}
|
|
391
|
+
busy={action === 'vibing'}
|
|
392
|
+
/>
|
|
393
|
+
|
|
394
|
+
{/* 4. Start/Stop Recording */}
|
|
395
|
+
<ActionRow
|
|
396
|
+
label={isRecordingVideo ? 'Stop Recording' : 'Start Recording'}
|
|
397
|
+
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
398
|
+
onPress={handleToggleRecording}
|
|
399
|
+
disabled={busy && action !== 'idle' && !isRecordingVideo}
|
|
400
|
+
/>
|
|
401
|
+
|
|
402
|
+
{/* 5. Send Video (only tappable when a clip is ready) */}
|
|
403
|
+
<ActionRow
|
|
404
|
+
label={
|
|
405
|
+
action === 'sending-video'
|
|
406
|
+
? 'Sending…'
|
|
407
|
+
: lastVideo
|
|
408
|
+
? `Send Video · ${Math.round(lastVideo.duration)}s`
|
|
409
|
+
: 'Send Video'
|
|
410
|
+
}
|
|
411
|
+
tint="#a78bfa"
|
|
412
|
+
onPress={handleSendVideo}
|
|
413
|
+
disabled={busy || !lastVideo}
|
|
414
|
+
busy={action === 'sending-video'}
|
|
415
|
+
/>
|
|
416
|
+
|
|
417
|
+
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
418
|
+
{error && <Text style={styles.error}>{error}</Text>}
|
|
419
|
+
</Pressable>
|
|
420
|
+
</Pressable>
|
|
421
|
+
</Modal>
|
|
482
422
|
)}
|
|
483
423
|
</>
|
|
484
424
|
);
|
|
485
425
|
};
|
|
486
426
|
|
|
427
|
+
interface ActionRowProps {
|
|
428
|
+
label: string;
|
|
429
|
+
tint: string;
|
|
430
|
+
onPress: () => void;
|
|
431
|
+
disabled?: boolean;
|
|
432
|
+
busy?: boolean;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const ActionRow: React.FC<ActionRowProps> = ({
|
|
436
|
+
label,
|
|
437
|
+
tint,
|
|
438
|
+
onPress,
|
|
439
|
+
disabled,
|
|
440
|
+
busy,
|
|
441
|
+
}) => (
|
|
442
|
+
<Pressable
|
|
443
|
+
onPress={onPress}
|
|
444
|
+
disabled={disabled}
|
|
445
|
+
style={({ pressed }) => [
|
|
446
|
+
styles.actionBtn,
|
|
447
|
+
{
|
|
448
|
+
borderColor: tint + '66',
|
|
449
|
+
backgroundColor: tint + '1f',
|
|
450
|
+
},
|
|
451
|
+
disabled && styles.actionBtnDisabled,
|
|
452
|
+
pressed && !disabled && { opacity: 0.7 },
|
|
453
|
+
]}
|
|
454
|
+
accessibilityRole="button"
|
|
455
|
+
accessibilityLabel={label}
|
|
456
|
+
>
|
|
457
|
+
{busy ? (
|
|
458
|
+
<ActivityIndicator color={tint} size="small" />
|
|
459
|
+
) : (
|
|
460
|
+
<Text style={[styles.actionText, { color: tint }]}>{label}</Text>
|
|
461
|
+
)}
|
|
462
|
+
</Pressable>
|
|
463
|
+
);
|
|
464
|
+
|
|
487
465
|
const styles = StyleSheet.create({
|
|
488
466
|
overlay: {
|
|
489
467
|
flex: 1,
|
|
490
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
468
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
491
469
|
justifyContent: 'flex-end',
|
|
492
470
|
},
|
|
493
471
|
modal: {
|
|
494
|
-
backgroundColor: '#
|
|
495
|
-
borderTopLeftRadius:
|
|
496
|
-
borderTopRightRadius:
|
|
497
|
-
padding:
|
|
498
|
-
paddingBottom:
|
|
499
|
-
|
|
472
|
+
backgroundColor: '#141422',
|
|
473
|
+
borderTopLeftRadius: 22,
|
|
474
|
+
borderTopRightRadius: 22,
|
|
475
|
+
padding: 22,
|
|
476
|
+
paddingBottom: 36,
|
|
477
|
+
gap: 12,
|
|
478
|
+
},
|
|
479
|
+
header: {
|
|
480
|
+
flexDirection: 'row',
|
|
481
|
+
alignItems: 'center',
|
|
482
|
+
justifyContent: 'space-between',
|
|
483
|
+
marginBottom: 6,
|
|
500
484
|
},
|
|
501
485
|
title: {
|
|
502
486
|
fontSize: 20,
|
|
503
487
|
fontWeight: '700',
|
|
504
488
|
color: '#fff',
|
|
505
|
-
marginBottom: 12,
|
|
506
|
-
},
|
|
507
|
-
modeSelector: {
|
|
508
|
-
flexDirection: 'row',
|
|
509
|
-
gap: 8,
|
|
510
|
-
marginBottom: 16,
|
|
511
489
|
},
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
borderRadius:
|
|
490
|
+
closeBtn: {
|
|
491
|
+
width: 36,
|
|
492
|
+
height: 36,
|
|
493
|
+
borderRadius: 18,
|
|
516
494
|
alignItems: 'center',
|
|
495
|
+
justifyContent: 'center',
|
|
517
496
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
518
|
-
borderWidth: 1,
|
|
519
|
-
borderColor: 'rgba(255,255,255,0.1)',
|
|
520
|
-
},
|
|
521
|
-
modeButtonActive: {
|
|
522
|
-
backgroundColor: 'rgba(99,102,241,0.3)',
|
|
523
|
-
borderColor: '#6366f1',
|
|
524
|
-
},
|
|
525
|
-
modeButtonText: {
|
|
526
|
-
color: '#999',
|
|
527
|
-
fontSize: 13,
|
|
528
|
-
fontWeight: '600',
|
|
529
|
-
},
|
|
530
|
-
modeButtonTextActive: {
|
|
531
|
-
color: '#c7c8ff',
|
|
532
|
-
},
|
|
533
|
-
commentaryList: {
|
|
534
|
-
maxHeight: 140,
|
|
535
|
-
marginBottom: 12,
|
|
536
497
|
},
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
borderLeftWidth: 3,
|
|
543
|
-
borderLeftColor: '#6366f1',
|
|
544
|
-
},
|
|
545
|
-
commentaryType: {
|
|
546
|
-
color: '#8b8bf5',
|
|
547
|
-
fontSize: 10,
|
|
548
|
-
fontWeight: '700',
|
|
549
|
-
textTransform: 'uppercase',
|
|
550
|
-
marginBottom: 2,
|
|
551
|
-
},
|
|
552
|
-
commentaryMessage: {
|
|
553
|
-
color: '#d0d0e0',
|
|
554
|
-
fontSize: 13,
|
|
555
|
-
lineHeight: 18,
|
|
556
|
-
},
|
|
557
|
-
timeline: {
|
|
558
|
-
maxHeight: 80,
|
|
559
|
-
marginBottom: 16,
|
|
498
|
+
closeIcon: {
|
|
499
|
+
color: '#fff',
|
|
500
|
+
fontSize: 22,
|
|
501
|
+
lineHeight: 24,
|
|
502
|
+
fontWeight: '400',
|
|
560
503
|
},
|
|
561
|
-
|
|
504
|
+
actionBtn: {
|
|
505
|
+
paddingVertical: 16,
|
|
506
|
+
borderRadius: 14,
|
|
562
507
|
alignItems: 'center',
|
|
563
|
-
|
|
564
|
-
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
565
|
-
borderRadius: 12,
|
|
566
|
-
padding: 10,
|
|
567
|
-
minWidth: 70,
|
|
568
|
-
},
|
|
569
|
-
timelineIcon: {
|
|
570
|
-
fontSize: 14,
|
|
571
|
-
color: '#ccc',
|
|
572
|
-
fontWeight: '600',
|
|
573
|
-
},
|
|
574
|
-
timelineLabel: {
|
|
575
|
-
color: '#ccc',
|
|
576
|
-
fontSize: 11,
|
|
577
|
-
marginTop: 4,
|
|
578
|
-
},
|
|
579
|
-
timelineDuration: {
|
|
580
|
-
color: '#999',
|
|
581
|
-
fontSize: 10,
|
|
582
|
-
},
|
|
583
|
-
actions: {
|
|
584
|
-
flexDirection: 'row',
|
|
585
|
-
gap: 12,
|
|
586
|
-
marginBottom: 12,
|
|
587
|
-
},
|
|
588
|
-
actionButton: {
|
|
589
|
-
flex: 1,
|
|
590
|
-
backgroundColor: 'rgba(99,102,241,0.2)',
|
|
508
|
+
justifyContent: 'center',
|
|
591
509
|
borderWidth: 1,
|
|
592
|
-
borderColor: 'rgba(99,102,241,0.4)',
|
|
593
|
-
borderRadius: 12,
|
|
594
|
-
paddingVertical: 14,
|
|
595
|
-
alignItems: 'center',
|
|
596
510
|
},
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
borderColor: 'rgba(239,68,68,0.6)',
|
|
511
|
+
actionBtnDisabled: {
|
|
512
|
+
opacity: 0.35,
|
|
600
513
|
},
|
|
601
514
|
actionText: {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
fontWeight: '600',
|
|
605
|
-
},
|
|
606
|
-
voiceCommandButton: {
|
|
607
|
-
backgroundColor: 'rgba(34,197,94,0.2)',
|
|
608
|
-
borderWidth: 1,
|
|
609
|
-
borderColor: 'rgba(34,197,94,0.4)',
|
|
610
|
-
borderRadius: 12,
|
|
611
|
-
paddingVertical: 14,
|
|
612
|
-
alignItems: 'center',
|
|
613
|
-
marginBottom: 12,
|
|
614
|
-
},
|
|
615
|
-
voiceCommandActive: {
|
|
616
|
-
backgroundColor: 'rgba(34,197,94,0.4)',
|
|
617
|
-
borderColor: '#22c55e',
|
|
515
|
+
fontSize: 15,
|
|
516
|
+
fontWeight: '700',
|
|
618
517
|
},
|
|
619
|
-
|
|
518
|
+
toast: {
|
|
620
519
|
color: '#22c55e',
|
|
621
|
-
fontSize:
|
|
622
|
-
|
|
520
|
+
fontSize: 13,
|
|
521
|
+
textAlign: 'center',
|
|
522
|
+
marginTop: 4,
|
|
623
523
|
},
|
|
624
524
|
error: {
|
|
625
525
|
color: '#ef4444',
|
|
626
526
|
fontSize: 12,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
footer: {
|
|
630
|
-
flexDirection: 'row',
|
|
631
|
-
gap: 12,
|
|
632
|
-
},
|
|
633
|
-
cancelButton: {
|
|
634
|
-
flex: 1,
|
|
635
|
-
paddingVertical: 14,
|
|
636
|
-
alignItems: 'center',
|
|
637
|
-
borderRadius: 12,
|
|
638
|
-
borderWidth: 1,
|
|
639
|
-
borderColor: 'rgba(255,255,255,0.2)',
|
|
640
|
-
},
|
|
641
|
-
cancelText: {
|
|
642
|
-
color: '#999',
|
|
643
|
-
fontSize: 16,
|
|
644
|
-
fontWeight: '600',
|
|
645
|
-
},
|
|
646
|
-
sendButton: {
|
|
647
|
-
flex: 2,
|
|
648
|
-
backgroundColor: '#6366f1',
|
|
649
|
-
paddingVertical: 14,
|
|
650
|
-
alignItems: 'center',
|
|
651
|
-
borderRadius: 12,
|
|
652
|
-
},
|
|
653
|
-
sendButtonDisabled: {
|
|
654
|
-
opacity: 0.5,
|
|
655
|
-
},
|
|
656
|
-
sendText: {
|
|
657
|
-
color: '#fff',
|
|
658
|
-
fontSize: 16,
|
|
659
|
-
fontWeight: '700',
|
|
660
|
-
},
|
|
661
|
-
reloadButton: {
|
|
662
|
-
backgroundColor: 'rgba(251,191,36,0.2)',
|
|
663
|
-
borderColor: 'rgba(251,191,36,0.4)',
|
|
664
|
-
},
|
|
665
|
-
streamingIndicator: {
|
|
666
|
-
flexDirection: 'row',
|
|
667
|
-
justifyContent: 'center',
|
|
668
|
-
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
669
|
-
borderColor: 'rgba(255,255,255,0.1)',
|
|
670
|
-
},
|
|
671
|
-
streamingDot: {
|
|
672
|
-
width: 8,
|
|
673
|
-
height: 8,
|
|
674
|
-
borderRadius: 4,
|
|
675
|
-
backgroundColor: '#555',
|
|
676
|
-
marginRight: 6,
|
|
677
|
-
},
|
|
678
|
-
streamingDotActive: {
|
|
679
|
-
backgroundColor: '#22c55e',
|
|
680
|
-
},
|
|
681
|
-
streamingText: {
|
|
682
|
-
color: '#999',
|
|
683
|
-
fontSize: 12,
|
|
527
|
+
textAlign: 'center',
|
|
528
|
+
marginTop: 4,
|
|
684
529
|
},
|
|
685
530
|
});
|