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/dist/FeedbackModal.js
CHANGED
|
@@ -40,6 +40,7 @@ const YaverFeedback_1 = require("./YaverFeedback");
|
|
|
40
40
|
const capture_1 = require("./capture");
|
|
41
41
|
const upload_1 = require("./upload");
|
|
42
42
|
const AuthOverlay_1 = require("./AuthOverlay");
|
|
43
|
+
const QuickActionIcon_1 = require("./QuickActionIcon");
|
|
43
44
|
const FeedbackModal = () => {
|
|
44
45
|
const [visible, setVisible] = (0, react_1.useState)(false);
|
|
45
46
|
const [action, setAction] = (0, react_1.useState)('idle');
|
|
@@ -47,11 +48,12 @@ const FeedbackModal = () => {
|
|
|
47
48
|
const [toast, setToast] = (0, react_1.useState)(null);
|
|
48
49
|
const [progress, setProgress] = (0, react_1.useState)(null);
|
|
49
50
|
const [isRecordingVideo, setIsRecordingVideo] = (0, react_1.useState)(false);
|
|
50
|
-
const [isRecordingVoice, setIsRecordingVoice] = (0, react_1.useState)(false);
|
|
51
|
-
// Cached once on mount — bare-RN apps without expo-av get a clean
|
|
52
|
-
// hidden button instead of a runtime error.
|
|
53
|
-
const voiceSupported = (0, react_1.useRef)((0, capture_1.isVoiceCaptureSupported)()).current;
|
|
54
51
|
const [lastVideo, setLastVideo] = (0, react_1.useState)(null);
|
|
52
|
+
// Tracks whether the user has hidden the QuickActionIcon via its
|
|
53
|
+
// long-press menu. Shake is always available, so the feedback modal
|
|
54
|
+
// is our guaranteed UI for bringing the icon back — we surface a
|
|
55
|
+
// small "Show quick icon" row when this is true.
|
|
56
|
+
const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
|
|
55
57
|
// Vibing-input mode: same expand-on-tap pattern as email login.
|
|
56
58
|
// Tap "Vibing" once → the button reveals an input + Send; that lets
|
|
57
59
|
// the user say WHAT they want to vibe on instead of firing a canned
|
|
@@ -59,6 +61,7 @@ const FeedbackModal = () => {
|
|
|
59
61
|
// the wrong project because the matcher grepped the prompt itself).
|
|
60
62
|
const [showVibeInput, setShowVibeInput] = (0, react_1.useState)(false);
|
|
61
63
|
const [vibePrompt, setVibePrompt] = (0, react_1.useState)('');
|
|
64
|
+
const [showCaptureChoices, setShowCaptureChoices] = (0, react_1.useState)(false);
|
|
62
65
|
const [lastVibeTaskId, setLastVibeTaskId] = (0, react_1.useState)(null);
|
|
63
66
|
const mountedRef = (0, react_1.useRef)(true);
|
|
64
67
|
(0, react_1.useEffect)(() => {
|
|
@@ -69,6 +72,16 @@ const FeedbackModal = () => {
|
|
|
69
72
|
setError(null);
|
|
70
73
|
setToast(null);
|
|
71
74
|
setAction('idle');
|
|
75
|
+
setShowCaptureChoices(false);
|
|
76
|
+
// Re-read the "user hid the quick icon" flag on every open so
|
|
77
|
+
// the re-enable row reflects the latest preference (the user
|
|
78
|
+
// might have hidden or shown it between opens).
|
|
79
|
+
YaverFeedback_1.YaverFeedback.isQuickIconHidden()
|
|
80
|
+
.then((v) => {
|
|
81
|
+
if (mountedRef.current)
|
|
82
|
+
setQuickIconHidden(v);
|
|
83
|
+
})
|
|
84
|
+
.catch(() => { });
|
|
72
85
|
}
|
|
73
86
|
});
|
|
74
87
|
// Agent streams build / compile progress through the BlackBox
|
|
@@ -108,6 +121,7 @@ const FeedbackModal = () => {
|
|
|
108
121
|
setError(null);
|
|
109
122
|
setToast(null);
|
|
110
123
|
setAction('idle');
|
|
124
|
+
setShowCaptureChoices(false);
|
|
111
125
|
}, []);
|
|
112
126
|
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
113
127
|
// re-query Convex for the fresh IP and retry once. Solves the common
|
|
@@ -188,26 +202,50 @@ const FeedbackModal = () => {
|
|
|
188
202
|
setAction('idle');
|
|
189
203
|
}
|
|
190
204
|
}, [closeSoon, runWithReconnect]);
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
// Hide the modal first so the screenshot captures the actual screen
|
|
194
|
-
// (the bug) — not the modal card. Await a short animation delay,
|
|
195
|
-
// snapshot, upload the feedback bundle with any buffered errors, then
|
|
196
|
-
// kick `/feedback/{id}/fix` to create the repair task.
|
|
197
|
-
const handleScreenshotAndFix = (0, react_1.useCallback)(async () => {
|
|
205
|
+
const uploadBundleWithOptionalFix = (0, react_1.useCallback)(async (bundle, fixOnUpload, successToast, failureToast) => {
|
|
198
206
|
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
199
207
|
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
200
208
|
if (!client || !config?.agentUrl) {
|
|
201
209
|
setError('Not connected to the agent yet.');
|
|
202
210
|
return;
|
|
203
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 () => {
|
|
204
243
|
setAction('capturing');
|
|
205
244
|
setError(null);
|
|
206
|
-
|
|
245
|
+
setShowCaptureChoices(false);
|
|
207
246
|
setVisible(false);
|
|
208
|
-
// Wait out the slide-down animation on both platforms.
|
|
209
247
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
210
|
-
let path
|
|
248
|
+
let path;
|
|
211
249
|
try {
|
|
212
250
|
path = await (0, capture_1.captureScreenshot)();
|
|
213
251
|
}
|
|
@@ -217,7 +255,6 @@ const FeedbackModal = () => {
|
|
|
217
255
|
setAction('idle');
|
|
218
256
|
return;
|
|
219
257
|
}
|
|
220
|
-
// Step 2: Re-show the modal for progress + ack.
|
|
221
258
|
setVisible(true);
|
|
222
259
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
223
260
|
try {
|
|
@@ -241,35 +278,57 @@ const FeedbackModal = () => {
|
|
|
241
278
|
screenshots: [path],
|
|
242
279
|
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
243
280
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.');
|
|
262
318
|
}
|
|
263
|
-
|
|
319
|
+
await uploadBundleWithOptionalFix(bundle, picked.kind === 'image', picked.kind === 'image' ? 'Fix task started' : 'File uploaded');
|
|
264
320
|
}
|
|
265
321
|
catch (err) {
|
|
266
|
-
|
|
322
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
323
|
+
if (message !== 'File selection canceled.') {
|
|
324
|
+
setError(message);
|
|
325
|
+
}
|
|
267
326
|
}
|
|
268
327
|
finally {
|
|
269
328
|
if (mountedRef.current)
|
|
270
329
|
setAction('idle');
|
|
271
330
|
}
|
|
272
|
-
}, [
|
|
331
|
+
}, [uploadBundleWithOptionalFix]);
|
|
273
332
|
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
274
333
|
// First tap expands the input; second submit fires the actual
|
|
275
334
|
// /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
|
|
@@ -322,180 +381,124 @@ const FeedbackModal = () => {
|
|
|
322
381
|
setAction('idle');
|
|
323
382
|
}
|
|
324
383
|
}, [vibePrompt]);
|
|
325
|
-
// ─── 4.
|
|
326
|
-
const
|
|
384
|
+
// ─── 4. Screen recording ───────────────────────────────────────────
|
|
385
|
+
const handleScreenRecording = (0, react_1.useCallback)(async () => {
|
|
327
386
|
setError(null);
|
|
328
|
-
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');
|
|
329
394
|
try {
|
|
330
|
-
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);
|
|
331
418
|
if (mountedRef.current) {
|
|
332
|
-
|
|
333
|
-
setLastVideo(
|
|
334
|
-
setToast(`Recording stopped — ${Math.round(result.duration)}s`);
|
|
419
|
+
setToast(`Recording uploaded — ${Math.round(lastVideo.duration)}s`);
|
|
420
|
+
setLastVideo(null);
|
|
335
421
|
}
|
|
422
|
+
closeSoon(1200);
|
|
336
423
|
}
|
|
337
424
|
catch (err) {
|
|
338
|
-
setIsRecordingVideo(false);
|
|
339
425
|
setError(err instanceof Error ? err.message : String(err));
|
|
340
426
|
}
|
|
427
|
+
finally {
|
|
428
|
+
if (mountedRef.current)
|
|
429
|
+
setAction('idle');
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
341
432
|
}
|
|
342
|
-
|
|
433
|
+
if (isRecordingVideo) {
|
|
343
434
|
try {
|
|
344
|
-
await (0, capture_1.
|
|
435
|
+
const result = await (0, capture_1.stopVideoRecording)();
|
|
345
436
|
if (mountedRef.current) {
|
|
346
|
-
setIsRecordingVideo(
|
|
347
|
-
|
|
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`);
|
|
348
471
|
setLastVideo(null);
|
|
349
472
|
}
|
|
473
|
+
closeSoon(1200);
|
|
350
474
|
}
|
|
351
475
|
catch (err) {
|
|
476
|
+
setIsRecordingVideo(false);
|
|
352
477
|
setError(err instanceof Error ? err.message : String(err));
|
|
353
478
|
}
|
|
479
|
+
finally {
|
|
480
|
+
if (mountedRef.current)
|
|
481
|
+
setAction('idle');
|
|
482
|
+
}
|
|
354
483
|
}
|
|
355
|
-
|
|
356
|
-
// ─── Voice note: record → transcribe → send as feedback ───────────
|
|
357
|
-
// Tap once to start; tap again to stop. On stop: upload the audio
|
|
358
|
-
// to the agent's /voice/transcribe (which routes through whichever
|
|
359
|
-
// STT provider is configured — Whisper / Deepgram / OpenAI / etc.),
|
|
360
|
-
// then file the transcript as a bug report. The audio file itself
|
|
361
|
-
// is also attached to the feedback bundle so the agent can re-play
|
|
362
|
-
// it if the transcript is wrong.
|
|
363
|
-
const handleToggleVoice = (0, react_1.useCallback)(async () => {
|
|
364
|
-
setError(null);
|
|
365
|
-
if (!isRecordingVoice) {
|
|
484
|
+
else {
|
|
366
485
|
try {
|
|
367
|
-
await (0, capture_1.
|
|
486
|
+
await (0, capture_1.startVideoRecording)();
|
|
368
487
|
if (mountedRef.current) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
488
|
+
setIsRecordingVideo(true);
|
|
489
|
+
setToast('Recording… tap again to stop and upload');
|
|
490
|
+
setLastVideo(null);
|
|
372
491
|
}
|
|
373
492
|
}
|
|
374
493
|
catch (err) {
|
|
375
|
-
setIsRecordingVoice(false);
|
|
376
|
-
setAction('idle');
|
|
377
494
|
setError(err instanceof Error ? err.message : String(err));
|
|
378
495
|
}
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
// Stopping → transcribe → send.
|
|
382
|
-
try {
|
|
383
|
-
const audio = await (0, capture_1.stopAudioRecording)();
|
|
384
|
-
setIsRecordingVoice(false);
|
|
385
|
-
if (!audio) {
|
|
386
|
-
setAction('idle');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
setAction('transcribing-voice');
|
|
390
|
-
setToast('Transcribing…');
|
|
391
|
-
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
392
|
-
if (!config?.agentUrl) {
|
|
393
|
-
setError('Not connected to the agent yet.');
|
|
394
|
-
setAction('idle');
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
let transcript = '';
|
|
398
|
-
try {
|
|
399
|
-
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
400
|
-
if (client) {
|
|
401
|
-
const res = await client.transcribeVoice(audio.path);
|
|
402
|
-
transcript = res.text ?? '';
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
catch {
|
|
406
|
-
// Transcription can fail (no STT provider configured on the
|
|
407
|
-
// agent, network blip, etc.). Don't block the flow — ship
|
|
408
|
-
// the raw audio file with a "[no transcript]" note so the
|
|
409
|
-
// agent + human reviewer can still play it back.
|
|
410
|
-
}
|
|
411
|
-
const { Dimensions } = require('react-native');
|
|
412
|
-
const { width, height } = Dimensions.get('window');
|
|
413
|
-
const deviceInfo = {
|
|
414
|
-
platform: react_native_1.Platform.OS,
|
|
415
|
-
osVersion: String(react_native_1.Platform.Version),
|
|
416
|
-
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
417
|
-
screenWidth: width,
|
|
418
|
-
screenHeight: height,
|
|
419
|
-
};
|
|
420
|
-
const bundle = {
|
|
421
|
-
metadata: {
|
|
422
|
-
timestamp: new Date().toISOString(),
|
|
423
|
-
device: deviceInfo,
|
|
424
|
-
app: {},
|
|
425
|
-
userNote: transcript.length > 0
|
|
426
|
-
? `[Voice note] ${transcript}`
|
|
427
|
-
: `[Voice note · ${Math.round(audio.duration)}s — transcription unavailable]`,
|
|
428
|
-
},
|
|
429
|
-
screenshots: [],
|
|
430
|
-
audio: audio.path,
|
|
431
|
-
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
432
|
-
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
433
|
-
: undefined,
|
|
434
|
-
};
|
|
435
|
-
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
436
|
-
setToast(transcript ? `Sent: "${transcript.slice(0, 60)}${transcript.length > 60 ? '…' : ''}"` : 'Voice note sent');
|
|
437
|
-
closeSoon(1800);
|
|
438
|
-
}
|
|
439
|
-
catch (err) {
|
|
440
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
441
|
-
}
|
|
442
|
-
finally {
|
|
443
|
-
if (mountedRef.current)
|
|
444
|
-
setAction('idle');
|
|
445
|
-
}
|
|
446
|
-
}, [isRecordingVoice, closeSoon]);
|
|
447
|
-
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
448
|
-
const handleSendVideo = (0, react_1.useCallback)(async () => {
|
|
449
|
-
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
450
|
-
if (!config?.agentUrl) {
|
|
451
|
-
setError('Not connected to the agent yet.');
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (!lastVideo) {
|
|
455
|
-
setError('No video recorded yet.');
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
setAction('sending-video');
|
|
459
|
-
setError(null);
|
|
460
|
-
try {
|
|
461
|
-
const { Dimensions } = require('react-native');
|
|
462
|
-
const { width, height } = Dimensions.get('window');
|
|
463
|
-
const deviceInfo = {
|
|
464
|
-
platform: react_native_1.Platform.OS,
|
|
465
|
-
osVersion: String(react_native_1.Platform.Version),
|
|
466
|
-
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
467
|
-
screenWidth: width,
|
|
468
|
-
screenHeight: height,
|
|
469
|
-
};
|
|
470
|
-
const bundle = {
|
|
471
|
-
metadata: {
|
|
472
|
-
timestamp: new Date().toISOString(),
|
|
473
|
-
device: deviceInfo,
|
|
474
|
-
app: {},
|
|
475
|
-
userNote: '[Screen recording]',
|
|
476
|
-
},
|
|
477
|
-
screenshots: [],
|
|
478
|
-
video: lastVideo.path,
|
|
479
|
-
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
480
|
-
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
481
|
-
: undefined,
|
|
482
|
-
};
|
|
483
|
-
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
484
|
-
setToast('Video sent');
|
|
485
|
-
setLastVideo(null);
|
|
486
|
-
closeSoon(1200);
|
|
487
|
-
}
|
|
488
|
-
catch (err) {
|
|
489
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
490
|
-
}
|
|
491
|
-
finally {
|
|
492
|
-
if (mountedRef.current)
|
|
493
|
-
setAction('idle');
|
|
494
496
|
}
|
|
495
|
-
}, [
|
|
497
|
+
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
496
498
|
const busy = action !== 'idle';
|
|
497
499
|
return (<>
|
|
498
500
|
<AuthOverlay_1.AuthOverlay />
|
|
501
|
+
<QuickActionIcon_1.QuickActionIcon />
|
|
499
502
|
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
|
|
500
503
|
<react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
|
|
501
504
|
<react_native_1.Pressable style={styles.modal} onPress={(e) => e.stopPropagation()}>
|
|
@@ -509,11 +512,6 @@ const FeedbackModal = () => {
|
|
|
509
512
|
{/* 1. Hot Reload — the common path */}
|
|
510
513
|
<ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
|
|
511
514
|
|
|
512
|
-
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
513
|
-
<ActionRow label={action === 'capturing'
|
|
514
|
-
? 'Capturing…'
|
|
515
|
-
: 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
|
|
516
|
-
|
|
517
515
|
{/* 3. Vibing — expands to an input box on first tap
|
|
518
516
|
so the user says WHAT they want to vibe on, just
|
|
519
517
|
like the Yaver mobile app's Vibing tab. Second
|
|
@@ -539,24 +537,22 @@ const FeedbackModal = () => {
|
|
|
539
537
|
Last vibing task: {lastVibeTaskId.slice(0, 12)}…
|
|
540
538
|
</react_native_1.Text>)}
|
|
541
539
|
|
|
542
|
-
{/*
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
: 'Voice Note'} tint={isRecordingVoice ? '#ef4444' : '#f472b6'} onPress={handleToggleVoice} disabled={busy && action !== 'recording-voice' && action !== 'idle'} busy={action === 'transcribing-voice'}/>)}
|
|
550
|
-
|
|
551
|
-
{/* 4. Start/Stop Recording */}
|
|
552
|
-
<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>)}
|
|
553
547
|
|
|
554
|
-
{/*
|
|
555
|
-
<ActionRow label={action === '
|
|
556
|
-
? '
|
|
557
|
-
:
|
|
558
|
-
?
|
|
559
|
-
:
|
|
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'}/>
|
|
560
556
|
|
|
561
557
|
{progress !== null && (<react_native_1.View style={styles.progressTrack}>
|
|
562
558
|
<react_native_1.View style={[
|
|
@@ -566,6 +562,34 @@ const FeedbackModal = () => {
|
|
|
566
562
|
</react_native_1.View>)}
|
|
567
563
|
{toast && <react_native_1.Text style={styles.toast}>{toast}</react_native_1.Text>}
|
|
568
564
|
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
565
|
+
|
|
566
|
+
{/* Quick-icon toggle. The user's three ways to control
|
|
567
|
+
the floating icon are: (1) long-press the icon →
|
|
568
|
+
Hide, (2) tap this row to toggle it on/off, (3) shake
|
|
569
|
+
→ this modal → tap this row. Shake is the unkillable
|
|
570
|
+
back-door when the icon is hidden and the dev hasn't
|
|
571
|
+
exposed their own settings UI. */}
|
|
572
|
+
<react_native_1.Pressable onPress={async () => {
|
|
573
|
+
const next = !quickIconHidden;
|
|
574
|
+
setQuickIconHidden(next);
|
|
575
|
+
await YaverFeedback_1.YaverFeedback.setQuickIconVisible(!next);
|
|
576
|
+
}} style={({ pressed }) => [
|
|
577
|
+
styles.quickIconToggle,
|
|
578
|
+
pressed && { opacity: 0.7 },
|
|
579
|
+
]} accessibilityRole="button" accessibilityLabel={quickIconHidden ? 'Show quick icon' : 'Hide quick icon'}>
|
|
580
|
+
<react_native_1.Text style={styles.quickIconToggleText}>
|
|
581
|
+
{quickIconHidden
|
|
582
|
+
? '◯ Show quick-access icon'
|
|
583
|
+
: '● Hide quick-access icon'}
|
|
584
|
+
</react_native_1.Text>
|
|
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>
|
|
569
593
|
</react_native_1.Pressable>
|
|
570
594
|
</react_native_1.Pressable>
|
|
571
595
|
</react_native_1.Modal>)}
|
|
@@ -686,6 +710,9 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
686
710
|
fontSize: 15,
|
|
687
711
|
fontWeight: '700',
|
|
688
712
|
},
|
|
713
|
+
captureChoices: {
|
|
714
|
+
gap: 10,
|
|
715
|
+
},
|
|
689
716
|
progressTrack: {
|
|
690
717
|
height: 6,
|
|
691
718
|
borderRadius: 3,
|
|
@@ -710,4 +737,33 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
710
737
|
textAlign: 'center',
|
|
711
738
|
marginTop: 4,
|
|
712
739
|
},
|
|
740
|
+
quickIconToggle: {
|
|
741
|
+
marginTop: 4,
|
|
742
|
+
alignSelf: 'center',
|
|
743
|
+
paddingVertical: 6,
|
|
744
|
+
paddingHorizontal: 12,
|
|
745
|
+
},
|
|
746
|
+
quickIconToggleText: {
|
|
747
|
+
color: '#9ca3af',
|
|
748
|
+
fontSize: 12,
|
|
749
|
+
fontWeight: '500',
|
|
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
|
+
},
|
|
713
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: {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface QuickActionIconProps {
|
|
3
|
+
/** Deprecated alias for `backgroundColor`. */
|
|
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;
|
|
13
|
+
/** Override the initial position from FeedbackConfig.quickIconInitialPosition. */
|
|
14
|
+
initialPosition?: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
/** Override the icon diameter. Default 44. */
|
|
19
|
+
size?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Small tap-to-open icon for the Yaver Feedback SDK.
|
|
23
|
+
*
|
|
24
|
+
* Default UX:
|
|
25
|
+
* - **Tap** opens the feedback modal (same as shake).
|
|
26
|
+
* - **Long-press** (~550ms) opens a menu with "Open feedback" and
|
|
27
|
+
* "Hide icon". Hiding is persisted to AsyncStorage so the user's
|
|
28
|
+
* decision survives app relaunches.
|
|
29
|
+
* - **Drag** repositions the icon.
|
|
30
|
+
*
|
|
31
|
+
* Shake always keeps working independently — even when the icon is
|
|
32
|
+
* hidden the user can still shake to open feedback.
|
|
33
|
+
*
|
|
34
|
+
* Visibility is controlled by `FeedbackConfig.quickIcon`:
|
|
35
|
+
* - `'auto'` (default) → `'after-shake'` on iOS/Android, `'off'` on web.
|
|
36
|
+
* - `'always'` → visible from first render.
|
|
37
|
+
* - `'after-shake'` → hidden until `yaverFeedback:firstShake` fires.
|
|
38
|
+
* - `'off'` → never rendered.
|
|
39
|
+
*
|
|
40
|
+
* Suppressed entirely when the SDK is loaded inside Yaver's super-host
|
|
41
|
+
* (the Yaver mobile app owns the shake gesture + overlay in that case).
|
|
42
|
+
*/
|
|
43
|
+
export declare const QuickActionIcon: React.FC<QuickActionIconProps>;
|