yaver-feedback-react-native 0.6.0 → 0.7.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/dist/FeedbackModal.d.ts +0 -9
- package/dist/FeedbackModal.js +265 -473
- package/dist/P2PClient.d.ts +24 -0
- package/dist/P2PClient.js +65 -10
- package/dist/YaverFeedback.d.ts +0 -4
- package/dist/YaverFeedback.js +3 -10
- package/dist/__tests__/YaverFeedback.test.js +2 -35
- package/dist/__tests__/types.test.js +3 -35
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +28 -3
- package/dist/capture.d.ts +25 -12
- package/dist/capture.js +73 -34
- 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 +21 -32
- package/dist/upload.d.ts +8 -3
- package/dist/upload.js +5 -12
- package/package.json +1 -1
- package/src/FeedbackModal.tsx +384 -565
- package/src/P2PClient.ts +66 -12
- package/src/YaverFeedback.ts +4 -12
- package/src/__tests__/YaverFeedback.test.ts +2 -41
- package/src/__tests__/types.test.ts +3 -39
- package/src/auth.ts +29 -3
- package/src/capture.ts +73 -37
- package/src/expo.ts +0 -2
- package/src/index.ts +19 -10
- package/src/types.ts +21 -33
- package/src/upload.ts +6 -15
package/src/FeedbackModal.tsx
CHANGED
|
@@ -2,262 +2,278 @@ 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
|
-
*
|
|
23
|
+
* Simplified feedback modal — 5 actions:
|
|
24
|
+
*
|
|
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
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
76
|
+
const closeSoon = useCallback((delayMs = 1200) => {
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
if (mountedRef.current) setVisible(false);
|
|
79
|
+
}, delayMs);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const handleClose = useCallback(() => {
|
|
83
|
+
setVisible(false);
|
|
84
|
+
setError(null);
|
|
85
|
+
setToast(null);
|
|
86
|
+
setAction('idle');
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// ─── 1. Hot reload ─────────────────────────────────────────────────
|
|
90
|
+
const handleHotReload = useCallback(async () => {
|
|
91
|
+
const client = YaverFeedback.getP2PClient();
|
|
92
|
+
if (!client) {
|
|
93
|
+
setError('Not connected to the agent yet.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setAction('hot-reloading');
|
|
97
|
+
setError(null);
|
|
83
98
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
await client.reloadApp('dev');
|
|
100
|
+
setToast('Reload sent');
|
|
101
|
+
closeSoon(800);
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
104
|
+
} finally {
|
|
105
|
+
if (mountedRef.current) setAction('idle');
|
|
106
|
+
}
|
|
107
|
+
}, [closeSoon]);
|
|
108
|
+
|
|
109
|
+
// ─── 2. Screenshot + Fix ───────────────────────────────────────────
|
|
110
|
+
//
|
|
111
|
+
// Hide the modal first so the screenshot captures the actual screen
|
|
112
|
+
// (the bug) — not the modal card. Await a short animation delay,
|
|
113
|
+
// snapshot, upload the feedback bundle with any buffered errors, then
|
|
114
|
+
// kick `/feedback/{id}/fix` to create the repair task.
|
|
115
|
+
const handleScreenshotAndFix = useCallback(async () => {
|
|
116
|
+
const client = YaverFeedback.getP2PClient();
|
|
117
|
+
const config = YaverFeedback.getConfig();
|
|
118
|
+
if (!client || !config?.agentUrl) {
|
|
119
|
+
setError('Not connected to the agent yet.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
setAction('capturing');
|
|
123
|
+
setError(null);
|
|
92
124
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.warn('[YaverFeedback] Live stream failed:', err);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
if (mountedRef.current) {
|
|
115
|
-
setError(String(err));
|
|
116
|
-
}
|
|
125
|
+
// Step 1: Hide the modal so the screenshot contains the real screen.
|
|
126
|
+
setVisible(false);
|
|
127
|
+
// Wait out the slide-down animation on both platforms.
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
129
|
+
|
|
130
|
+
let path: string | null = null;
|
|
131
|
+
try {
|
|
132
|
+
path = await captureScreenshot();
|
|
133
|
+
} catch (err: unknown) {
|
|
134
|
+
setVisible(true);
|
|
135
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
136
|
+
setAction('idle');
|
|
137
|
+
return;
|
|
117
138
|
}
|
|
118
|
-
}, [mode]);
|
|
119
139
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const result = await stopAudioRecording();
|
|
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]);
|
|
140
|
+
// Step 2: Re-show the modal for progress + ack.
|
|
141
|
+
setVisible(true);
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
144
|
+
try {
|
|
145
|
+
const { Dimensions } = require('react-native');
|
|
146
|
+
const { width, height } = Dimensions.get('window');
|
|
147
|
+
const deviceInfo: DeviceInfo = {
|
|
148
|
+
platform: Platform.OS,
|
|
149
|
+
osVersion: String(Platform.Version),
|
|
150
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
151
|
+
screenWidth: width,
|
|
152
|
+
screenHeight: height,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
156
|
+
const bundle: FeedbackBundle = {
|
|
157
|
+
metadata: {
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
device: deviceInfo,
|
|
160
|
+
app: {},
|
|
161
|
+
userNote: '[Screenshot + Fix]',
|
|
162
|
+
},
|
|
163
|
+
screenshots: [path],
|
|
164
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const uploaded = await uploadFeedback(
|
|
168
|
+
config.agentUrl,
|
|
169
|
+
config.authToken ?? '',
|
|
170
|
+
bundle,
|
|
171
|
+
);
|
|
172
|
+
// The agent returns the new report id as `id` (see
|
|
173
|
+
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
174
|
+
// one back; otherwise just ack the upload.
|
|
175
|
+
const reportId =
|
|
176
|
+
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
177
|
+
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
178
|
+
if (reportId) {
|
|
179
|
+
try {
|
|
180
|
+
await client.triggerFix(reportId);
|
|
181
|
+
setToast('Fix task started');
|
|
182
|
+
} catch (err: unknown) {
|
|
183
|
+
setToast('Report uploaded — fix trigger failed');
|
|
184
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
169
185
|
}
|
|
186
|
+
} else {
|
|
187
|
+
setToast('Report uploaded');
|
|
170
188
|
}
|
|
189
|
+
closeSoon(1400);
|
|
190
|
+
} catch (err: unknown) {
|
|
191
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
192
|
+
} finally {
|
|
193
|
+
if (mountedRef.current) setAction('idle');
|
|
194
|
+
}
|
|
195
|
+
}, [closeSoon]);
|
|
196
|
+
|
|
197
|
+
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
198
|
+
const handleVibing = useCallback(async () => {
|
|
199
|
+
const client = YaverFeedback.getP2PClient();
|
|
200
|
+
if (!client) {
|
|
201
|
+
setError('Not connected to the agent yet.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
setAction('vibing');
|
|
205
|
+
setError(null);
|
|
206
|
+
try {
|
|
207
|
+
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
208
|
+
const errNote =
|
|
209
|
+
capturedErrors.length > 0
|
|
210
|
+
? `\n\nRecent captured errors:\n` +
|
|
211
|
+
capturedErrors
|
|
212
|
+
.slice(-3)
|
|
213
|
+
.map((e) => `- ${e.message}`)
|
|
214
|
+
.join('\n')
|
|
215
|
+
: '';
|
|
216
|
+
const prompt =
|
|
217
|
+
'The user opened the feedback modal on their phone and tapped Vibing. ' +
|
|
218
|
+
'Investigate whatever they are likely to be asking about — pick the ' +
|
|
219
|
+
'next small improvement or fix based on recent activity and the ' +
|
|
220
|
+
'current screen.' +
|
|
221
|
+
errNote;
|
|
222
|
+
await client.vibing(prompt);
|
|
223
|
+
setToast('Vibing task created');
|
|
224
|
+
closeSoon(1200);
|
|
225
|
+
} catch (err: unknown) {
|
|
226
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
227
|
+
} finally {
|
|
228
|
+
if (mountedRef.current) setAction('idle');
|
|
171
229
|
}
|
|
172
|
-
}, [
|
|
230
|
+
}, [closeSoon]);
|
|
173
231
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
232
|
+
// ─── 4. Toggle screen recording ────────────────────────────────────
|
|
233
|
+
const handleToggleRecording = useCallback(async () => {
|
|
234
|
+
setError(null);
|
|
235
|
+
if (isRecordingVideo) {
|
|
177
236
|
try {
|
|
178
|
-
const result = await
|
|
179
|
-
if (mountedRef.current) {
|
|
180
|
-
setIsVoiceCommand(false);
|
|
181
|
-
|
|
182
|
-
const client = YaverFeedback.getP2PClient();
|
|
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) {
|
|
237
|
+
const result = await stopVideoRecording();
|
|
203
238
|
if (mountedRef.current) {
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
setIsRecordingVideo(false);
|
|
240
|
+
setLastVideo(result);
|
|
241
|
+
setToast(`Recording stopped — ${Math.round(result.duration)}s`);
|
|
206
242
|
}
|
|
243
|
+
} catch (err: unknown) {
|
|
244
|
+
setIsRecordingVideo(false);
|
|
245
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
207
246
|
}
|
|
208
247
|
} else {
|
|
209
248
|
try {
|
|
210
|
-
await
|
|
249
|
+
await startVideoRecording();
|
|
211
250
|
if (mountedRef.current) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (mountedRef.current) {
|
|
216
|
-
setError(String(err));
|
|
251
|
+
setIsRecordingVideo(true);
|
|
252
|
+
setToast('Recording…');
|
|
253
|
+
setLastVideo(null);
|
|
217
254
|
}
|
|
255
|
+
} catch (err: unknown) {
|
|
256
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
218
257
|
}
|
|
219
258
|
}
|
|
220
|
-
}, [
|
|
259
|
+
}, [isRecordingVideo]);
|
|
221
260
|
|
|
222
|
-
|
|
261
|
+
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
262
|
+
const handleSendVideo = useCallback(async () => {
|
|
223
263
|
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
|
-
}
|
|
264
|
+
if (!config?.agentUrl) {
|
|
265
|
+
setError('Not connected to the agent yet.');
|
|
266
|
+
return;
|
|
247
267
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
setIsSending(true);
|
|
268
|
+
if (!lastVideo) {
|
|
269
|
+
setError('No video recorded yet.');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
setAction('sending-video');
|
|
255
273
|
setError(null);
|
|
256
|
-
|
|
257
274
|
try {
|
|
258
275
|
const { Dimensions } = require('react-native');
|
|
259
276
|
const { width, height } = Dimensions.get('window');
|
|
260
|
-
|
|
261
277
|
const deviceInfo: DeviceInfo = {
|
|
262
278
|
platform: Platform.OS,
|
|
263
279
|
osVersion: String(Platform.Version),
|
|
@@ -265,421 +281,224 @@ export const FeedbackModal: React.FC = () => {
|
|
|
265
281
|
screenWidth: width,
|
|
266
282
|
screenHeight: height,
|
|
267
283
|
};
|
|
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
284
|
const bundle: FeedbackBundle = {
|
|
279
285
|
metadata: {
|
|
280
286
|
timestamp: new Date().toISOString(),
|
|
281
287
|
device: deviceInfo,
|
|
282
288
|
app: {},
|
|
289
|
+
userNote: '[Screen recording]',
|
|
283
290
|
},
|
|
284
|
-
screenshots,
|
|
285
|
-
|
|
286
|
-
errors:
|
|
291
|
+
screenshots: [],
|
|
292
|
+
video: lastVideo.path,
|
|
293
|
+
errors: YaverFeedback.getCapturedErrors().length
|
|
294
|
+
? YaverFeedback.getCapturedErrors()
|
|
295
|
+
: undefined,
|
|
287
296
|
};
|
|
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
|
-
}
|
|
297
|
+
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
298
|
+
setToast('Video sent');
|
|
299
|
+
setLastVideo(null);
|
|
300
|
+
closeSoon(1200);
|
|
301
|
+
} catch (err: unknown) {
|
|
302
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
304
303
|
} finally {
|
|
305
|
-
if (mountedRef.current)
|
|
306
|
-
setIsSending(false);
|
|
307
|
-
}
|
|
304
|
+
if (mountedRef.current) setAction('idle');
|
|
308
305
|
}
|
|
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
|
-
}, []);
|
|
306
|
+
}, [lastVideo, closeSoon]);
|
|
320
307
|
|
|
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
|
-
);
|
|
308
|
+
const busy = action !== 'idle';
|
|
330
309
|
|
|
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
310
|
return (
|
|
335
311
|
<>
|
|
336
312
|
<AuthOverlay />
|
|
337
313
|
{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]}
|
|
314
|
+
<Modal
|
|
315
|
+
visible={visible}
|
|
316
|
+
animationType="slide"
|
|
317
|
+
transparent
|
|
318
|
+
onRequestClose={handleClose}
|
|
319
|
+
>
|
|
320
|
+
<Pressable style={styles.overlay} onPress={handleClose}>
|
|
321
|
+
<Pressable style={styles.modal} onPress={(e) => e.stopPropagation()}>
|
|
322
|
+
<View style={styles.header}>
|
|
323
|
+
<Text style={styles.title}>Send Feedback</Text>
|
|
324
|
+
<Pressable
|
|
325
|
+
onPress={handleClose}
|
|
326
|
+
hitSlop={12}
|
|
327
|
+
style={styles.closeBtn}
|
|
328
|
+
accessibilityRole="button"
|
|
329
|
+
accessibilityLabel="Close"
|
|
358
330
|
>
|
|
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>
|
|
331
|
+
<Text style={styles.closeIcon}>×</Text>
|
|
332
|
+
</Pressable>
|
|
464
333
|
</View>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
334
|
+
|
|
335
|
+
{/* 1. Hot Reload — the common path */}
|
|
336
|
+
<ActionRow
|
|
337
|
+
label={
|
|
338
|
+
action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'
|
|
339
|
+
}
|
|
340
|
+
tint="#fbbf24"
|
|
341
|
+
onPress={handleHotReload}
|
|
342
|
+
disabled={busy}
|
|
343
|
+
busy={action === 'hot-reloading'}
|
|
344
|
+
/>
|
|
345
|
+
|
|
346
|
+
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
347
|
+
<ActionRow
|
|
348
|
+
label={
|
|
349
|
+
action === 'capturing'
|
|
350
|
+
? 'Capturing…'
|
|
351
|
+
: 'Screenshot & Fix'
|
|
352
|
+
}
|
|
353
|
+
tint="#22c55e"
|
|
354
|
+
onPress={handleScreenshotAndFix}
|
|
355
|
+
disabled={busy}
|
|
356
|
+
busy={action === 'capturing'}
|
|
357
|
+
/>
|
|
358
|
+
|
|
359
|
+
{/* 3. Vibing */}
|
|
360
|
+
<ActionRow
|
|
361
|
+
label={action === 'vibing' ? 'Starting…' : 'Vibing'}
|
|
362
|
+
tint="#818cf8"
|
|
363
|
+
onPress={handleVibing}
|
|
364
|
+
disabled={busy}
|
|
365
|
+
busy={action === 'vibing'}
|
|
366
|
+
/>
|
|
367
|
+
|
|
368
|
+
{/* 4. Start/Stop Recording */}
|
|
369
|
+
<ActionRow
|
|
370
|
+
label={isRecordingVideo ? 'Stop Recording' : 'Start Recording'}
|
|
371
|
+
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
372
|
+
onPress={handleToggleRecording}
|
|
373
|
+
disabled={busy && action !== 'idle' && !isRecordingVideo}
|
|
374
|
+
/>
|
|
375
|
+
|
|
376
|
+
{/* 5. Send Video (only tappable when a clip is ready) */}
|
|
377
|
+
<ActionRow
|
|
378
|
+
label={
|
|
379
|
+
action === 'sending-video'
|
|
380
|
+
? 'Sending…'
|
|
381
|
+
: lastVideo
|
|
382
|
+
? `Send Video · ${Math.round(lastVideo.duration)}s`
|
|
383
|
+
: 'Send Video'
|
|
384
|
+
}
|
|
385
|
+
tint="#a78bfa"
|
|
386
|
+
onPress={handleSendVideo}
|
|
387
|
+
disabled={busy || !lastVideo}
|
|
388
|
+
busy={action === 'sending-video'}
|
|
389
|
+
/>
|
|
390
|
+
|
|
391
|
+
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
392
|
+
{error && <Text style={styles.error}>{error}</Text>}
|
|
393
|
+
</Pressable>
|
|
394
|
+
</Pressable>
|
|
395
|
+
</Modal>
|
|
482
396
|
)}
|
|
483
397
|
</>
|
|
484
398
|
);
|
|
485
399
|
};
|
|
486
400
|
|
|
401
|
+
interface ActionRowProps {
|
|
402
|
+
label: string;
|
|
403
|
+
tint: string;
|
|
404
|
+
onPress: () => void;
|
|
405
|
+
disabled?: boolean;
|
|
406
|
+
busy?: boolean;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const ActionRow: React.FC<ActionRowProps> = ({
|
|
410
|
+
label,
|
|
411
|
+
tint,
|
|
412
|
+
onPress,
|
|
413
|
+
disabled,
|
|
414
|
+
busy,
|
|
415
|
+
}) => (
|
|
416
|
+
<Pressable
|
|
417
|
+
onPress={onPress}
|
|
418
|
+
disabled={disabled}
|
|
419
|
+
style={({ pressed }) => [
|
|
420
|
+
styles.actionBtn,
|
|
421
|
+
{
|
|
422
|
+
borderColor: tint + '66',
|
|
423
|
+
backgroundColor: tint + '1f',
|
|
424
|
+
},
|
|
425
|
+
disabled && styles.actionBtnDisabled,
|
|
426
|
+
pressed && !disabled && { opacity: 0.7 },
|
|
427
|
+
]}
|
|
428
|
+
accessibilityRole="button"
|
|
429
|
+
accessibilityLabel={label}
|
|
430
|
+
>
|
|
431
|
+
{busy ? (
|
|
432
|
+
<ActivityIndicator color={tint} size="small" />
|
|
433
|
+
) : (
|
|
434
|
+
<Text style={[styles.actionText, { color: tint }]}>{label}</Text>
|
|
435
|
+
)}
|
|
436
|
+
</Pressable>
|
|
437
|
+
);
|
|
438
|
+
|
|
487
439
|
const styles = StyleSheet.create({
|
|
488
440
|
overlay: {
|
|
489
441
|
flex: 1,
|
|
490
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
442
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
491
443
|
justifyContent: 'flex-end',
|
|
492
444
|
},
|
|
493
445
|
modal: {
|
|
494
|
-
backgroundColor: '#
|
|
495
|
-
borderTopLeftRadius:
|
|
496
|
-
borderTopRightRadius:
|
|
497
|
-
padding:
|
|
498
|
-
paddingBottom:
|
|
499
|
-
|
|
446
|
+
backgroundColor: '#141422',
|
|
447
|
+
borderTopLeftRadius: 22,
|
|
448
|
+
borderTopRightRadius: 22,
|
|
449
|
+
padding: 22,
|
|
450
|
+
paddingBottom: 36,
|
|
451
|
+
gap: 12,
|
|
452
|
+
},
|
|
453
|
+
header: {
|
|
454
|
+
flexDirection: 'row',
|
|
455
|
+
alignItems: 'center',
|
|
456
|
+
justifyContent: 'space-between',
|
|
457
|
+
marginBottom: 6,
|
|
500
458
|
},
|
|
501
459
|
title: {
|
|
502
460
|
fontSize: 20,
|
|
503
461
|
fontWeight: '700',
|
|
504
462
|
color: '#fff',
|
|
505
|
-
marginBottom: 12,
|
|
506
|
-
},
|
|
507
|
-
modeSelector: {
|
|
508
|
-
flexDirection: 'row',
|
|
509
|
-
gap: 8,
|
|
510
|
-
marginBottom: 16,
|
|
511
463
|
},
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
borderRadius:
|
|
464
|
+
closeBtn: {
|
|
465
|
+
width: 36,
|
|
466
|
+
height: 36,
|
|
467
|
+
borderRadius: 18,
|
|
516
468
|
alignItems: 'center',
|
|
469
|
+
justifyContent: 'center',
|
|
517
470
|
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
471
|
},
|
|
530
|
-
|
|
531
|
-
color: '#
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
marginBottom: 12,
|
|
536
|
-
},
|
|
537
|
-
commentaryBubble: {
|
|
538
|
-
backgroundColor: 'rgba(99,102,241,0.15)',
|
|
539
|
-
borderRadius: 10,
|
|
540
|
-
padding: 10,
|
|
541
|
-
marginBottom: 6,
|
|
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,
|
|
472
|
+
closeIcon: {
|
|
473
|
+
color: '#fff',
|
|
474
|
+
fontSize: 22,
|
|
475
|
+
lineHeight: 24,
|
|
476
|
+
fontWeight: '400',
|
|
560
477
|
},
|
|
561
|
-
|
|
478
|
+
actionBtn: {
|
|
479
|
+
paddingVertical: 16,
|
|
480
|
+
borderRadius: 14,
|
|
562
481
|
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)',
|
|
482
|
+
justifyContent: 'center',
|
|
591
483
|
borderWidth: 1,
|
|
592
|
-
borderColor: 'rgba(99,102,241,0.4)',
|
|
593
|
-
borderRadius: 12,
|
|
594
|
-
paddingVertical: 14,
|
|
595
|
-
alignItems: 'center',
|
|
596
484
|
},
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
borderColor: 'rgba(239,68,68,0.6)',
|
|
485
|
+
actionBtnDisabled: {
|
|
486
|
+
opacity: 0.35,
|
|
600
487
|
},
|
|
601
488
|
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',
|
|
489
|
+
fontSize: 15,
|
|
490
|
+
fontWeight: '700',
|
|
618
491
|
},
|
|
619
|
-
|
|
492
|
+
toast: {
|
|
620
493
|
color: '#22c55e',
|
|
621
|
-
fontSize:
|
|
622
|
-
|
|
494
|
+
fontSize: 13,
|
|
495
|
+
textAlign: 'center',
|
|
496
|
+
marginTop: 4,
|
|
623
497
|
},
|
|
624
498
|
error: {
|
|
625
499
|
color: '#ef4444',
|
|
626
500
|
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,
|
|
501
|
+
textAlign: 'center',
|
|
502
|
+
marginTop: 4,
|
|
684
503
|
},
|
|
685
504
|
});
|