yaver-feedback-react-native 0.8.0 → 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 +211 -203
- package/dist/P2PClient.js +7 -0
- package/dist/QuickActionIcon.d.ts +10 -2
- package/dist/QuickActionIcon.js +37 -12
- package/dist/YaverFeedback.js +2 -0
- package/dist/capture.d.ts +11 -0
- package/dist/capture.js +59 -0
- package/dist/index.d.ts +9 -7
- package/dist/index.js +10 -7
- package/dist/types.d.ts +29 -4
- package/dist/upload.d.ts +1 -0
- package/dist/upload.js +8 -0
- package/package.json +6 -2
- package/src/FeedbackModal.tsx +257 -243
- package/src/P2PClient.ts +8 -0
- package/src/QuickActionIcon.tsx +55 -12
- package/src/YaverFeedback.ts +2 -0
- package/src/capture.ts +74 -0
- package/src/index.ts +9 -6
- package/src/types.ts +29 -4
- package/src/upload.ts +9 -0
package/src/FeedbackModal.tsx
CHANGED
|
@@ -13,11 +13,9 @@ 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';
|
|
@@ -25,20 +23,17 @@ import { AuthOverlay } from './AuthOverlay';
|
|
|
25
23
|
import { QuickActionIcon } from './QuickActionIcon';
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
|
-
* Simplified feedback modal —
|
|
26
|
+
* Simplified feedback modal — 4 actions:
|
|
29
27
|
*
|
|
30
28
|
* 1. Hot Reload — instant JS reload (most common use case)
|
|
31
|
-
* 2.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 4.
|
|
36
|
-
* 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
|
|
37
34
|
*
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
40
|
-
* were removed in 0.7.0 — those flows never worked end-to-end against
|
|
41
|
-
* 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.
|
|
42
37
|
*/
|
|
43
38
|
|
|
44
39
|
interface LastVideo {
|
|
@@ -51,9 +46,7 @@ type ActionState =
|
|
|
51
46
|
| 'hot-reloading'
|
|
52
47
|
| 'capturing'
|
|
53
48
|
| 'vibing'
|
|
54
|
-
| '
|
|
55
|
-
| 'recording-voice'
|
|
56
|
-
| 'transcribing-voice';
|
|
49
|
+
| 'uploading-video';
|
|
57
50
|
|
|
58
51
|
export const FeedbackModal: React.FC = () => {
|
|
59
52
|
const [visible, setVisible] = useState(false);
|
|
@@ -62,10 +55,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
62
55
|
const [toast, setToast] = useState<string | null>(null);
|
|
63
56
|
const [progress, setProgress] = useState<number | null>(null);
|
|
64
57
|
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
|
|
65
|
-
const [isRecordingVoice, setIsRecordingVoice] = useState(false);
|
|
66
|
-
// Cached once on mount — bare-RN apps without expo-av get a clean
|
|
67
|
-
// hidden button instead of a runtime error.
|
|
68
|
-
const voiceSupported = useRef<boolean>(isVoiceCaptureSupported()).current;
|
|
69
58
|
const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
|
|
70
59
|
// Tracks whether the user has hidden the QuickActionIcon via its
|
|
71
60
|
// long-press menu. Shake is always available, so the feedback modal
|
|
@@ -79,6 +68,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
79
68
|
// the wrong project because the matcher grepped the prompt itself).
|
|
80
69
|
const [showVibeInput, setShowVibeInput] = useState(false);
|
|
81
70
|
const [vibePrompt, setVibePrompt] = useState('');
|
|
71
|
+
const [showCaptureChoices, setShowCaptureChoices] = useState(false);
|
|
82
72
|
const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
|
|
83
73
|
const mountedRef = useRef(true);
|
|
84
74
|
|
|
@@ -90,6 +80,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
90
80
|
setError(null);
|
|
91
81
|
setToast(null);
|
|
92
82
|
setAction('idle');
|
|
83
|
+
setShowCaptureChoices(false);
|
|
93
84
|
// Re-read the "user hid the quick icon" flag on every open so
|
|
94
85
|
// the re-enable row reflects the latest preference (the user
|
|
95
86
|
// might have hidden or shown it between opens).
|
|
@@ -139,6 +130,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
139
130
|
setError(null);
|
|
140
131
|
setToast(null);
|
|
141
132
|
setAction('idle');
|
|
133
|
+
setShowCaptureChoices(false);
|
|
142
134
|
}, []);
|
|
143
135
|
|
|
144
136
|
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
@@ -219,28 +211,61 @@ export const FeedbackModal: React.FC = () => {
|
|
|
219
211
|
}
|
|
220
212
|
}, [closeSoon, runWithReconnect]);
|
|
221
213
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const handleScreenshotAndFix = useCallback(async () => {
|
|
214
|
+
const uploadBundleWithOptionalFix = useCallback(async (
|
|
215
|
+
bundle: FeedbackBundle,
|
|
216
|
+
fixOnUpload: boolean,
|
|
217
|
+
successToast: string,
|
|
218
|
+
failureToast?: string,
|
|
219
|
+
) => {
|
|
229
220
|
const client = YaverFeedback.getP2PClient();
|
|
230
221
|
const config = YaverFeedback.getConfig();
|
|
231
222
|
if (!client || !config?.agentUrl) {
|
|
232
223
|
setError('Not connected to the agent yet.');
|
|
233
224
|
return;
|
|
234
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 () => {
|
|
235
261
|
setAction('capturing');
|
|
236
262
|
setError(null);
|
|
263
|
+
setShowCaptureChoices(false);
|
|
237
264
|
|
|
238
|
-
// Step 1: Hide the modal so the screenshot contains the real screen.
|
|
239
265
|
setVisible(false);
|
|
240
|
-
// Wait out the slide-down animation on both platforms.
|
|
241
266
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
242
267
|
|
|
243
|
-
let path: string
|
|
268
|
+
let path: string;
|
|
244
269
|
try {
|
|
245
270
|
path = await captureScreenshot();
|
|
246
271
|
} catch (err: unknown) {
|
|
@@ -250,7 +275,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
250
275
|
return;
|
|
251
276
|
}
|
|
252
277
|
|
|
253
|
-
// Step 2: Re-show the modal for progress + ack.
|
|
254
278
|
setVisible(true);
|
|
255
279
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
256
280
|
|
|
@@ -264,7 +288,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
264
288
|
screenWidth: width,
|
|
265
289
|
screenHeight: height,
|
|
266
290
|
};
|
|
267
|
-
|
|
268
291
|
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
269
292
|
const bundle: FeedbackBundle = {
|
|
270
293
|
metadata: {
|
|
@@ -276,36 +299,61 @@ export const FeedbackModal: React.FC = () => {
|
|
|
276
299
|
screenshots: [path],
|
|
277
300
|
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
278
301
|
};
|
|
279
|
-
|
|
280
|
-
const uploaded = await uploadFeedback(
|
|
281
|
-
config.agentUrl,
|
|
282
|
-
config.authToken ?? '',
|
|
302
|
+
await uploadBundleWithOptionalFix(
|
|
283
303
|
bundle,
|
|
304
|
+
true,
|
|
305
|
+
'Fix task started',
|
|
284
306
|
);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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.');
|
|
301
342
|
}
|
|
302
|
-
|
|
343
|
+
await uploadBundleWithOptionalFix(
|
|
344
|
+
bundle,
|
|
345
|
+
picked.kind === 'image',
|
|
346
|
+
picked.kind === 'image' ? 'Fix task started' : 'File uploaded',
|
|
347
|
+
);
|
|
303
348
|
} catch (err: unknown) {
|
|
304
|
-
|
|
349
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
350
|
+
if (message !== 'File selection canceled.') {
|
|
351
|
+
setError(message);
|
|
352
|
+
}
|
|
305
353
|
} finally {
|
|
306
354
|
if (mountedRef.current) setAction('idle');
|
|
307
355
|
}
|
|
308
|
-
}, [
|
|
356
|
+
}, [uploadBundleWithOptionalFix]);
|
|
309
357
|
|
|
310
358
|
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
311
359
|
// First tap expands the input; second submit fires the actual
|
|
@@ -359,172 +407,113 @@ export const FeedbackModal: React.FC = () => {
|
|
|
359
407
|
}
|
|
360
408
|
}, [vibePrompt]);
|
|
361
409
|
|
|
362
|
-
// ─── 4.
|
|
363
|
-
const
|
|
410
|
+
// ─── 4. Screen recording ───────────────────────────────────────────
|
|
411
|
+
const handleScreenRecording = useCallback(async () => {
|
|
364
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
|
+
|
|
365
457
|
if (isRecordingVideo) {
|
|
366
458
|
try {
|
|
367
459
|
const result = await stopVideoRecording();
|
|
368
460
|
if (mountedRef.current) {
|
|
369
461
|
setIsRecordingVideo(false);
|
|
370
462
|
setLastVideo(result);
|
|
371
|
-
|
|
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.');
|
|
372
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);
|
|
373
498
|
} catch (err: unknown) {
|
|
374
499
|
setIsRecordingVideo(false);
|
|
375
500
|
setError(err instanceof Error ? err.message : String(err));
|
|
501
|
+
} finally {
|
|
502
|
+
if (mountedRef.current) setAction('idle');
|
|
376
503
|
}
|
|
377
504
|
} else {
|
|
378
505
|
try {
|
|
379
506
|
await startVideoRecording();
|
|
380
507
|
if (mountedRef.current) {
|
|
381
508
|
setIsRecordingVideo(true);
|
|
382
|
-
setToast('Recording…');
|
|
509
|
+
setToast('Recording… tap again to stop and upload');
|
|
383
510
|
setLastVideo(null);
|
|
384
511
|
}
|
|
385
512
|
} catch (err: unknown) {
|
|
386
513
|
setError(err instanceof Error ? err.message : String(err));
|
|
387
514
|
}
|
|
388
515
|
}
|
|
389
|
-
}, [isRecordingVideo]);
|
|
390
|
-
|
|
391
|
-
// ─── Voice note: record → transcribe → send as feedback ───────────
|
|
392
|
-
// Tap once to start; tap again to stop. On stop: upload the audio
|
|
393
|
-
// to the agent's /voice/transcribe (which routes through whichever
|
|
394
|
-
// STT provider is configured — Whisper / Deepgram / OpenAI / etc.),
|
|
395
|
-
// then file the transcript as a bug report. The audio file itself
|
|
396
|
-
// is also attached to the feedback bundle so the agent can re-play
|
|
397
|
-
// it if the transcript is wrong.
|
|
398
|
-
const handleToggleVoice = useCallback(async () => {
|
|
399
|
-
setError(null);
|
|
400
|
-
if (!isRecordingVoice) {
|
|
401
|
-
try {
|
|
402
|
-
await startAudioRecording();
|
|
403
|
-
if (mountedRef.current) {
|
|
404
|
-
setIsRecordingVoice(true);
|
|
405
|
-
setAction('recording-voice');
|
|
406
|
-
setToast('Recording voice note…');
|
|
407
|
-
}
|
|
408
|
-
} catch (err: unknown) {
|
|
409
|
-
setIsRecordingVoice(false);
|
|
410
|
-
setAction('idle');
|
|
411
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
412
|
-
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Stopping → transcribe → send.
|
|
417
|
-
try {
|
|
418
|
-
const audio = await stopAudioRecording();
|
|
419
|
-
setIsRecordingVoice(false);
|
|
420
|
-
if (!audio) {
|
|
421
|
-
setAction('idle');
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
setAction('transcribing-voice');
|
|
425
|
-
setToast('Transcribing…');
|
|
426
|
-
|
|
427
|
-
const config = YaverFeedback.getConfig();
|
|
428
|
-
if (!config?.agentUrl) {
|
|
429
|
-
setError('Not connected to the agent yet.');
|
|
430
|
-
setAction('idle');
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
let transcript = '';
|
|
434
|
-
try {
|
|
435
|
-
const client = YaverFeedback.getP2PClient();
|
|
436
|
-
if (client) {
|
|
437
|
-
const res = await client.transcribeVoice(audio.path);
|
|
438
|
-
transcript = res.text ?? '';
|
|
439
|
-
}
|
|
440
|
-
} catch {
|
|
441
|
-
// Transcription can fail (no STT provider configured on the
|
|
442
|
-
// agent, network blip, etc.). Don't block the flow — ship
|
|
443
|
-
// the raw audio file with a "[no transcript]" note so the
|
|
444
|
-
// agent + human reviewer can still play it back.
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const { Dimensions } = require('react-native');
|
|
448
|
-
const { width, height } = Dimensions.get('window');
|
|
449
|
-
const deviceInfo: DeviceInfo = {
|
|
450
|
-
platform: Platform.OS,
|
|
451
|
-
osVersion: String(Platform.Version),
|
|
452
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
453
|
-
screenWidth: width,
|
|
454
|
-
screenHeight: height,
|
|
455
|
-
};
|
|
456
|
-
const bundle: FeedbackBundle = {
|
|
457
|
-
metadata: {
|
|
458
|
-
timestamp: new Date().toISOString(),
|
|
459
|
-
device: deviceInfo,
|
|
460
|
-
app: {},
|
|
461
|
-
userNote:
|
|
462
|
-
transcript.length > 0
|
|
463
|
-
? `[Voice note] ${transcript}`
|
|
464
|
-
: `[Voice note · ${Math.round(audio.duration)}s — transcription unavailable]`,
|
|
465
|
-
},
|
|
466
|
-
screenshots: [],
|
|
467
|
-
audio: audio.path,
|
|
468
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
469
|
-
? YaverFeedback.getCapturedErrors()
|
|
470
|
-
: undefined,
|
|
471
|
-
};
|
|
472
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
473
|
-
setToast(transcript ? `Sent: "${transcript.slice(0, 60)}${transcript.length > 60 ? '…' : ''}"` : 'Voice note sent');
|
|
474
|
-
closeSoon(1800);
|
|
475
|
-
} catch (err: unknown) {
|
|
476
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
477
|
-
} finally {
|
|
478
|
-
if (mountedRef.current) setAction('idle');
|
|
479
|
-
}
|
|
480
|
-
}, [isRecordingVoice, closeSoon]);
|
|
481
|
-
|
|
482
|
-
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
483
|
-
const handleSendVideo = useCallback(async () => {
|
|
484
|
-
const config = YaverFeedback.getConfig();
|
|
485
|
-
if (!config?.agentUrl) {
|
|
486
|
-
setError('Not connected to the agent yet.');
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (!lastVideo) {
|
|
490
|
-
setError('No video recorded yet.');
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
setAction('sending-video');
|
|
494
|
-
setError(null);
|
|
495
|
-
try {
|
|
496
|
-
const { Dimensions } = require('react-native');
|
|
497
|
-
const { width, height } = Dimensions.get('window');
|
|
498
|
-
const deviceInfo: DeviceInfo = {
|
|
499
|
-
platform: Platform.OS,
|
|
500
|
-
osVersion: String(Platform.Version),
|
|
501
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
502
|
-
screenWidth: width,
|
|
503
|
-
screenHeight: height,
|
|
504
|
-
};
|
|
505
|
-
const bundle: FeedbackBundle = {
|
|
506
|
-
metadata: {
|
|
507
|
-
timestamp: new Date().toISOString(),
|
|
508
|
-
device: deviceInfo,
|
|
509
|
-
app: {},
|
|
510
|
-
userNote: '[Screen recording]',
|
|
511
|
-
},
|
|
512
|
-
screenshots: [],
|
|
513
|
-
video: lastVideo.path,
|
|
514
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
515
|
-
? YaverFeedback.getCapturedErrors()
|
|
516
|
-
: undefined,
|
|
517
|
-
};
|
|
518
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
519
|
-
setToast('Video sent');
|
|
520
|
-
setLastVideo(null);
|
|
521
|
-
closeSoon(1200);
|
|
522
|
-
} catch (err: unknown) {
|
|
523
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
524
|
-
} finally {
|
|
525
|
-
if (mountedRef.current) setAction('idle');
|
|
526
|
-
}
|
|
527
|
-
}, [lastVideo, closeSoon]);
|
|
516
|
+
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
528
517
|
|
|
529
518
|
const busy = action !== 'idle';
|
|
530
519
|
|
|
@@ -565,19 +554,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
565
554
|
busy={action === 'hot-reloading'}
|
|
566
555
|
/>
|
|
567
556
|
|
|
568
|
-
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
569
|
-
<ActionRow
|
|
570
|
-
label={
|
|
571
|
-
action === 'capturing'
|
|
572
|
-
? 'Capturing…'
|
|
573
|
-
: 'Screenshot & Fix'
|
|
574
|
-
}
|
|
575
|
-
tint="#22c55e"
|
|
576
|
-
onPress={handleScreenshotAndFix}
|
|
577
|
-
disabled={busy}
|
|
578
|
-
busy={action === 'capturing'}
|
|
579
|
-
/>
|
|
580
|
-
|
|
581
557
|
{/* 3. Vibing — expands to an input box on first tap
|
|
582
558
|
so the user says WHAT they want to vibe on, just
|
|
583
559
|
like the Yaver mobile app's Vibing tab. Second
|
|
@@ -637,46 +613,51 @@ export const FeedbackModal: React.FC = () => {
|
|
|
637
613
|
</Text>
|
|
638
614
|
)}
|
|
639
615
|
|
|
640
|
-
{/*
|
|
641
|
-
|
|
642
|
-
the agent and files as a feedback report. */}
|
|
643
|
-
{voiceSupported && (
|
|
616
|
+
{/* Screenshot / Upload */}
|
|
617
|
+
{!showCaptureChoices ? (
|
|
644
618
|
<ActionRow
|
|
645
619
|
label={
|
|
646
|
-
action === '
|
|
647
|
-
? '
|
|
648
|
-
:
|
|
649
|
-
? 'Stop & Send Voice'
|
|
650
|
-
: 'Voice Note'
|
|
620
|
+
action === 'capturing'
|
|
621
|
+
? 'Working…'
|
|
622
|
+
: 'Screenshot / Upload'
|
|
651
623
|
}
|
|
652
|
-
tint=
|
|
653
|
-
onPress={
|
|
654
|
-
disabled={busy
|
|
655
|
-
busy={action === '
|
|
624
|
+
tint="#22c55e"
|
|
625
|
+
onPress={handleCaptureChoiceToggle}
|
|
626
|
+
disabled={busy}
|
|
627
|
+
busy={action === 'capturing'}
|
|
656
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>
|
|
657
644
|
)}
|
|
658
645
|
|
|
659
|
-
{/* 4.
|
|
660
|
-
<ActionRow
|
|
661
|
-
label={isRecordingVideo ? 'Stop Recording' : 'Start Recording'}
|
|
662
|
-
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
663
|
-
onPress={handleToggleRecording}
|
|
664
|
-
disabled={busy && action !== 'idle' && !isRecordingVideo}
|
|
665
|
-
/>
|
|
666
|
-
|
|
667
|
-
{/* 5. Send Video (only tappable when a clip is ready) */}
|
|
646
|
+
{/* 4. Screen recording */}
|
|
668
647
|
<ActionRow
|
|
669
648
|
label={
|
|
670
|
-
action === '
|
|
671
|
-
? '
|
|
672
|
-
:
|
|
673
|
-
?
|
|
674
|
-
:
|
|
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'
|
|
675
656
|
}
|
|
676
|
-
tint=
|
|
677
|
-
onPress={
|
|
678
|
-
disabled={busy
|
|
679
|
-
busy={action === '
|
|
657
|
+
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
658
|
+
onPress={handleScreenRecording}
|
|
659
|
+
disabled={busy && action !== 'uploading-video' && !isRecordingVideo}
|
|
660
|
+
busy={action === 'uploading-video'}
|
|
680
661
|
/>
|
|
681
662
|
|
|
682
663
|
{progress !== null && (
|
|
@@ -719,6 +700,18 @@ export const FeedbackModal: React.FC = () => {
|
|
|
719
700
|
: '● Hide quick-access icon'}
|
|
720
701
|
</Text>
|
|
721
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>
|
|
722
715
|
</Pressable>
|
|
723
716
|
</Pressable>
|
|
724
717
|
</Modal>
|
|
@@ -868,6 +861,9 @@ const styles = StyleSheet.create({
|
|
|
868
861
|
fontSize: 15,
|
|
869
862
|
fontWeight: '700',
|
|
870
863
|
},
|
|
864
|
+
captureChoices: {
|
|
865
|
+
gap: 10,
|
|
866
|
+
},
|
|
871
867
|
progressTrack: {
|
|
872
868
|
height: 6,
|
|
873
869
|
borderRadius: 3,
|
|
@@ -903,4 +899,22 @@ const styles = StyleSheet.create({
|
|
|
903
899
|
fontSize: 12,
|
|
904
900
|
fontWeight: '500',
|
|
905
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
|
+
},
|
|
906
920
|
});
|
package/src/P2PClient.ts
CHANGED
|
@@ -179,6 +179,14 @@ export class P2PClient {
|
|
|
179
179
|
} as any);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
if (bundle.audio) {
|
|
183
|
+
formData.append('audio', {
|
|
184
|
+
uri: Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
|
|
185
|
+
type: 'audio/m4a',
|
|
186
|
+
name: 'voice_note.m4a',
|
|
187
|
+
} as any);
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
const response = await fetch(`${this.baseUrl}/feedback`, {
|
|
183
191
|
method: 'POST',
|
|
184
192
|
headers: {
|