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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# yaver-feedback-react-native
|
|
2
2
|
|
|
3
|
-
Visual feedback SDK for Yaver. Lets testers and developers shake their phone
|
|
3
|
+
Visual feedback SDK for Yaver. Lets testers and developers shake their phone and then keep a small quick-access icon on screen for hot reload, vibing, screenshot or file upload, and screen recording uploads straight to a Yaver agent running on a dev machine.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -32,8 +32,8 @@ npm install @react-native-async-storage/async-storage
|
|
|
32
32
|
# Screenshots
|
|
33
33
|
npm install react-native-view-shot
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
|
|
35
|
+
# Optional file upload
|
|
36
|
+
npx expo install expo-document-picker
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
`expo-apple-authentication` is optional — if it's missing, "Continue with Apple" falls through to in-app browser OAuth on Android. iOS hosts that want true native Apple Sign-In must also enable the **Sign in with Apple** capability in Xcode.
|
|
@@ -85,7 +85,41 @@ function App() {
|
|
|
85
85
|
}
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Shake your phone to open the feedback modal.
|
|
88
|
+
Shake your phone to open the feedback modal. On mobile the quick-access icon stays hidden until that first shake, then remains tappable for the rest of the session unless the user hides it. From there the sheet exposes four core actions: hot reload, vibing, screenshot or file upload, and screen recording upload.
|
|
89
|
+
|
|
90
|
+
### Quick Icon Styling
|
|
91
|
+
|
|
92
|
+
The quick icon is configurable at compile time through `YaverFeedback.init(...)`, with render-time overrides still available through `<QuickActionIcon />` props when needed. If you do nothing, the SDK uses a high-visibility orange bubble by default so it stays distinct from the blue/indigo FAB styling many mobile apps already have.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
YaverFeedback.init({
|
|
96
|
+
trigger: 'shake',
|
|
97
|
+
quickIcon: 'after-shake',
|
|
98
|
+
quickIconBackgroundColor: '#0f766e',
|
|
99
|
+
quickIconForegroundColor: '#f8fafc',
|
|
100
|
+
quickIconBorderColor: 'rgba(255,255,255,0.85)',
|
|
101
|
+
quickIconShadowColor: '#000000',
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Available options:
|
|
106
|
+
|
|
107
|
+
- `quickIconBackgroundColor`: bubble fill color.
|
|
108
|
+
- `quickIconForegroundColor`: the `y` label color.
|
|
109
|
+
- `quickIconBorderColor`: outer ring color.
|
|
110
|
+
- `quickIconShadowColor`: shadow tint.
|
|
111
|
+
- `quickIconColor`: deprecated alias for `quickIconBackgroundColor`.
|
|
112
|
+
- `quickIconInitialPosition`: initial top-left position in pixels.
|
|
113
|
+
|
|
114
|
+
If you need a one-off local override instead of global SDK config:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<QuickActionIcon
|
|
118
|
+
backgroundColor="#111827"
|
|
119
|
+
foregroundColor="#f9fafb"
|
|
120
|
+
borderColor="#f59e0b"
|
|
121
|
+
/>
|
|
122
|
+
```
|
|
89
123
|
|
|
90
124
|
### Opt-out: bring-your-own auth (pre-0.5 behavior)
|
|
91
125
|
|
|
@@ -728,7 +762,7 @@ YaverFeedback.setEnabled(false);
|
|
|
728
762
|
- Yaver CLI running on your dev machine (`yaver serve`)
|
|
729
763
|
- Optional: `@react-native-async-storage/async-storage` for device discovery persistence
|
|
730
764
|
- Optional: `react-native-view-shot` for screenshots
|
|
731
|
-
- Optional: `
|
|
765
|
+
- Optional: `expo-document-picker` for file uploads
|
|
732
766
|
|
|
733
767
|
## API Reference
|
|
734
768
|
|
|
@@ -805,7 +839,7 @@ YaverFeedback.setEnabled(false);
|
|
|
805
839
|
|
|
806
840
|
| Component | Description |
|
|
807
841
|
|-----------|-------------|
|
|
808
|
-
| `FeedbackModal` | Modal with
|
|
842
|
+
| `FeedbackModal` | Modal with hot reload, vibing, screenshot or upload, and recording |
|
|
809
843
|
| `FloatingButton` | Draggable overlay button to trigger feedback |
|
|
810
844
|
| `YaverConnectionScreen` | Full-screen device discovery and connection UI |
|
|
811
845
|
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -48,10 +48,6 @@ const FeedbackModal = () => {
|
|
|
48
48
|
const [toast, setToast] = (0, react_1.useState)(null);
|
|
49
49
|
const [progress, setProgress] = (0, react_1.useState)(null);
|
|
50
50
|
const [isRecordingVideo, setIsRecordingVideo] = (0, react_1.useState)(false);
|
|
51
|
-
const [isRecordingVoice, setIsRecordingVoice] = (0, react_1.useState)(false);
|
|
52
|
-
// Cached once on mount — bare-RN apps without expo-av get a clean
|
|
53
|
-
// hidden button instead of a runtime error.
|
|
54
|
-
const voiceSupported = (0, react_1.useRef)((0, capture_1.isVoiceCaptureSupported)()).current;
|
|
55
51
|
const [lastVideo, setLastVideo] = (0, react_1.useState)(null);
|
|
56
52
|
// Tracks whether the user has hidden the QuickActionIcon via its
|
|
57
53
|
// long-press menu. Shake is always available, so the feedback modal
|
|
@@ -65,6 +61,7 @@ const FeedbackModal = () => {
|
|
|
65
61
|
// the wrong project because the matcher grepped the prompt itself).
|
|
66
62
|
const [showVibeInput, setShowVibeInput] = (0, react_1.useState)(false);
|
|
67
63
|
const [vibePrompt, setVibePrompt] = (0, react_1.useState)('');
|
|
64
|
+
const [showCaptureChoices, setShowCaptureChoices] = (0, react_1.useState)(false);
|
|
68
65
|
const [lastVibeTaskId, setLastVibeTaskId] = (0, react_1.useState)(null);
|
|
69
66
|
const mountedRef = (0, react_1.useRef)(true);
|
|
70
67
|
(0, react_1.useEffect)(() => {
|
|
@@ -75,6 +72,7 @@ const FeedbackModal = () => {
|
|
|
75
72
|
setError(null);
|
|
76
73
|
setToast(null);
|
|
77
74
|
setAction('idle');
|
|
75
|
+
setShowCaptureChoices(false);
|
|
78
76
|
// Re-read the "user hid the quick icon" flag on every open so
|
|
79
77
|
// the re-enable row reflects the latest preference (the user
|
|
80
78
|
// might have hidden or shown it between opens).
|
|
@@ -123,6 +121,7 @@ const FeedbackModal = () => {
|
|
|
123
121
|
setError(null);
|
|
124
122
|
setToast(null);
|
|
125
123
|
setAction('idle');
|
|
124
|
+
setShowCaptureChoices(false);
|
|
126
125
|
}, []);
|
|
127
126
|
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
128
127
|
// re-query Convex for the fresh IP and retry once. Solves the common
|
|
@@ -203,26 +202,50 @@ const FeedbackModal = () => {
|
|
|
203
202
|
setAction('idle');
|
|
204
203
|
}
|
|
205
204
|
}, [closeSoon, runWithReconnect]);
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
// Hide the modal first so the screenshot captures the actual screen
|
|
209
|
-
// (the bug) — not the modal card. Await a short animation delay,
|
|
210
|
-
// snapshot, upload the feedback bundle with any buffered errors, then
|
|
211
|
-
// kick `/feedback/{id}/fix` to create the repair task.
|
|
212
|
-
const handleScreenshotAndFix = (0, react_1.useCallback)(async () => {
|
|
205
|
+
const uploadBundleWithOptionalFix = (0, react_1.useCallback)(async (bundle, fixOnUpload, successToast, failureToast) => {
|
|
213
206
|
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
214
207
|
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
215
208
|
if (!client || !config?.agentUrl) {
|
|
216
209
|
setError('Not connected to the agent yet.');
|
|
217
210
|
return;
|
|
218
211
|
}
|
|
212
|
+
try {
|
|
213
|
+
const uploaded = await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
214
|
+
// The agent returns the new report id as `id` (see
|
|
215
|
+
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
216
|
+
// one back; otherwise just ack the upload.
|
|
217
|
+
const reportId = uploaded?.id ??
|
|
218
|
+
uploaded?.reportId;
|
|
219
|
+
if (reportId && fixOnUpload) {
|
|
220
|
+
try {
|
|
221
|
+
await client.triggerFix(reportId);
|
|
222
|
+
setToast(successToast);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
setToast(failureToast ?? 'Report uploaded — fix trigger failed');
|
|
226
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
setToast(successToast);
|
|
231
|
+
}
|
|
232
|
+
closeSoon(1400);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
236
|
+
}
|
|
237
|
+
}, [closeSoon]);
|
|
238
|
+
// ─── 3. Screenshot / Upload ───────────────────────────────────────
|
|
239
|
+
const handleCaptureChoiceToggle = (0, react_1.useCallback)(() => {
|
|
240
|
+
setShowCaptureChoices((v) => !v);
|
|
241
|
+
}, []);
|
|
242
|
+
const handleScreenshotAndFix = (0, react_1.useCallback)(async () => {
|
|
219
243
|
setAction('capturing');
|
|
220
244
|
setError(null);
|
|
221
|
-
|
|
245
|
+
setShowCaptureChoices(false);
|
|
222
246
|
setVisible(false);
|
|
223
|
-
// Wait out the slide-down animation on both platforms.
|
|
224
247
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
225
|
-
let path
|
|
248
|
+
let path;
|
|
226
249
|
try {
|
|
227
250
|
path = await (0, capture_1.captureScreenshot)();
|
|
228
251
|
}
|
|
@@ -232,7 +255,6 @@ const FeedbackModal = () => {
|
|
|
232
255
|
setAction('idle');
|
|
233
256
|
return;
|
|
234
257
|
}
|
|
235
|
-
// Step 2: Re-show the modal for progress + ack.
|
|
236
258
|
setVisible(true);
|
|
237
259
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
238
260
|
try {
|
|
@@ -256,35 +278,57 @@ const FeedbackModal = () => {
|
|
|
256
278
|
screenshots: [path],
|
|
257
279
|
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
258
280
|
};
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
await uploadBundleWithOptionalFix(bundle, true, 'Fix task started');
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
if (mountedRef.current)
|
|
285
|
+
setAction('idle');
|
|
286
|
+
}
|
|
287
|
+
}, [uploadBundleWithOptionalFix]);
|
|
288
|
+
const handleFileUpload = (0, react_1.useCallback)(async () => {
|
|
289
|
+
setAction('capturing');
|
|
290
|
+
setError(null);
|
|
291
|
+
setShowCaptureChoices(false);
|
|
292
|
+
try {
|
|
293
|
+
const picked = await (0, capture_1.pickFeedbackFile)();
|
|
294
|
+
const { Dimensions } = require('react-native');
|
|
295
|
+
const { width, height } = Dimensions.get('window');
|
|
296
|
+
const deviceInfo = {
|
|
297
|
+
platform: react_native_1.Platform.OS,
|
|
298
|
+
osVersion: String(react_native_1.Platform.Version),
|
|
299
|
+
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
300
|
+
screenWidth: width,
|
|
301
|
+
screenHeight: height,
|
|
302
|
+
};
|
|
303
|
+
const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
|
|
304
|
+
const bundle = {
|
|
305
|
+
metadata: {
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
device: deviceInfo,
|
|
308
|
+
app: {},
|
|
309
|
+
userNote: `[Uploaded file] ${picked.name}`,
|
|
310
|
+
},
|
|
311
|
+
screenshots: picked.kind === 'image' ? [picked.path] : [],
|
|
312
|
+
video: picked.kind === 'video' ? picked.path : undefined,
|
|
313
|
+
audio: picked.kind === 'audio' ? picked.path : undefined,
|
|
314
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
315
|
+
};
|
|
316
|
+
if (picked.kind === 'unknown') {
|
|
317
|
+
throw new Error('Pick an image, video, or audio file.');
|
|
277
318
|
}
|
|
278
|
-
|
|
319
|
+
await uploadBundleWithOptionalFix(bundle, picked.kind === 'image', picked.kind === 'image' ? 'Fix task started' : 'File uploaded');
|
|
279
320
|
}
|
|
280
321
|
catch (err) {
|
|
281
|
-
|
|
322
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
323
|
+
if (message !== 'File selection canceled.') {
|
|
324
|
+
setError(message);
|
|
325
|
+
}
|
|
282
326
|
}
|
|
283
327
|
finally {
|
|
284
328
|
if (mountedRef.current)
|
|
285
329
|
setAction('idle');
|
|
286
330
|
}
|
|
287
|
-
}, [
|
|
331
|
+
}, [uploadBundleWithOptionalFix]);
|
|
288
332
|
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
289
333
|
// First tap expands the input; second submit fires the actual
|
|
290
334
|
// /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
|
|
@@ -337,177 +381,120 @@ const FeedbackModal = () => {
|
|
|
337
381
|
setAction('idle');
|
|
338
382
|
}
|
|
339
383
|
}, [vibePrompt]);
|
|
340
|
-
// ─── 4.
|
|
341
|
-
const
|
|
384
|
+
// ─── 4. Screen recording ───────────────────────────────────────────
|
|
385
|
+
const handleScreenRecording = (0, react_1.useCallback)(async () => {
|
|
342
386
|
setError(null);
|
|
343
|
-
if (isRecordingVideo) {
|
|
387
|
+
if (!isRecordingVideo && lastVideo) {
|
|
388
|
+
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
389
|
+
if (!config?.agentUrl) {
|
|
390
|
+
setError('Not connected to the agent yet.');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
setAction('uploading-video');
|
|
344
394
|
try {
|
|
345
|
-
const
|
|
395
|
+
const { Dimensions } = require('react-native');
|
|
396
|
+
const { width, height } = Dimensions.get('window');
|
|
397
|
+
const deviceInfo = {
|
|
398
|
+
platform: react_native_1.Platform.OS,
|
|
399
|
+
osVersion: String(react_native_1.Platform.Version),
|
|
400
|
+
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
401
|
+
screenWidth: width,
|
|
402
|
+
screenHeight: height,
|
|
403
|
+
};
|
|
404
|
+
const bundle = {
|
|
405
|
+
metadata: {
|
|
406
|
+
timestamp: new Date().toISOString(),
|
|
407
|
+
device: deviceInfo,
|
|
408
|
+
app: {},
|
|
409
|
+
userNote: '[Screen recording]',
|
|
410
|
+
},
|
|
411
|
+
screenshots: [],
|
|
412
|
+
video: lastVideo.path,
|
|
413
|
+
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
414
|
+
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
415
|
+
: undefined,
|
|
416
|
+
};
|
|
417
|
+
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
346
418
|
if (mountedRef.current) {
|
|
347
|
-
|
|
348
|
-
setLastVideo(
|
|
349
|
-
setToast(`Recording stopped — ${Math.round(result.duration)}s`);
|
|
419
|
+
setToast(`Recording uploaded — ${Math.round(lastVideo.duration)}s`);
|
|
420
|
+
setLastVideo(null);
|
|
350
421
|
}
|
|
422
|
+
closeSoon(1200);
|
|
351
423
|
}
|
|
352
424
|
catch (err) {
|
|
353
|
-
setIsRecordingVideo(false);
|
|
354
425
|
setError(err instanceof Error ? err.message : String(err));
|
|
355
426
|
}
|
|
427
|
+
finally {
|
|
428
|
+
if (mountedRef.current)
|
|
429
|
+
setAction('idle');
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
356
432
|
}
|
|
357
|
-
|
|
433
|
+
if (isRecordingVideo) {
|
|
358
434
|
try {
|
|
359
|
-
await (0, capture_1.
|
|
435
|
+
const result = await (0, capture_1.stopVideoRecording)();
|
|
360
436
|
if (mountedRef.current) {
|
|
361
|
-
setIsRecordingVideo(
|
|
362
|
-
|
|
437
|
+
setIsRecordingVideo(false);
|
|
438
|
+
setLastVideo(result);
|
|
439
|
+
setAction('uploading-video');
|
|
440
|
+
setToast('Uploading recording…');
|
|
441
|
+
}
|
|
442
|
+
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
443
|
+
if (!config?.agentUrl) {
|
|
444
|
+
throw new Error('Not connected to the agent yet.');
|
|
445
|
+
}
|
|
446
|
+
const { Dimensions } = require('react-native');
|
|
447
|
+
const { width, height } = Dimensions.get('window');
|
|
448
|
+
const deviceInfo = {
|
|
449
|
+
platform: react_native_1.Platform.OS,
|
|
450
|
+
osVersion: String(react_native_1.Platform.Version),
|
|
451
|
+
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
452
|
+
screenWidth: width,
|
|
453
|
+
screenHeight: height,
|
|
454
|
+
};
|
|
455
|
+
const bundle = {
|
|
456
|
+
metadata: {
|
|
457
|
+
timestamp: new Date().toISOString(),
|
|
458
|
+
device: deviceInfo,
|
|
459
|
+
app: {},
|
|
460
|
+
userNote: '[Screen recording]',
|
|
461
|
+
},
|
|
462
|
+
screenshots: [],
|
|
463
|
+
video: result.path,
|
|
464
|
+
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
465
|
+
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
466
|
+
: undefined,
|
|
467
|
+
};
|
|
468
|
+
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
469
|
+
if (mountedRef.current) {
|
|
470
|
+
setToast(`Recording uploaded — ${Math.round(result.duration)}s`);
|
|
363
471
|
setLastVideo(null);
|
|
364
472
|
}
|
|
473
|
+
closeSoon(1200);
|
|
365
474
|
}
|
|
366
475
|
catch (err) {
|
|
476
|
+
setIsRecordingVideo(false);
|
|
367
477
|
setError(err instanceof Error ? err.message : String(err));
|
|
368
478
|
}
|
|
479
|
+
finally {
|
|
480
|
+
if (mountedRef.current)
|
|
481
|
+
setAction('idle');
|
|
482
|
+
}
|
|
369
483
|
}
|
|
370
|
-
|
|
371
|
-
// ─── Voice note: record → transcribe → send as feedback ───────────
|
|
372
|
-
// Tap once to start; tap again to stop. On stop: upload the audio
|
|
373
|
-
// to the agent's /voice/transcribe (which routes through whichever
|
|
374
|
-
// STT provider is configured — Whisper / Deepgram / OpenAI / etc.),
|
|
375
|
-
// then file the transcript as a bug report. The audio file itself
|
|
376
|
-
// is also attached to the feedback bundle so the agent can re-play
|
|
377
|
-
// it if the transcript is wrong.
|
|
378
|
-
const handleToggleVoice = (0, react_1.useCallback)(async () => {
|
|
379
|
-
setError(null);
|
|
380
|
-
if (!isRecordingVoice) {
|
|
484
|
+
else {
|
|
381
485
|
try {
|
|
382
|
-
await (0, capture_1.
|
|
486
|
+
await (0, capture_1.startVideoRecording)();
|
|
383
487
|
if (mountedRef.current) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
488
|
+
setIsRecordingVideo(true);
|
|
489
|
+
setToast('Recording… tap again to stop and upload');
|
|
490
|
+
setLastVideo(null);
|
|
387
491
|
}
|
|
388
492
|
}
|
|
389
493
|
catch (err) {
|
|
390
|
-
setIsRecordingVoice(false);
|
|
391
|
-
setAction('idle');
|
|
392
494
|
setError(err instanceof Error ? err.message : String(err));
|
|
393
495
|
}
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
// Stopping → transcribe → send.
|
|
397
|
-
try {
|
|
398
|
-
const audio = await (0, capture_1.stopAudioRecording)();
|
|
399
|
-
setIsRecordingVoice(false);
|
|
400
|
-
if (!audio) {
|
|
401
|
-
setAction('idle');
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
setAction('transcribing-voice');
|
|
405
|
-
setToast('Transcribing…');
|
|
406
|
-
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
407
|
-
if (!config?.agentUrl) {
|
|
408
|
-
setError('Not connected to the agent yet.');
|
|
409
|
-
setAction('idle');
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
let transcript = '';
|
|
413
|
-
try {
|
|
414
|
-
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
415
|
-
if (client) {
|
|
416
|
-
const res = await client.transcribeVoice(audio.path);
|
|
417
|
-
transcript = res.text ?? '';
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
// Transcription can fail (no STT provider configured on the
|
|
422
|
-
// agent, network blip, etc.). Don't block the flow — ship
|
|
423
|
-
// the raw audio file with a "[no transcript]" note so the
|
|
424
|
-
// agent + human reviewer can still play it back.
|
|
425
|
-
}
|
|
426
|
-
const { Dimensions } = require('react-native');
|
|
427
|
-
const { width, height } = Dimensions.get('window');
|
|
428
|
-
const deviceInfo = {
|
|
429
|
-
platform: react_native_1.Platform.OS,
|
|
430
|
-
osVersion: String(react_native_1.Platform.Version),
|
|
431
|
-
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
432
|
-
screenWidth: width,
|
|
433
|
-
screenHeight: height,
|
|
434
|
-
};
|
|
435
|
-
const bundle = {
|
|
436
|
-
metadata: {
|
|
437
|
-
timestamp: new Date().toISOString(),
|
|
438
|
-
device: deviceInfo,
|
|
439
|
-
app: {},
|
|
440
|
-
userNote: transcript.length > 0
|
|
441
|
-
? `[Voice note] ${transcript}`
|
|
442
|
-
: `[Voice note · ${Math.round(audio.duration)}s — transcription unavailable]`,
|
|
443
|
-
},
|
|
444
|
-
screenshots: [],
|
|
445
|
-
audio: audio.path,
|
|
446
|
-
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
447
|
-
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
448
|
-
: undefined,
|
|
449
|
-
};
|
|
450
|
-
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
451
|
-
setToast(transcript ? `Sent: "${transcript.slice(0, 60)}${transcript.length > 60 ? '…' : ''}"` : 'Voice note sent');
|
|
452
|
-
closeSoon(1800);
|
|
453
|
-
}
|
|
454
|
-
catch (err) {
|
|
455
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
456
|
-
}
|
|
457
|
-
finally {
|
|
458
|
-
if (mountedRef.current)
|
|
459
|
-
setAction('idle');
|
|
460
|
-
}
|
|
461
|
-
}, [isRecordingVoice, closeSoon]);
|
|
462
|
-
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
463
|
-
const handleSendVideo = (0, react_1.useCallback)(async () => {
|
|
464
|
-
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
465
|
-
if (!config?.agentUrl) {
|
|
466
|
-
setError('Not connected to the agent yet.');
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (!lastVideo) {
|
|
470
|
-
setError('No video recorded yet.');
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
setAction('sending-video');
|
|
474
|
-
setError(null);
|
|
475
|
-
try {
|
|
476
|
-
const { Dimensions } = require('react-native');
|
|
477
|
-
const { width, height } = Dimensions.get('window');
|
|
478
|
-
const deviceInfo = {
|
|
479
|
-
platform: react_native_1.Platform.OS,
|
|
480
|
-
osVersion: String(react_native_1.Platform.Version),
|
|
481
|
-
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
482
|
-
screenWidth: width,
|
|
483
|
-
screenHeight: height,
|
|
484
|
-
};
|
|
485
|
-
const bundle = {
|
|
486
|
-
metadata: {
|
|
487
|
-
timestamp: new Date().toISOString(),
|
|
488
|
-
device: deviceInfo,
|
|
489
|
-
app: {},
|
|
490
|
-
userNote: '[Screen recording]',
|
|
491
|
-
},
|
|
492
|
-
screenshots: [],
|
|
493
|
-
video: lastVideo.path,
|
|
494
|
-
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
495
|
-
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
496
|
-
: undefined,
|
|
497
|
-
};
|
|
498
|
-
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
499
|
-
setToast('Video sent');
|
|
500
|
-
setLastVideo(null);
|
|
501
|
-
closeSoon(1200);
|
|
502
496
|
}
|
|
503
|
-
|
|
504
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
505
|
-
}
|
|
506
|
-
finally {
|
|
507
|
-
if (mountedRef.current)
|
|
508
|
-
setAction('idle');
|
|
509
|
-
}
|
|
510
|
-
}, [lastVideo, closeSoon]);
|
|
497
|
+
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
511
498
|
const busy = action !== 'idle';
|
|
512
499
|
return (<>
|
|
513
500
|
<AuthOverlay_1.AuthOverlay />
|
|
@@ -525,11 +512,6 @@ const FeedbackModal = () => {
|
|
|
525
512
|
{/* 1. Hot Reload — the common path */}
|
|
526
513
|
<ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
|
|
527
514
|
|
|
528
|
-
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
529
|
-
<ActionRow label={action === 'capturing'
|
|
530
|
-
? 'Capturing…'
|
|
531
|
-
: 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
|
|
532
|
-
|
|
533
515
|
{/* 3. Vibing — expands to an input box on first tap
|
|
534
516
|
so the user says WHAT they want to vibe on, just
|
|
535
517
|
like the Yaver mobile app's Vibing tab. Second
|
|
@@ -555,24 +537,22 @@ const FeedbackModal = () => {
|
|
|
555
537
|
Last vibing task: {lastVibeTaskId.slice(0, 12)}…
|
|
556
538
|
</react_native_1.Text>)}
|
|
557
539
|
|
|
558
|
-
{/*
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
: 'Voice Note'} tint={isRecordingVoice ? '#ef4444' : '#f472b6'} onPress={handleToggleVoice} disabled={busy && action !== 'recording-voice' && action !== 'idle'} busy={action === 'transcribing-voice'}/>)}
|
|
566
|
-
|
|
567
|
-
{/* 4. Start/Stop Recording */}
|
|
568
|
-
<ActionRow label={isRecordingVideo ? 'Stop Recording' : 'Start Recording'} tint={isRecordingVideo ? '#ef4444' : '#60a5fa'} onPress={handleToggleRecording} disabled={busy && action !== 'idle' && !isRecordingVideo}/>
|
|
540
|
+
{/* Screenshot / Upload */}
|
|
541
|
+
{!showCaptureChoices ? (<ActionRow label={action === 'capturing'
|
|
542
|
+
? 'Working…'
|
|
543
|
+
: 'Screenshot / Upload'} tint="#22c55e" onPress={handleCaptureChoiceToggle} disabled={busy} busy={action === 'capturing'}/>) : (<react_native_1.View style={styles.captureChoices}>
|
|
544
|
+
<ActionRow label="Take Screenshot" tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy}/>
|
|
545
|
+
<ActionRow label="Upload File" tint="#34d399" onPress={handleFileUpload} disabled={busy}/>
|
|
546
|
+
</react_native_1.View>)}
|
|
569
547
|
|
|
570
|
-
{/*
|
|
571
|
-
<ActionRow label={action === '
|
|
572
|
-
? '
|
|
573
|
-
:
|
|
574
|
-
?
|
|
575
|
-
:
|
|
548
|
+
{/* 4. Screen recording */}
|
|
549
|
+
<ActionRow label={action === 'uploading-video'
|
|
550
|
+
? 'Uploading…'
|
|
551
|
+
: isRecordingVideo
|
|
552
|
+
? 'Stop & Upload Recording'
|
|
553
|
+
: lastVideo
|
|
554
|
+
? `Retry Upload Recording · ${Math.round(lastVideo.duration)}s`
|
|
555
|
+
: 'Screen Recording'} tint={isRecordingVideo ? '#ef4444' : '#60a5fa'} onPress={handleScreenRecording} disabled={busy && action !== 'uploading-video' && !isRecordingVideo} busy={action === 'uploading-video'}/>
|
|
576
556
|
|
|
577
557
|
{progress !== null && (<react_native_1.View style={styles.progressTrack}>
|
|
578
558
|
<react_native_1.View style={[
|
|
@@ -603,6 +583,13 @@ const FeedbackModal = () => {
|
|
|
603
583
|
: '● Hide quick-access icon'}
|
|
604
584
|
</react_native_1.Text>
|
|
605
585
|
</react_native_1.Pressable>
|
|
586
|
+
|
|
587
|
+
<react_native_1.Pressable onPress={handleClose} style={({ pressed }) => [
|
|
588
|
+
styles.cancelBtn,
|
|
589
|
+
pressed && styles.buttonPressed,
|
|
590
|
+
]} accessibilityRole="button" accessibilityLabel="Cancel">
|
|
591
|
+
<react_native_1.Text style={styles.cancelBtnText}>Cancel</react_native_1.Text>
|
|
592
|
+
</react_native_1.Pressable>
|
|
606
593
|
</react_native_1.Pressable>
|
|
607
594
|
</react_native_1.Pressable>
|
|
608
595
|
</react_native_1.Modal>)}
|
|
@@ -723,6 +710,9 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
723
710
|
fontSize: 15,
|
|
724
711
|
fontWeight: '700',
|
|
725
712
|
},
|
|
713
|
+
captureChoices: {
|
|
714
|
+
gap: 10,
|
|
715
|
+
},
|
|
726
716
|
progressTrack: {
|
|
727
717
|
height: 6,
|
|
728
718
|
borderRadius: 3,
|
|
@@ -758,4 +748,22 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
758
748
|
fontSize: 12,
|
|
759
749
|
fontWeight: '500',
|
|
760
750
|
},
|
|
751
|
+
cancelBtn: {
|
|
752
|
+
marginTop: 4,
|
|
753
|
+
borderRadius: 14,
|
|
754
|
+
borderWidth: 1,
|
|
755
|
+
borderColor: 'rgba(255,255,255,0.1)',
|
|
756
|
+
paddingVertical: 15,
|
|
757
|
+
alignItems: 'center',
|
|
758
|
+
justifyContent: 'center',
|
|
759
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
760
|
+
},
|
|
761
|
+
cancelBtnText: {
|
|
762
|
+
color: '#e5e7eb',
|
|
763
|
+
fontSize: 15,
|
|
764
|
+
fontWeight: '700',
|
|
765
|
+
},
|
|
766
|
+
buttonPressed: {
|
|
767
|
+
opacity: 0.7,
|
|
768
|
+
},
|
|
761
769
|
});
|
package/dist/P2PClient.js
CHANGED
|
@@ -152,6 +152,13 @@ class P2PClient {
|
|
|
152
152
|
name: 'screen_recording.mp4',
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
+
if (bundle.audio) {
|
|
156
|
+
formData.append('audio', {
|
|
157
|
+
uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
|
|
158
|
+
type: 'audio/m4a',
|
|
159
|
+
name: 'voice_note.m4a',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
155
162
|
const response = await fetch(`${this.baseUrl}/feedback`, {
|
|
156
163
|
method: 'POST',
|
|
157
164
|
headers: {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
export interface QuickActionIconProps {
|
|
3
|
-
/**
|
|
3
|
+
/** Deprecated alias for `backgroundColor`. */
|
|
4
4
|
color?: string;
|
|
5
|
+
/** Override the background from FeedbackConfig.quickIconBackgroundColor. */
|
|
6
|
+
backgroundColor?: string;
|
|
7
|
+
/** Override the label color from FeedbackConfig.quickIconForegroundColor. */
|
|
8
|
+
foregroundColor?: string;
|
|
9
|
+
/** Override the border color from FeedbackConfig.quickIconBorderColor. */
|
|
10
|
+
borderColor?: string;
|
|
11
|
+
/** Override the shadow color from FeedbackConfig.quickIconShadowColor. */
|
|
12
|
+
shadowColor?: string;
|
|
5
13
|
/** Override the initial position from FeedbackConfig.quickIconInitialPosition. */
|
|
6
14
|
initialPosition?: {
|
|
7
15
|
x: number;
|
|
@@ -24,7 +32,7 @@ export interface QuickActionIconProps {
|
|
|
24
32
|
* hidden the user can still shake to open feedback.
|
|
25
33
|
*
|
|
26
34
|
* Visibility is controlled by `FeedbackConfig.quickIcon`:
|
|
27
|
-
* - `'auto'` (default) → `'
|
|
35
|
+
* - `'auto'` (default) → `'after-shake'` on iOS/Android, `'off'` on web.
|
|
28
36
|
* - `'always'` → visible from first render.
|
|
29
37
|
* - `'after-shake'` → hidden until `yaverFeedback:firstShake` fires.
|
|
30
38
|
* - `'off'` → never rendered.
|