yaver-feedback-react-native 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FeedbackModal.d.ts +0 -9
- package/dist/FeedbackModal.js +265 -473
- package/dist/P2PClient.d.ts +24 -0
- package/dist/P2PClient.js +65 -10
- package/dist/YaverFeedback.d.ts +0 -4
- package/dist/YaverFeedback.js +0 -10
- package/dist/__tests__/YaverFeedback.test.js +2 -35
- package/dist/__tests__/types.test.js +3 -35
- package/dist/capture.d.ts +25 -12
- package/dist/capture.js +73 -34
- package/dist/expo.d.ts +0 -1
- package/dist/expo.js +0 -2
- package/dist/index.d.ts +15 -10
- package/dist/index.js +17 -11
- package/dist/types.d.ts +2 -32
- package/dist/upload.d.ts +8 -3
- package/dist/upload.js +5 -12
- package/package.json +1 -1
- package/src/FeedbackModal.tsx +384 -565
- package/src/P2PClient.ts +66 -12
- package/src/YaverFeedback.ts +0 -12
- package/src/__tests__/YaverFeedback.test.ts +2 -41
- package/src/__tests__/types.test.ts +3 -39
- package/src/capture.ts +73 -37
- package/src/expo.ts +0 -2
- package/src/index.ts +19 -10
- package/src/types.ts +2 -33
- package/src/upload.ts +6 -15
package/src/P2PClient.ts
CHANGED
|
@@ -79,14 +79,6 @@ export class P2PClient {
|
|
|
79
79
|
} as any);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
if (bundle.audio) {
|
|
83
|
-
formData.append('audio', {
|
|
84
|
-
uri: Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
|
|
85
|
-
type: 'audio/m4a',
|
|
86
|
-
name: 'voice_note.m4a',
|
|
87
|
-
} as any);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
82
|
if (bundle.video) {
|
|
91
83
|
formData.append('video', {
|
|
92
84
|
uri: Platform.OS === 'android' ? `file://${bundle.video}` : bundle.video,
|
|
@@ -223,20 +215,82 @@ export class P2PClient {
|
|
|
223
215
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
224
216
|
*/
|
|
225
217
|
async reloadApp(mode: 'dev' | 'bundle' = 'dev'): Promise<{ ok: boolean }> {
|
|
226
|
-
|
|
218
|
+
// Primary path: /dev/reload — same endpoint the Yaver mobile app uses.
|
|
219
|
+
// Triggers Metro/Expo HMR synchronously and emits an SSE `reload` event
|
|
220
|
+
// on /dev/events that the FeedbackModal can subscribe to for progress.
|
|
221
|
+
//
|
|
222
|
+
// Only fall back to /dev/reload-app (the BlackBox-SSE-broadcast path)
|
|
223
|
+
// when the primary path reports "no dev server running" — that mode is
|
|
224
|
+
// really for the mobile app remotely kicking a third-party app, not
|
|
225
|
+
// for the app kicking itself.
|
|
226
|
+
const primary = await fetch(`${this.baseUrl}/dev/reload`, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
229
|
+
});
|
|
230
|
+
if (primary.ok) {
|
|
231
|
+
return primary.json().catch(() => ({ ok: true }));
|
|
232
|
+
}
|
|
233
|
+
if (primary.status >= 500 || primary.status === 404 || mode === 'bundle') {
|
|
234
|
+
const fallback = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify({ mode }),
|
|
241
|
+
});
|
|
242
|
+
if (!fallback.ok) {
|
|
243
|
+
const text = await fallback.text().catch(() => '');
|
|
244
|
+
throw new Error(`[P2PClient] Reload failed (${fallback.status}): ${text}`);
|
|
245
|
+
}
|
|
246
|
+
return fallback.json().catch(() => ({ ok: true }));
|
|
247
|
+
}
|
|
248
|
+
const text = await primary.text().catch(() => '');
|
|
249
|
+
throw new Error(`[P2PClient] Reload failed (${primary.status}): ${text}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Open a vibing session on the connected agent. Vibing is the Yaver
|
|
254
|
+
* interactive coding-agent flow — `/vibing/execute` creates a task with
|
|
255
|
+
* the project context plus the user's prompt. Returns the task id the
|
|
256
|
+
* caller can poll via `/tasks/{id}` if needed.
|
|
257
|
+
*
|
|
258
|
+
* Requires an owner/CLI/paired token — the `/vibing*` routes do not
|
|
259
|
+
* currently accept SDK-minted tokens. Power users typically drive
|
|
260
|
+
* vibing from Claude Code / the Yaver mobile app; this method is a
|
|
261
|
+
* convenience for the SDK's one-tap bug-report-to-vibing path.
|
|
262
|
+
*/
|
|
263
|
+
async vibing(prompt: string, projectPath?: string): Promise<{ taskId: string }> {
|
|
264
|
+
const response = await fetch(`${this.baseUrl}/vibing/execute`, {
|
|
227
265
|
method: 'POST',
|
|
228
266
|
headers: {
|
|
229
267
|
Authorization: `Bearer ${this.authToken}`,
|
|
230
268
|
'Content-Type': 'application/json',
|
|
231
269
|
},
|
|
232
|
-
body: JSON.stringify({
|
|
270
|
+
body: JSON.stringify({ prompt, projectPath: projectPath ?? '' }),
|
|
233
271
|
});
|
|
234
|
-
|
|
235
272
|
if (!response.ok) {
|
|
236
273
|
const text = await response.text().catch(() => '');
|
|
237
|
-
throw new Error(`[P2PClient]
|
|
274
|
+
throw new Error(`[P2PClient] Vibing failed (${response.status}): ${text}`);
|
|
238
275
|
}
|
|
276
|
+
return response.json();
|
|
277
|
+
}
|
|
239
278
|
|
|
279
|
+
/**
|
|
280
|
+
* After uploading a feedback bundle with `uploadFeedback`, call this
|
|
281
|
+
* with the returned report id to create a fix task on the agent. The
|
|
282
|
+
* task includes the feedback's screenshots, errors, and (when available)
|
|
283
|
+
* the BlackBox context for the originating device.
|
|
284
|
+
*/
|
|
285
|
+
async triggerFix(feedbackId: string): Promise<{ taskId: string; prompt: string }> {
|
|
286
|
+
const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
const text = await response.text().catch(() => '');
|
|
292
|
+
throw new Error(`[P2PClient] Fix trigger failed (${response.status}): ${text}`);
|
|
293
|
+
}
|
|
240
294
|
return response.json();
|
|
241
295
|
}
|
|
242
296
|
|
package/src/YaverFeedback.ts
CHANGED
|
@@ -77,8 +77,6 @@ export class YaverFeedback {
|
|
|
77
77
|
config = {
|
|
78
78
|
trigger: 'shake',
|
|
79
79
|
maxRecordingDuration: 120,
|
|
80
|
-
feedbackMode: 'batch',
|
|
81
|
-
agentCommentaryLevel: 0,
|
|
82
80
|
autoLogin: true,
|
|
83
81
|
...cfg,
|
|
84
82
|
};
|
|
@@ -488,16 +486,6 @@ export class YaverFeedback {
|
|
|
488
486
|
return p2pClient;
|
|
489
487
|
}
|
|
490
488
|
|
|
491
|
-
/** Returns the current feedback mode. */
|
|
492
|
-
static getFeedbackMode(): 'live' | 'narrated' | 'batch' {
|
|
493
|
-
return config?.feedbackMode ?? 'batch';
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/** Returns the agent commentary level (0-10). */
|
|
497
|
-
static getCommentaryLevel(): number {
|
|
498
|
-
return config?.agentCommentaryLevel ?? 0;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
489
|
// ─── One-stop SaaS replacement methods ─────────────────────────
|
|
502
490
|
//
|
|
503
491
|
// These are the three solo-dev SaaS-replacement entry points
|
|
@@ -39,8 +39,6 @@ describe('YaverFeedback', () => {
|
|
|
39
39
|
expect(cfg!.agentUrl).toBe('http://localhost:18080');
|
|
40
40
|
expect(cfg!.trigger).toBe('shake');
|
|
41
41
|
expect(cfg!.maxRecordingDuration).toBe(120);
|
|
42
|
-
expect(cfg!.feedbackMode).toBe('batch');
|
|
43
|
-
expect(cfg!.agentCommentaryLevel).toBe(0);
|
|
44
42
|
});
|
|
45
43
|
|
|
46
44
|
it('respects user-provided values over defaults', () => {
|
|
@@ -48,15 +46,13 @@ describe('YaverFeedback', () => {
|
|
|
48
46
|
authToken: 'tok',
|
|
49
47
|
trigger: 'floating-button',
|
|
50
48
|
maxRecordingDuration: 60,
|
|
51
|
-
|
|
52
|
-
agentCommentaryLevel: 7,
|
|
49
|
+
strictNativeAuth: true,
|
|
53
50
|
});
|
|
54
51
|
|
|
55
52
|
const cfg = YaverFeedback.getConfig();
|
|
56
53
|
expect(cfg!.trigger).toBe('floating-button');
|
|
57
54
|
expect(cfg!.maxRecordingDuration).toBe(60);
|
|
58
|
-
expect(cfg!.
|
|
59
|
-
expect(cfg!.agentCommentaryLevel).toBe(7);
|
|
55
|
+
expect(cfg!.strictNativeAuth).toBe(true);
|
|
60
56
|
});
|
|
61
57
|
|
|
62
58
|
it('with enabled=false sets enabled to false', () => {
|
|
@@ -130,41 +126,6 @@ describe('YaverFeedback', () => {
|
|
|
130
126
|
});
|
|
131
127
|
});
|
|
132
128
|
|
|
133
|
-
describe('getFeedbackMode()', () => {
|
|
134
|
-
it('defaults to batch when no config', () => {
|
|
135
|
-
// After any init, feedbackMode defaults to 'batch'
|
|
136
|
-
YaverFeedback.init({ authToken: 'tok' });
|
|
137
|
-
expect(YaverFeedback.getFeedbackMode()).toBe('batch');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('returns configured mode', () => {
|
|
141
|
-
YaverFeedback.init({ authToken: 'tok', feedbackMode: 'narrated' });
|
|
142
|
-
expect(YaverFeedback.getFeedbackMode()).toBe('narrated');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('returns live when configured', () => {
|
|
146
|
-
YaverFeedback.init({ authToken: 'tok', feedbackMode: 'live' });
|
|
147
|
-
expect(YaverFeedback.getFeedbackMode()).toBe('live');
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('getCommentaryLevel()', () => {
|
|
152
|
-
it('defaults to 0', () => {
|
|
153
|
-
YaverFeedback.init({ authToken: 'tok' });
|
|
154
|
-
expect(YaverFeedback.getCommentaryLevel()).toBe(0);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('returns configured level', () => {
|
|
158
|
-
YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 5 });
|
|
159
|
-
expect(YaverFeedback.getCommentaryLevel()).toBe(5);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('returns max level when set to 10', () => {
|
|
163
|
-
YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 10 });
|
|
164
|
-
expect(YaverFeedback.getCommentaryLevel()).toBe(10);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
129
|
describe('startReport()', () => {
|
|
169
130
|
it('does nothing when not enabled', async () => {
|
|
170
131
|
YaverFeedback.init({ authToken: 'tok', enabled: false });
|
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
DeviceInfo,
|
|
7
7
|
AppInfo,
|
|
8
8
|
FeedbackReport,
|
|
9
|
-
AgentCommentary,
|
|
10
9
|
FeedbackStreamEvent,
|
|
11
10
|
} from '../types';
|
|
12
11
|
|
|
@@ -21,8 +20,6 @@ describe('React Native SDK types', () => {
|
|
|
21
20
|
expect(config.trigger).toBeUndefined();
|
|
22
21
|
expect(config.enabled).toBeUndefined();
|
|
23
22
|
expect(config.maxRecordingDuration).toBeUndefined();
|
|
24
|
-
expect(config.feedbackMode).toBeUndefined();
|
|
25
|
-
expect(config.agentCommentaryLevel).toBeUndefined();
|
|
26
23
|
});
|
|
27
24
|
|
|
28
25
|
it('can be constructed with all optional fields', () => {
|
|
@@ -32,12 +29,10 @@ describe('React Native SDK types', () => {
|
|
|
32
29
|
trigger: 'shake',
|
|
33
30
|
enabled: true,
|
|
34
31
|
maxRecordingDuration: 60,
|
|
35
|
-
|
|
36
|
-
agentCommentaryLevel: 7,
|
|
32
|
+
strictNativeAuth: true,
|
|
37
33
|
};
|
|
38
34
|
expect(config.trigger).toBe('shake');
|
|
39
|
-
expect(config.
|
|
40
|
-
expect(config.agentCommentaryLevel).toBe(7);
|
|
35
|
+
expect(config.strictNativeAuth).toBe(true);
|
|
41
36
|
});
|
|
42
37
|
|
|
43
38
|
it('accepts all trigger types', () => {
|
|
@@ -47,14 +42,6 @@ describe('React Native SDK types', () => {
|
|
|
47
42
|
expect(config.trigger).toBe(trigger);
|
|
48
43
|
});
|
|
49
44
|
});
|
|
50
|
-
|
|
51
|
-
it('accepts all feedback modes', () => {
|
|
52
|
-
const modes: FeedbackConfig['feedbackMode'][] = ['live', 'narrated', 'batch'];
|
|
53
|
-
modes.forEach((mode) => {
|
|
54
|
-
const config: FeedbackConfig = { authToken: 'tok', feedbackMode: mode };
|
|
55
|
-
expect(config.feedbackMode).toBe(mode);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
45
|
});
|
|
59
46
|
|
|
60
47
|
describe('FeedbackBundle', () => {
|
|
@@ -82,10 +69,9 @@ describe('React Native SDK types', () => {
|
|
|
82
69
|
expect(bundle.metadata.device.platform).toBe('ios');
|
|
83
70
|
expect(bundle.screenshots).toEqual([]);
|
|
84
71
|
expect(bundle.video).toBeUndefined();
|
|
85
|
-
expect(bundle.audio).toBeUndefined();
|
|
86
72
|
});
|
|
87
73
|
|
|
88
|
-
it('can include optional video
|
|
74
|
+
it('can include optional video + screenshots', () => {
|
|
89
75
|
const bundle: FeedbackBundle = {
|
|
90
76
|
metadata: {
|
|
91
77
|
timestamp: '2026-03-24T12:00:00Z',
|
|
@@ -100,12 +86,10 @@ describe('React Native SDK types', () => {
|
|
|
100
86
|
userNote: 'This button does not work',
|
|
101
87
|
},
|
|
102
88
|
video: '/tmp/recording.mp4',
|
|
103
|
-
audio: '/tmp/voice.m4a',
|
|
104
89
|
screenshots: ['/tmp/ss1.png', '/tmp/ss2.png'],
|
|
105
90
|
};
|
|
106
91
|
|
|
107
92
|
expect(bundle.video).toBe('/tmp/recording.mp4');
|
|
108
|
-
expect(bundle.audio).toBe('/tmp/voice.m4a');
|
|
109
93
|
expect(bundle.screenshots).toHaveLength(2);
|
|
110
94
|
expect(bundle.metadata.userNote).toBe('This button does not work');
|
|
111
95
|
});
|
|
@@ -213,26 +197,6 @@ describe('React Native SDK types', () => {
|
|
|
213
197
|
});
|
|
214
198
|
});
|
|
215
199
|
|
|
216
|
-
describe('AgentCommentary', () => {
|
|
217
|
-
it('has correct structure', () => {
|
|
218
|
-
const commentary: AgentCommentary = {
|
|
219
|
-
id: 'cmt-1',
|
|
220
|
-
timestamp: '2026-03-24T12:00:00Z',
|
|
221
|
-
message: 'I see a layout issue on the login screen',
|
|
222
|
-
type: 'observation',
|
|
223
|
-
};
|
|
224
|
-
expect(commentary.type).toBe('observation');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('accepts all commentary types', () => {
|
|
228
|
-
const types: AgentCommentary['type'][] = ['observation', 'suggestion', 'question', 'action'];
|
|
229
|
-
types.forEach((type) => {
|
|
230
|
-
const c: AgentCommentary = { id: '1', timestamp: 'now', message: 'test', type };
|
|
231
|
-
expect(c.type).toBe(type);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
200
|
describe('FeedbackStreamEvent', () => {
|
|
237
201
|
it('has correct structure', () => {
|
|
238
202
|
const event: FeedbackStreamEvent = {
|
package/src/capture.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Screen capture
|
|
2
|
+
* Screen capture helpers — screenshot + video recording.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Peer deps (all optional — loaded lazily):
|
|
5
|
+
* - `react-native-view-shot` — screenshot
|
|
6
|
+
* - `react-native-record-screen` — video recording (iOS ReplayKit /
|
|
7
|
+
* Android MediaProjection)
|
|
8
|
+
*
|
|
9
|
+
* Each helper surfaces a clear error if the module is missing so a host
|
|
10
|
+
* app knows exactly which peer dep to add. Audio-note / voice-command
|
|
11
|
+
* recording was removed in 0.7.0 — see FeedbackModal for the new
|
|
12
|
+
* 5-button surface.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
|
-
let audioRecorderModule: any = null;
|
|
11
|
-
|
|
12
15
|
/**
|
|
13
16
|
* Capture the current screen as a PNG image.
|
|
14
17
|
* Requires `react-native-view-shot` to be installed.
|
|
15
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Note: the feedback modal should hide itself *before* calling this so the
|
|
20
|
+
* screenshot contains the underlying app state (the actual bug), not the
|
|
21
|
+
* modal. See `FeedbackModal.handleScreenshotForFix`.
|
|
16
22
|
*/
|
|
17
23
|
export async function captureScreenshot(): Promise<string> {
|
|
18
24
|
try {
|
|
@@ -24,61 +30,91 @@ export async function captureScreenshot(): Promise<string> {
|
|
|
24
30
|
return uri;
|
|
25
31
|
} catch (err) {
|
|
26
32
|
throw new Error(
|
|
27
|
-
'[YaverFeedback] Screenshot capture failed.
|
|
33
|
+
'[YaverFeedback] Screenshot capture failed. Install react-native-view-shot as a peer dep. ' +
|
|
28
34
|
String(err),
|
|
29
35
|
);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
let videoRecorderModule: any = null;
|
|
40
|
+
let videoRecordingActive = false;
|
|
41
|
+
|
|
33
42
|
/**
|
|
34
|
-
* Start recording
|
|
35
|
-
*
|
|
43
|
+
* Start a screen-recording session. Requires
|
|
44
|
+
* `react-native-record-screen` as a peer dep.
|
|
45
|
+
*
|
|
46
|
+
* The user must grant the iOS ReplayKit / Android MediaProjection
|
|
47
|
+
* permission the first time; the prompt is shown by the native module,
|
|
48
|
+
* not the SDK.
|
|
36
49
|
*/
|
|
37
|
-
export async function
|
|
50
|
+
export async function startVideoRecording(): Promise<void> {
|
|
51
|
+
if (videoRecordingActive) {
|
|
52
|
+
throw new Error('[YaverFeedback] A video recording is already in progress.');
|
|
53
|
+
}
|
|
38
54
|
try {
|
|
39
|
-
|
|
40
|
-
require('react-native-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
videoRecorderModule = require('react-native-record-screen').default ??
|
|
56
|
+
require('react-native-record-screen');
|
|
57
|
+
if (typeof videoRecorderModule.startRecording !== 'function') {
|
|
58
|
+
throw new Error('react-native-record-screen missing startRecording()');
|
|
59
|
+
}
|
|
60
|
+
const result = await videoRecorderModule.startRecording({
|
|
61
|
+
mic: false,
|
|
62
|
+
width: 720,
|
|
63
|
+
bitrate: 1024 * 1000,
|
|
64
|
+
});
|
|
65
|
+
if (result && result.status && result.status !== 'success') {
|
|
66
|
+
throw new Error(`startRecording returned ${result.status}`);
|
|
67
|
+
}
|
|
68
|
+
videoRecordingActive = true;
|
|
43
69
|
} catch (err) {
|
|
44
|
-
|
|
70
|
+
videoRecorderModule = null;
|
|
71
|
+
videoRecordingActive = false;
|
|
45
72
|
throw new Error(
|
|
46
|
-
'[YaverFeedback]
|
|
73
|
+
'[YaverFeedback] Could not start screen recording. Install react-native-record-screen. ' +
|
|
47
74
|
String(err),
|
|
48
75
|
);
|
|
49
76
|
}
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
/**
|
|
53
|
-
* Stop the current
|
|
54
|
-
* @returns Object with the file path and duration in seconds.
|
|
80
|
+
* Stop the current video recording and return the on-device file path.
|
|
55
81
|
*/
|
|
56
|
-
export async function
|
|
82
|
+
export async function stopVideoRecording(): Promise<{
|
|
57
83
|
path: string;
|
|
58
84
|
duration: number;
|
|
59
85
|
}> {
|
|
60
|
-
if (!
|
|
61
|
-
throw new Error('[YaverFeedback] No
|
|
86
|
+
if (!videoRecordingActive || !videoRecorderModule) {
|
|
87
|
+
throw new Error('[YaverFeedback] No video recording in progress.');
|
|
62
88
|
}
|
|
63
|
-
|
|
64
89
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
const res = await videoRecorderModule.stopRecording();
|
|
91
|
+
videoRecordingActive = false;
|
|
92
|
+
const path =
|
|
93
|
+
typeof res === 'string'
|
|
94
|
+
? res
|
|
95
|
+
: (res?.result?.outputURL as string) ??
|
|
96
|
+
(res?.outputURL as string) ??
|
|
97
|
+
(res?.uri as string) ??
|
|
98
|
+
'';
|
|
72
99
|
const durationMs =
|
|
73
|
-
typeof
|
|
74
|
-
?
|
|
75
|
-
:
|
|
76
|
-
|
|
100
|
+
typeof res?.result?.duration === 'number'
|
|
101
|
+
? res.result.duration
|
|
102
|
+
: typeof res?.duration === 'number'
|
|
103
|
+
? res.duration
|
|
104
|
+
: 0;
|
|
105
|
+
if (!path) {
|
|
106
|
+
throw new Error('stopRecording() returned no file path');
|
|
107
|
+
}
|
|
77
108
|
return { path, duration: durationMs / 1000 };
|
|
78
109
|
} catch (err) {
|
|
79
|
-
|
|
110
|
+
videoRecordingActive = false;
|
|
80
111
|
throw new Error(
|
|
81
|
-
'[YaverFeedback] Failed to stop
|
|
112
|
+
'[YaverFeedback] Failed to stop screen recording. ' + String(err),
|
|
82
113
|
);
|
|
83
114
|
}
|
|
84
115
|
}
|
|
116
|
+
|
|
117
|
+
/** Whether a video recording is currently active. */
|
|
118
|
+
export function isVideoRecording(): boolean {
|
|
119
|
+
return videoRecordingActive;
|
|
120
|
+
}
|
package/src/expo.ts
CHANGED
|
@@ -31,7 +31,6 @@ import type { FeedbackConfig } from './types';
|
|
|
31
31
|
*
|
|
32
32
|
* Defaults:
|
|
33
33
|
* - trigger: 'shake'
|
|
34
|
-
* - feedbackMode: 'batch'
|
|
35
34
|
* - enabled: __DEV__ (only active in development)
|
|
36
35
|
*
|
|
37
36
|
* @param overrides - Optional partial config to override defaults
|
|
@@ -54,7 +53,6 @@ export function initExpo(overrides?: Partial<FeedbackConfig>): void {
|
|
|
54
53
|
YaverFeedback.init({
|
|
55
54
|
authToken: '', // LAN auto-discovery doesn't require a token
|
|
56
55
|
trigger: 'shake',
|
|
57
|
-
feedbackMode: 'batch',
|
|
58
56
|
enabled: __DEV__,
|
|
59
57
|
...overrides,
|
|
60
58
|
...(agentUrl ? { agentUrl } : {}),
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* yaver-feedback-react-native — Visual feedback SDK for Yaver.
|
|
3
3
|
*
|
|
4
|
-
* Shake-to-report
|
|
5
|
-
*
|
|
4
|
+
* Shake-to-report surface with five one-tap actions:
|
|
5
|
+
* 1. Hot Reload — instant JS reload
|
|
6
|
+
* 2. Screenshot & Fix — capture the screen under the modal and
|
|
7
|
+
* kick a fix task on the agent
|
|
8
|
+
* 3. Vibing — open a vibing session on the agent
|
|
9
|
+
* 4. Start / Stop Recording — screen recording toggle
|
|
10
|
+
* 5. Send Video — submit the last recording
|
|
6
11
|
*
|
|
7
12
|
* @example
|
|
8
13
|
* ```tsx
|
|
9
|
-
* import { YaverFeedback,
|
|
14
|
+
* import { YaverFeedback, FeedbackModal } from 'yaver-feedback-react-native';
|
|
10
15
|
*
|
|
11
16
|
* YaverFeedback.init({
|
|
12
17
|
* agentUrl: 'http://192.168.1.10:18080',
|
|
13
18
|
* authToken: 'your-token',
|
|
14
19
|
* trigger: 'shake',
|
|
15
|
-
*
|
|
20
|
+
* strictNativeAuth: true,
|
|
16
21
|
* });
|
|
17
22
|
*
|
|
18
|
-
*
|
|
19
|
-
* <FeedbackProvider>
|
|
23
|
+
* <>
|
|
20
24
|
* <App />
|
|
21
|
-
*
|
|
25
|
+
* <FeedbackModal />
|
|
26
|
+
* </>
|
|
22
27
|
* ```
|
|
23
28
|
*/
|
|
24
29
|
|
|
@@ -67,7 +72,12 @@ export type {
|
|
|
67
72
|
RemoteDevice,
|
|
68
73
|
DeviceList,
|
|
69
74
|
} from './auth';
|
|
70
|
-
export {
|
|
75
|
+
export {
|
|
76
|
+
captureScreenshot,
|
|
77
|
+
startVideoRecording,
|
|
78
|
+
stopVideoRecording,
|
|
79
|
+
isVideoRecording,
|
|
80
|
+
} from './capture';
|
|
71
81
|
export { uploadFeedback } from './upload';
|
|
72
82
|
export type {
|
|
73
83
|
FeedbackConfig,
|
|
@@ -77,7 +87,6 @@ export type {
|
|
|
77
87
|
AppInfo,
|
|
78
88
|
TimelineEvent,
|
|
79
89
|
FeedbackReport,
|
|
80
|
-
AgentCommentary,
|
|
81
90
|
FeedbackStreamEvent,
|
|
82
91
|
VoiceCapability,
|
|
83
92
|
CapturedError,
|
package/src/types.ts
CHANGED
|
@@ -65,27 +65,6 @@ export interface FeedbackConfig {
|
|
|
65
65
|
reportingOnly?: boolean;
|
|
66
66
|
/** Max screen recording duration in seconds. Default: 120 */
|
|
67
67
|
maxRecordingDuration?: number;
|
|
68
|
-
/**
|
|
69
|
-
* Feedback mode:
|
|
70
|
-
* - 'live': stream events to the agent as they happen
|
|
71
|
-
* - 'narrated': record everything, send on stop
|
|
72
|
-
* - 'batch': dump everything at end (default)
|
|
73
|
-
*/
|
|
74
|
-
feedbackMode?: 'live' | 'narrated' | 'batch';
|
|
75
|
-
/**
|
|
76
|
-
* Agent commentary level (0-10).
|
|
77
|
-
* 0 = silent, 10 = agent comments on everything it sees.
|
|
78
|
-
* Only relevant in live mode. Default: 0.
|
|
79
|
-
*/
|
|
80
|
-
agentCommentaryLevel?: number;
|
|
81
|
-
/**
|
|
82
|
-
* Enable voice input for feedback annotations. Always true by default.
|
|
83
|
-
* Audio is recorded on the device and sent to the agent for transcription.
|
|
84
|
-
* Works regardless of whether a speech-to-speech provider is configured —
|
|
85
|
-
* if STT is available on the agent, audio is auto-transcribed; otherwise
|
|
86
|
-
* raw audio is attached to the feedback report.
|
|
87
|
-
*/
|
|
88
|
-
voiceEnabled?: boolean;
|
|
89
68
|
/**
|
|
90
69
|
* Maximum number of captured errors to keep in memory (ring buffer).
|
|
91
70
|
* Oldest errors are evicted when the buffer is full.
|
|
@@ -180,13 +159,10 @@ export interface FeedbackConfig {
|
|
|
180
159
|
|
|
181
160
|
export interface FeedbackBundle {
|
|
182
161
|
metadata: FeedbackMetadata;
|
|
162
|
+
/** Screen-recording file path, when produced by the "Start Recording" action. */
|
|
183
163
|
video?: string;
|
|
184
|
-
/** Voice annotation audio file path (WAV). Always available when voiceEnabled. */
|
|
185
|
-
audio?: string;
|
|
186
|
-
/** Transcribed text from voice annotation (if STT/S2S provider is available on agent). */
|
|
187
|
-
audioTranscript?: string;
|
|
188
164
|
screenshots: string[];
|
|
189
|
-
/** Captured errors with stack traces, attached
|
|
165
|
+
/** Captured errors with stack traces, attached via attachError / wrapErrorHandler. */
|
|
190
166
|
errors?: CapturedError[];
|
|
191
167
|
}
|
|
192
168
|
|
|
@@ -239,13 +215,6 @@ export interface FeedbackReport {
|
|
|
239
215
|
error?: string;
|
|
240
216
|
}
|
|
241
217
|
|
|
242
|
-
export interface AgentCommentary {
|
|
243
|
-
id: string;
|
|
244
|
-
timestamp: string;
|
|
245
|
-
message: string;
|
|
246
|
-
type: 'observation' | 'suggestion' | 'question' | 'action';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
218
|
export interface FeedbackStreamEvent {
|
|
250
219
|
type: string;
|
|
251
220
|
timestamp: string;
|
package/src/upload.ts
CHANGED
|
@@ -7,16 +7,17 @@ import { FeedbackBundle } from './types';
|
|
|
7
7
|
* The agent receives the bundle at POST /feedback with:
|
|
8
8
|
* - `metadata` (JSON string)
|
|
9
9
|
* - `screenshot_0`, `screenshot_1`, ... (image files)
|
|
10
|
-
* - `audio` (audio file, if present)
|
|
11
10
|
* - `video` (video file, if present)
|
|
12
11
|
*
|
|
13
|
-
*
|
|
12
|
+
* Returns the parsed agent response — typically `{ ok, id, reportId }`.
|
|
13
|
+
* Callers can inspect `.id` / `.reportId` to drive a follow-up
|
|
14
|
+
* `/feedback/{id}/fix` kick.
|
|
14
15
|
*/
|
|
15
16
|
export async function uploadFeedback(
|
|
16
17
|
agentUrl: string,
|
|
17
18
|
authToken: string,
|
|
18
19
|
bundle: FeedbackBundle,
|
|
19
|
-
): Promise<string> {
|
|
20
|
+
): Promise<{ id?: string; reportId?: string; [k: string]: unknown }> {
|
|
20
21
|
const formData = new FormData();
|
|
21
22
|
|
|
22
23
|
// Attach metadata as JSON
|
|
@@ -32,16 +33,6 @@ export async function uploadFeedback(
|
|
|
32
33
|
} as any);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// Attach audio
|
|
36
|
-
if (bundle.audio) {
|
|
37
|
-
formData.append('audio', {
|
|
38
|
-
uri:
|
|
39
|
-
Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
|
|
40
|
-
type: 'audio/m4a',
|
|
41
|
-
name: 'voice_note.m4a',
|
|
42
|
-
} as any);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
36
|
// Attach video
|
|
46
37
|
if (bundle.video) {
|
|
47
38
|
formData.append('video', {
|
|
@@ -69,6 +60,6 @@ export async function uploadFeedback(
|
|
|
69
60
|
);
|
|
70
61
|
}
|
|
71
62
|
|
|
72
|
-
const result = await response.json();
|
|
73
|
-
return result
|
|
63
|
+
const result = await response.json().catch(() => ({}));
|
|
64
|
+
return result as { id?: string; reportId?: string; [k: string]: unknown };
|
|
74
65
|
}
|