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