yaver-feedback-react-native 0.7.17 → 0.8.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/README.md +40 -6
- package/dist/FeedbackModal.js +259 -203
- package/dist/P2PClient.js +7 -0
- package/dist/QuickActionIcon.d.ts +43 -0
- package/dist/QuickActionIcon.js +324 -0
- package/dist/YaverFeedback.d.ts +26 -0
- package/dist/YaverFeedback.js +62 -0
- package/dist/capture.d.ts +11 -0
- package/dist/capture.js +59 -0
- package/dist/index.d.ts +12 -7
- package/dist/index.js +16 -7
- package/dist/preferences.d.ts +18 -0
- package/dist/preferences.js +57 -0
- package/dist/types.d.ts +56 -0
- package/dist/upload.d.ts +1 -0
- package/dist/upload.js +8 -0
- package/package.json +6 -2
- package/src/FeedbackModal.tsx +311 -243
- package/src/P2PClient.ts +8 -0
- package/src/QuickActionIcon.tsx +375 -0
- package/src/YaverFeedback.ts +69 -0
- package/src/capture.ts +74 -0
- package/src/index.ts +16 -6
- package/src/preferences.ts +55 -0
- package/src/types.ts +53 -0
- package/src/upload.ts +9 -0
package/src/FeedbackModal.tsx
CHANGED
|
@@ -13,31 +13,27 @@ import {
|
|
|
13
13
|
import { YaverFeedback } from './YaverFeedback';
|
|
14
14
|
import {
|
|
15
15
|
captureScreenshot,
|
|
16
|
+
pickFeedbackFile,
|
|
16
17
|
startVideoRecording,
|
|
17
18
|
stopVideoRecording,
|
|
18
|
-
startAudioRecording,
|
|
19
|
-
stopAudioRecording,
|
|
20
|
-
isVoiceCaptureSupported,
|
|
21
19
|
} from './capture';
|
|
22
20
|
import { uploadFeedback } from './upload';
|
|
23
21
|
import { DeviceInfo, FeedbackBundle } from './types';
|
|
24
22
|
import { AuthOverlay } from './AuthOverlay';
|
|
23
|
+
import { QuickActionIcon } from './QuickActionIcon';
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
|
-
* Simplified feedback modal —
|
|
26
|
+
* Simplified feedback modal — 4 actions:
|
|
28
27
|
*
|
|
29
28
|
* 1. Hot Reload — instant JS reload (most common use case)
|
|
30
|
-
* 2.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* 4.
|
|
35
|
-
* 5. Send Video — submit the last recorded video
|
|
29
|
+
* 2. Vibing — open a vibing session on the agent
|
|
30
|
+
* 3. Screenshot / Upload — capture the underlying app (modal hidden
|
|
31
|
+
* during capture) or upload an existing
|
|
32
|
+
* media file through the Go agent
|
|
33
|
+
* 4. Screen Recording — start recording, then stop + upload
|
|
36
34
|
*
|
|
37
|
-
* The
|
|
38
|
-
*
|
|
39
|
-
* were removed in 0.7.0 — those flows never worked end-to-end against
|
|
40
|
-
* the Go agent (see MISSINGS_FEEDBACK_SDK.md).
|
|
35
|
+
* The footer also has an explicit Cancel button so the icon tap path
|
|
36
|
+
* feels like a standard action sheet rather than a hidden modal.
|
|
41
37
|
*/
|
|
42
38
|
|
|
43
39
|
interface LastVideo {
|
|
@@ -50,9 +46,7 @@ type ActionState =
|
|
|
50
46
|
| 'hot-reloading'
|
|
51
47
|
| 'capturing'
|
|
52
48
|
| 'vibing'
|
|
53
|
-
| '
|
|
54
|
-
| 'recording-voice'
|
|
55
|
-
| 'transcribing-voice';
|
|
49
|
+
| 'uploading-video';
|
|
56
50
|
|
|
57
51
|
export const FeedbackModal: React.FC = () => {
|
|
58
52
|
const [visible, setVisible] = useState(false);
|
|
@@ -61,11 +55,12 @@ export const FeedbackModal: React.FC = () => {
|
|
|
61
55
|
const [toast, setToast] = useState<string | null>(null);
|
|
62
56
|
const [progress, setProgress] = useState<number | null>(null);
|
|
63
57
|
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
|
|
64
|
-
const [isRecordingVoice, setIsRecordingVoice] = useState(false);
|
|
65
|
-
// Cached once on mount — bare-RN apps without expo-av get a clean
|
|
66
|
-
// hidden button instead of a runtime error.
|
|
67
|
-
const voiceSupported = useRef<boolean>(isVoiceCaptureSupported()).current;
|
|
68
58
|
const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
|
|
59
|
+
// Tracks whether the user has hidden the QuickActionIcon via its
|
|
60
|
+
// long-press menu. Shake is always available, so the feedback modal
|
|
61
|
+
// is our guaranteed UI for bringing the icon back — we surface a
|
|
62
|
+
// small "Show quick icon" row when this is true.
|
|
63
|
+
const [quickIconHidden, setQuickIconHidden] = useState(false);
|
|
69
64
|
// Vibing-input mode: same expand-on-tap pattern as email login.
|
|
70
65
|
// Tap "Vibing" once → the button reveals an input + Send; that lets
|
|
71
66
|
// the user say WHAT they want to vibe on instead of firing a canned
|
|
@@ -73,6 +68,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
73
68
|
// the wrong project because the matcher grepped the prompt itself).
|
|
74
69
|
const [showVibeInput, setShowVibeInput] = useState(false);
|
|
75
70
|
const [vibePrompt, setVibePrompt] = useState('');
|
|
71
|
+
const [showCaptureChoices, setShowCaptureChoices] = useState(false);
|
|
76
72
|
const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
|
|
77
73
|
const mountedRef = useRef(true);
|
|
78
74
|
|
|
@@ -84,6 +80,15 @@ export const FeedbackModal: React.FC = () => {
|
|
|
84
80
|
setError(null);
|
|
85
81
|
setToast(null);
|
|
86
82
|
setAction('idle');
|
|
83
|
+
setShowCaptureChoices(false);
|
|
84
|
+
// Re-read the "user hid the quick icon" flag on every open so
|
|
85
|
+
// the re-enable row reflects the latest preference (the user
|
|
86
|
+
// might have hidden or shown it between opens).
|
|
87
|
+
YaverFeedback.isQuickIconHidden()
|
|
88
|
+
.then((v) => {
|
|
89
|
+
if (mountedRef.current) setQuickIconHidden(v);
|
|
90
|
+
})
|
|
91
|
+
.catch(() => {});
|
|
87
92
|
}
|
|
88
93
|
});
|
|
89
94
|
// Agent streams build / compile progress through the BlackBox
|
|
@@ -125,6 +130,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
125
130
|
setError(null);
|
|
126
131
|
setToast(null);
|
|
127
132
|
setAction('idle');
|
|
133
|
+
setShowCaptureChoices(false);
|
|
128
134
|
}, []);
|
|
129
135
|
|
|
130
136
|
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
@@ -205,28 +211,61 @@ export const FeedbackModal: React.FC = () => {
|
|
|
205
211
|
}
|
|
206
212
|
}, [closeSoon, runWithReconnect]);
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const handleScreenshotAndFix = useCallback(async () => {
|
|
214
|
+
const uploadBundleWithOptionalFix = useCallback(async (
|
|
215
|
+
bundle: FeedbackBundle,
|
|
216
|
+
fixOnUpload: boolean,
|
|
217
|
+
successToast: string,
|
|
218
|
+
failureToast?: string,
|
|
219
|
+
) => {
|
|
215
220
|
const client = YaverFeedback.getP2PClient();
|
|
216
221
|
const config = YaverFeedback.getConfig();
|
|
217
222
|
if (!client || !config?.agentUrl) {
|
|
218
223
|
setError('Not connected to the agent yet.');
|
|
219
224
|
return;
|
|
220
225
|
}
|
|
226
|
+
try {
|
|
227
|
+
const uploaded = await uploadFeedback(
|
|
228
|
+
config.agentUrl,
|
|
229
|
+
config.authToken ?? '',
|
|
230
|
+
bundle,
|
|
231
|
+
);
|
|
232
|
+
// The agent returns the new report id as `id` (see
|
|
233
|
+
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
234
|
+
// one back; otherwise just ack the upload.
|
|
235
|
+
const reportId =
|
|
236
|
+
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
237
|
+
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
238
|
+
if (reportId && fixOnUpload) {
|
|
239
|
+
try {
|
|
240
|
+
await client.triggerFix(reportId);
|
|
241
|
+
setToast(successToast);
|
|
242
|
+
} catch (err: unknown) {
|
|
243
|
+
setToast(failureToast ?? 'Report uploaded — fix trigger failed');
|
|
244
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
setToast(successToast);
|
|
248
|
+
}
|
|
249
|
+
closeSoon(1400);
|
|
250
|
+
} catch (err: unknown) {
|
|
251
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
252
|
+
}
|
|
253
|
+
}, [closeSoon]);
|
|
254
|
+
|
|
255
|
+
// ─── 3. Screenshot / Upload ───────────────────────────────────────
|
|
256
|
+
const handleCaptureChoiceToggle = useCallback(() => {
|
|
257
|
+
setShowCaptureChoices((v) => !v);
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
const handleScreenshotAndFix = useCallback(async () => {
|
|
221
261
|
setAction('capturing');
|
|
222
262
|
setError(null);
|
|
263
|
+
setShowCaptureChoices(false);
|
|
223
264
|
|
|
224
|
-
// Step 1: Hide the modal so the screenshot contains the real screen.
|
|
225
265
|
setVisible(false);
|
|
226
|
-
// Wait out the slide-down animation on both platforms.
|
|
227
266
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
228
267
|
|
|
229
|
-
let path: string
|
|
268
|
+
let path: string;
|
|
230
269
|
try {
|
|
231
270
|
path = await captureScreenshot();
|
|
232
271
|
} catch (err: unknown) {
|
|
@@ -236,7 +275,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
236
275
|
return;
|
|
237
276
|
}
|
|
238
277
|
|
|
239
|
-
// Step 2: Re-show the modal for progress + ack.
|
|
240
278
|
setVisible(true);
|
|
241
279
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
242
280
|
|
|
@@ -250,7 +288,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
250
288
|
screenWidth: width,
|
|
251
289
|
screenHeight: height,
|
|
252
290
|
};
|
|
253
|
-
|
|
254
291
|
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
255
292
|
const bundle: FeedbackBundle = {
|
|
256
293
|
metadata: {
|
|
@@ -262,36 +299,61 @@ export const FeedbackModal: React.FC = () => {
|
|
|
262
299
|
screenshots: [path],
|
|
263
300
|
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
264
301
|
};
|
|
265
|
-
|
|
266
|
-
const uploaded = await uploadFeedback(
|
|
267
|
-
config.agentUrl,
|
|
268
|
-
config.authToken ?? '',
|
|
302
|
+
await uploadBundleWithOptionalFix(
|
|
269
303
|
bundle,
|
|
304
|
+
true,
|
|
305
|
+
'Fix task started',
|
|
270
306
|
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
307
|
+
} finally {
|
|
308
|
+
if (mountedRef.current) setAction('idle');
|
|
309
|
+
}
|
|
310
|
+
}, [uploadBundleWithOptionalFix]);
|
|
311
|
+
|
|
312
|
+
const handleFileUpload = useCallback(async () => {
|
|
313
|
+
setAction('capturing');
|
|
314
|
+
setError(null);
|
|
315
|
+
setShowCaptureChoices(false);
|
|
316
|
+
try {
|
|
317
|
+
const picked = await pickFeedbackFile();
|
|
318
|
+
const { Dimensions } = require('react-native');
|
|
319
|
+
const { width, height } = Dimensions.get('window');
|
|
320
|
+
const deviceInfo: DeviceInfo = {
|
|
321
|
+
platform: Platform.OS,
|
|
322
|
+
osVersion: String(Platform.Version),
|
|
323
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
324
|
+
screenWidth: width,
|
|
325
|
+
screenHeight: height,
|
|
326
|
+
};
|
|
327
|
+
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
328
|
+
const bundle: FeedbackBundle = {
|
|
329
|
+
metadata: {
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
device: deviceInfo,
|
|
332
|
+
app: {},
|
|
333
|
+
userNote: `[Uploaded file] ${picked.name}`,
|
|
334
|
+
},
|
|
335
|
+
screenshots: picked.kind === 'image' ? [picked.path] : [],
|
|
336
|
+
video: picked.kind === 'video' ? picked.path : undefined,
|
|
337
|
+
audio: picked.kind === 'audio' ? picked.path : undefined,
|
|
338
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
339
|
+
};
|
|
340
|
+
if (picked.kind === 'unknown') {
|
|
341
|
+
throw new Error('Pick an image, video, or audio file.');
|
|
287
342
|
}
|
|
288
|
-
|
|
343
|
+
await uploadBundleWithOptionalFix(
|
|
344
|
+
bundle,
|
|
345
|
+
picked.kind === 'image',
|
|
346
|
+
picked.kind === 'image' ? 'Fix task started' : 'File uploaded',
|
|
347
|
+
);
|
|
289
348
|
} catch (err: unknown) {
|
|
290
|
-
|
|
349
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
350
|
+
if (message !== 'File selection canceled.') {
|
|
351
|
+
setError(message);
|
|
352
|
+
}
|
|
291
353
|
} finally {
|
|
292
354
|
if (mountedRef.current) setAction('idle');
|
|
293
355
|
}
|
|
294
|
-
}, [
|
|
356
|
+
}, [uploadBundleWithOptionalFix]);
|
|
295
357
|
|
|
296
358
|
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
297
359
|
// First tap expands the input; second submit fires the actual
|
|
@@ -345,178 +407,120 @@ export const FeedbackModal: React.FC = () => {
|
|
|
345
407
|
}
|
|
346
408
|
}, [vibePrompt]);
|
|
347
409
|
|
|
348
|
-
// ─── 4.
|
|
349
|
-
const
|
|
410
|
+
// ─── 4. Screen recording ───────────────────────────────────────────
|
|
411
|
+
const handleScreenRecording = useCallback(async () => {
|
|
350
412
|
setError(null);
|
|
413
|
+
if (!isRecordingVideo && lastVideo) {
|
|
414
|
+
const config = YaverFeedback.getConfig();
|
|
415
|
+
if (!config?.agentUrl) {
|
|
416
|
+
setError('Not connected to the agent yet.');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
setAction('uploading-video');
|
|
420
|
+
try {
|
|
421
|
+
const { Dimensions } = require('react-native');
|
|
422
|
+
const { width, height } = Dimensions.get('window');
|
|
423
|
+
const deviceInfo: DeviceInfo = {
|
|
424
|
+
platform: Platform.OS,
|
|
425
|
+
osVersion: String(Platform.Version),
|
|
426
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
427
|
+
screenWidth: width,
|
|
428
|
+
screenHeight: height,
|
|
429
|
+
};
|
|
430
|
+
const bundle: FeedbackBundle = {
|
|
431
|
+
metadata: {
|
|
432
|
+
timestamp: new Date().toISOString(),
|
|
433
|
+
device: deviceInfo,
|
|
434
|
+
app: {},
|
|
435
|
+
userNote: '[Screen recording]',
|
|
436
|
+
},
|
|
437
|
+
screenshots: [],
|
|
438
|
+
video: lastVideo.path,
|
|
439
|
+
errors: YaverFeedback.getCapturedErrors().length
|
|
440
|
+
? YaverFeedback.getCapturedErrors()
|
|
441
|
+
: undefined,
|
|
442
|
+
};
|
|
443
|
+
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
444
|
+
if (mountedRef.current) {
|
|
445
|
+
setToast(`Recording uploaded — ${Math.round(lastVideo.duration)}s`);
|
|
446
|
+
setLastVideo(null);
|
|
447
|
+
}
|
|
448
|
+
closeSoon(1200);
|
|
449
|
+
} catch (err: unknown) {
|
|
450
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
451
|
+
} finally {
|
|
452
|
+
if (mountedRef.current) setAction('idle');
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
351
457
|
if (isRecordingVideo) {
|
|
352
458
|
try {
|
|
353
459
|
const result = await stopVideoRecording();
|
|
354
460
|
if (mountedRef.current) {
|
|
355
461
|
setIsRecordingVideo(false);
|
|
356
462
|
setLastVideo(result);
|
|
357
|
-
|
|
463
|
+
setAction('uploading-video');
|
|
464
|
+
setToast('Uploading recording…');
|
|
465
|
+
}
|
|
466
|
+
const config = YaverFeedback.getConfig();
|
|
467
|
+
if (!config?.agentUrl) {
|
|
468
|
+
throw new Error('Not connected to the agent yet.');
|
|
358
469
|
}
|
|
470
|
+
const { Dimensions } = require('react-native');
|
|
471
|
+
const { width, height } = Dimensions.get('window');
|
|
472
|
+
const deviceInfo: DeviceInfo = {
|
|
473
|
+
platform: Platform.OS,
|
|
474
|
+
osVersion: String(Platform.Version),
|
|
475
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
476
|
+
screenWidth: width,
|
|
477
|
+
screenHeight: height,
|
|
478
|
+
};
|
|
479
|
+
const bundle: FeedbackBundle = {
|
|
480
|
+
metadata: {
|
|
481
|
+
timestamp: new Date().toISOString(),
|
|
482
|
+
device: deviceInfo,
|
|
483
|
+
app: {},
|
|
484
|
+
userNote: '[Screen recording]',
|
|
485
|
+
},
|
|
486
|
+
screenshots: [],
|
|
487
|
+
video: result.path,
|
|
488
|
+
errors: YaverFeedback.getCapturedErrors().length
|
|
489
|
+
? YaverFeedback.getCapturedErrors()
|
|
490
|
+
: undefined,
|
|
491
|
+
};
|
|
492
|
+
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
493
|
+
if (mountedRef.current) {
|
|
494
|
+
setToast(`Recording uploaded — ${Math.round(result.duration)}s`);
|
|
495
|
+
setLastVideo(null);
|
|
496
|
+
}
|
|
497
|
+
closeSoon(1200);
|
|
359
498
|
} catch (err: unknown) {
|
|
360
499
|
setIsRecordingVideo(false);
|
|
361
500
|
setError(err instanceof Error ? err.message : String(err));
|
|
501
|
+
} finally {
|
|
502
|
+
if (mountedRef.current) setAction('idle');
|
|
362
503
|
}
|
|
363
504
|
} else {
|
|
364
505
|
try {
|
|
365
506
|
await startVideoRecording();
|
|
366
507
|
if (mountedRef.current) {
|
|
367
508
|
setIsRecordingVideo(true);
|
|
368
|
-
setToast('Recording…');
|
|
509
|
+
setToast('Recording… tap again to stop and upload');
|
|
369
510
|
setLastVideo(null);
|
|
370
511
|
}
|
|
371
512
|
} catch (err: unknown) {
|
|
372
513
|
setError(err instanceof Error ? err.message : String(err));
|
|
373
514
|
}
|
|
374
515
|
}
|
|
375
|
-
}, [isRecordingVideo]);
|
|
376
|
-
|
|
377
|
-
// ─── Voice note: record → transcribe → send as feedback ───────────
|
|
378
|
-
// Tap once to start; tap again to stop. On stop: upload the audio
|
|
379
|
-
// to the agent's /voice/transcribe (which routes through whichever
|
|
380
|
-
// STT provider is configured — Whisper / Deepgram / OpenAI / etc.),
|
|
381
|
-
// then file the transcript as a bug report. The audio file itself
|
|
382
|
-
// is also attached to the feedback bundle so the agent can re-play
|
|
383
|
-
// it if the transcript is wrong.
|
|
384
|
-
const handleToggleVoice = useCallback(async () => {
|
|
385
|
-
setError(null);
|
|
386
|
-
if (!isRecordingVoice) {
|
|
387
|
-
try {
|
|
388
|
-
await startAudioRecording();
|
|
389
|
-
if (mountedRef.current) {
|
|
390
|
-
setIsRecordingVoice(true);
|
|
391
|
-
setAction('recording-voice');
|
|
392
|
-
setToast('Recording voice note…');
|
|
393
|
-
}
|
|
394
|
-
} catch (err: unknown) {
|
|
395
|
-
setIsRecordingVoice(false);
|
|
396
|
-
setAction('idle');
|
|
397
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
398
|
-
}
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Stopping → transcribe → send.
|
|
403
|
-
try {
|
|
404
|
-
const audio = await stopAudioRecording();
|
|
405
|
-
setIsRecordingVoice(false);
|
|
406
|
-
if (!audio) {
|
|
407
|
-
setAction('idle');
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
setAction('transcribing-voice');
|
|
411
|
-
setToast('Transcribing…');
|
|
412
|
-
|
|
413
|
-
const config = YaverFeedback.getConfig();
|
|
414
|
-
if (!config?.agentUrl) {
|
|
415
|
-
setError('Not connected to the agent yet.');
|
|
416
|
-
setAction('idle');
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
let transcript = '';
|
|
420
|
-
try {
|
|
421
|
-
const client = YaverFeedback.getP2PClient();
|
|
422
|
-
if (client) {
|
|
423
|
-
const res = await client.transcribeVoice(audio.path);
|
|
424
|
-
transcript = res.text ?? '';
|
|
425
|
-
}
|
|
426
|
-
} catch {
|
|
427
|
-
// Transcription can fail (no STT provider configured on the
|
|
428
|
-
// agent, network blip, etc.). Don't block the flow — ship
|
|
429
|
-
// the raw audio file with a "[no transcript]" note so the
|
|
430
|
-
// agent + human reviewer can still play it back.
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const { Dimensions } = require('react-native');
|
|
434
|
-
const { width, height } = Dimensions.get('window');
|
|
435
|
-
const deviceInfo: DeviceInfo = {
|
|
436
|
-
platform: Platform.OS,
|
|
437
|
-
osVersion: String(Platform.Version),
|
|
438
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
439
|
-
screenWidth: width,
|
|
440
|
-
screenHeight: height,
|
|
441
|
-
};
|
|
442
|
-
const bundle: FeedbackBundle = {
|
|
443
|
-
metadata: {
|
|
444
|
-
timestamp: new Date().toISOString(),
|
|
445
|
-
device: deviceInfo,
|
|
446
|
-
app: {},
|
|
447
|
-
userNote:
|
|
448
|
-
transcript.length > 0
|
|
449
|
-
? `[Voice note] ${transcript}`
|
|
450
|
-
: `[Voice note · ${Math.round(audio.duration)}s — transcription unavailable]`,
|
|
451
|
-
},
|
|
452
|
-
screenshots: [],
|
|
453
|
-
audio: audio.path,
|
|
454
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
455
|
-
? YaverFeedback.getCapturedErrors()
|
|
456
|
-
: undefined,
|
|
457
|
-
};
|
|
458
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
459
|
-
setToast(transcript ? `Sent: "${transcript.slice(0, 60)}${transcript.length > 60 ? '…' : ''}"` : 'Voice note sent');
|
|
460
|
-
closeSoon(1800);
|
|
461
|
-
} catch (err: unknown) {
|
|
462
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
463
|
-
} finally {
|
|
464
|
-
if (mountedRef.current) setAction('idle');
|
|
465
|
-
}
|
|
466
|
-
}, [isRecordingVoice, closeSoon]);
|
|
467
|
-
|
|
468
|
-
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
469
|
-
const handleSendVideo = useCallback(async () => {
|
|
470
|
-
const config = YaverFeedback.getConfig();
|
|
471
|
-
if (!config?.agentUrl) {
|
|
472
|
-
setError('Not connected to the agent yet.');
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (!lastVideo) {
|
|
476
|
-
setError('No video recorded yet.');
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
setAction('sending-video');
|
|
480
|
-
setError(null);
|
|
481
|
-
try {
|
|
482
|
-
const { Dimensions } = require('react-native');
|
|
483
|
-
const { width, height } = Dimensions.get('window');
|
|
484
|
-
const deviceInfo: DeviceInfo = {
|
|
485
|
-
platform: Platform.OS,
|
|
486
|
-
osVersion: String(Platform.Version),
|
|
487
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
488
|
-
screenWidth: width,
|
|
489
|
-
screenHeight: height,
|
|
490
|
-
};
|
|
491
|
-
const bundle: FeedbackBundle = {
|
|
492
|
-
metadata: {
|
|
493
|
-
timestamp: new Date().toISOString(),
|
|
494
|
-
device: deviceInfo,
|
|
495
|
-
app: {},
|
|
496
|
-
userNote: '[Screen recording]',
|
|
497
|
-
},
|
|
498
|
-
screenshots: [],
|
|
499
|
-
video: lastVideo.path,
|
|
500
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
501
|
-
? YaverFeedback.getCapturedErrors()
|
|
502
|
-
: undefined,
|
|
503
|
-
};
|
|
504
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
505
|
-
setToast('Video sent');
|
|
506
|
-
setLastVideo(null);
|
|
507
|
-
closeSoon(1200);
|
|
508
|
-
} catch (err: unknown) {
|
|
509
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
510
|
-
} finally {
|
|
511
|
-
if (mountedRef.current) setAction('idle');
|
|
512
|
-
}
|
|
513
|
-
}, [lastVideo, closeSoon]);
|
|
516
|
+
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
514
517
|
|
|
515
518
|
const busy = action !== 'idle';
|
|
516
519
|
|
|
517
520
|
return (
|
|
518
521
|
<>
|
|
519
522
|
<AuthOverlay />
|
|
523
|
+
<QuickActionIcon />
|
|
520
524
|
{visible && (
|
|
521
525
|
<Modal
|
|
522
526
|
visible={visible}
|
|
@@ -550,19 +554,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
550
554
|
busy={action === 'hot-reloading'}
|
|
551
555
|
/>
|
|
552
556
|
|
|
553
|
-
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
554
|
-
<ActionRow
|
|
555
|
-
label={
|
|
556
|
-
action === 'capturing'
|
|
557
|
-
? 'Capturing…'
|
|
558
|
-
: 'Screenshot & Fix'
|
|
559
|
-
}
|
|
560
|
-
tint="#22c55e"
|
|
561
|
-
onPress={handleScreenshotAndFix}
|
|
562
|
-
disabled={busy}
|
|
563
|
-
busy={action === 'capturing'}
|
|
564
|
-
/>
|
|
565
|
-
|
|
566
557
|
{/* 3. Vibing — expands to an input box on first tap
|
|
567
558
|
so the user says WHAT they want to vibe on, just
|
|
568
559
|
like the Yaver mobile app's Vibing tab. Second
|
|
@@ -622,46 +613,51 @@ export const FeedbackModal: React.FC = () => {
|
|
|
622
613
|
</Text>
|
|
623
614
|
)}
|
|
624
615
|
|
|
625
|
-
{/*
|
|
626
|
-
|
|
627
|
-
the agent and files as a feedback report. */}
|
|
628
|
-
{voiceSupported && (
|
|
616
|
+
{/* Screenshot / Upload */}
|
|
617
|
+
{!showCaptureChoices ? (
|
|
629
618
|
<ActionRow
|
|
630
619
|
label={
|
|
631
|
-
action === '
|
|
632
|
-
? '
|
|
633
|
-
:
|
|
634
|
-
? 'Stop & Send Voice'
|
|
635
|
-
: 'Voice Note'
|
|
620
|
+
action === 'capturing'
|
|
621
|
+
? 'Working…'
|
|
622
|
+
: 'Screenshot / Upload'
|
|
636
623
|
}
|
|
637
|
-
tint=
|
|
638
|
-
onPress={
|
|
639
|
-
disabled={busy
|
|
640
|
-
busy={action === '
|
|
624
|
+
tint="#22c55e"
|
|
625
|
+
onPress={handleCaptureChoiceToggle}
|
|
626
|
+
disabled={busy}
|
|
627
|
+
busy={action === 'capturing'}
|
|
641
628
|
/>
|
|
629
|
+
) : (
|
|
630
|
+
<View style={styles.captureChoices}>
|
|
631
|
+
<ActionRow
|
|
632
|
+
label="Take Screenshot"
|
|
633
|
+
tint="#22c55e"
|
|
634
|
+
onPress={handleScreenshotAndFix}
|
|
635
|
+
disabled={busy}
|
|
636
|
+
/>
|
|
637
|
+
<ActionRow
|
|
638
|
+
label="Upload File"
|
|
639
|
+
tint="#34d399"
|
|
640
|
+
onPress={handleFileUpload}
|
|
641
|
+
disabled={busy}
|
|
642
|
+
/>
|
|
643
|
+
</View>
|
|
642
644
|
)}
|
|
643
645
|
|
|
644
|
-
{/* 4.
|
|
645
|
-
<ActionRow
|
|
646
|
-
label={isRecordingVideo ? 'Stop Recording' : 'Start Recording'}
|
|
647
|
-
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
648
|
-
onPress={handleToggleRecording}
|
|
649
|
-
disabled={busy && action !== 'idle' && !isRecordingVideo}
|
|
650
|
-
/>
|
|
651
|
-
|
|
652
|
-
{/* 5. Send Video (only tappable when a clip is ready) */}
|
|
646
|
+
{/* 4. Screen recording */}
|
|
653
647
|
<ActionRow
|
|
654
648
|
label={
|
|
655
|
-
action === '
|
|
656
|
-
? '
|
|
657
|
-
:
|
|
658
|
-
?
|
|
659
|
-
:
|
|
649
|
+
action === 'uploading-video'
|
|
650
|
+
? 'Uploading…'
|
|
651
|
+
: isRecordingVideo
|
|
652
|
+
? 'Stop & Upload Recording'
|
|
653
|
+
: lastVideo
|
|
654
|
+
? `Retry Upload Recording · ${Math.round(lastVideo.duration)}s`
|
|
655
|
+
: 'Screen Recording'
|
|
660
656
|
}
|
|
661
|
-
tint=
|
|
662
|
-
onPress={
|
|
663
|
-
disabled={busy
|
|
664
|
-
busy={action === '
|
|
657
|
+
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
658
|
+
onPress={handleScreenRecording}
|
|
659
|
+
disabled={busy && action !== 'uploading-video' && !isRecordingVideo}
|
|
660
|
+
busy={action === 'uploading-video'}
|
|
665
661
|
/>
|
|
666
662
|
|
|
667
663
|
{progress !== null && (
|
|
@@ -676,6 +672,46 @@ export const FeedbackModal: React.FC = () => {
|
|
|
676
672
|
)}
|
|
677
673
|
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
678
674
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
675
|
+
|
|
676
|
+
{/* Quick-icon toggle. The user's three ways to control
|
|
677
|
+
the floating icon are: (1) long-press the icon →
|
|
678
|
+
Hide, (2) tap this row to toggle it on/off, (3) shake
|
|
679
|
+
→ this modal → tap this row. Shake is the unkillable
|
|
680
|
+
back-door when the icon is hidden and the dev hasn't
|
|
681
|
+
exposed their own settings UI. */}
|
|
682
|
+
<Pressable
|
|
683
|
+
onPress={async () => {
|
|
684
|
+
const next = !quickIconHidden;
|
|
685
|
+
setQuickIconHidden(next);
|
|
686
|
+
await YaverFeedback.setQuickIconVisible(!next);
|
|
687
|
+
}}
|
|
688
|
+
style={({ pressed }) => [
|
|
689
|
+
styles.quickIconToggle,
|
|
690
|
+
pressed && { opacity: 0.7 },
|
|
691
|
+
]}
|
|
692
|
+
accessibilityRole="button"
|
|
693
|
+
accessibilityLabel={
|
|
694
|
+
quickIconHidden ? 'Show quick icon' : 'Hide quick icon'
|
|
695
|
+
}
|
|
696
|
+
>
|
|
697
|
+
<Text style={styles.quickIconToggleText}>
|
|
698
|
+
{quickIconHidden
|
|
699
|
+
? '◯ Show quick-access icon'
|
|
700
|
+
: '● Hide quick-access icon'}
|
|
701
|
+
</Text>
|
|
702
|
+
</Pressable>
|
|
703
|
+
|
|
704
|
+
<Pressable
|
|
705
|
+
onPress={handleClose}
|
|
706
|
+
style={({ pressed }) => [
|
|
707
|
+
styles.cancelBtn,
|
|
708
|
+
pressed && styles.buttonPressed,
|
|
709
|
+
]}
|
|
710
|
+
accessibilityRole="button"
|
|
711
|
+
accessibilityLabel="Cancel"
|
|
712
|
+
>
|
|
713
|
+
<Text style={styles.cancelBtnText}>Cancel</Text>
|
|
714
|
+
</Pressable>
|
|
679
715
|
</Pressable>
|
|
680
716
|
</Pressable>
|
|
681
717
|
</Modal>
|
|
@@ -825,6 +861,9 @@ const styles = StyleSheet.create({
|
|
|
825
861
|
fontSize: 15,
|
|
826
862
|
fontWeight: '700',
|
|
827
863
|
},
|
|
864
|
+
captureChoices: {
|
|
865
|
+
gap: 10,
|
|
866
|
+
},
|
|
828
867
|
progressTrack: {
|
|
829
868
|
height: 6,
|
|
830
869
|
borderRadius: 3,
|
|
@@ -849,4 +888,33 @@ const styles = StyleSheet.create({
|
|
|
849
888
|
textAlign: 'center',
|
|
850
889
|
marginTop: 4,
|
|
851
890
|
},
|
|
891
|
+
quickIconToggle: {
|
|
892
|
+
marginTop: 4,
|
|
893
|
+
alignSelf: 'center',
|
|
894
|
+
paddingVertical: 6,
|
|
895
|
+
paddingHorizontal: 12,
|
|
896
|
+
},
|
|
897
|
+
quickIconToggleText: {
|
|
898
|
+
color: '#9ca3af',
|
|
899
|
+
fontSize: 12,
|
|
900
|
+
fontWeight: '500',
|
|
901
|
+
},
|
|
902
|
+
cancelBtn: {
|
|
903
|
+
marginTop: 4,
|
|
904
|
+
borderRadius: 14,
|
|
905
|
+
borderWidth: 1,
|
|
906
|
+
borderColor: 'rgba(255,255,255,0.1)',
|
|
907
|
+
paddingVertical: 15,
|
|
908
|
+
alignItems: 'center',
|
|
909
|
+
justifyContent: 'center',
|
|
910
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
911
|
+
},
|
|
912
|
+
cancelBtnText: {
|
|
913
|
+
color: '#e5e7eb',
|
|
914
|
+
fontSize: 15,
|
|
915
|
+
fontWeight: '700',
|
|
916
|
+
},
|
|
917
|
+
buttonPressed: {
|
|
918
|
+
opacity: 0.7,
|
|
919
|
+
},
|
|
852
920
|
});
|