yaver-feedback-react-native 0.6.1 → 0.7.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/dist/Discovery.d.ts +40 -27
- package/dist/Discovery.js +164 -70
- package/dist/FeedbackModal.d.ts +0 -9
- package/dist/FeedbackModal.js +292 -473
- package/dist/MachinePickerScreen.js +5 -1
- package/dist/P2PClient.d.ts +24 -0
- package/dist/P2PClient.js +65 -10
- package/dist/YaverFeedback.d.ts +10 -4
- package/dist/YaverFeedback.js +30 -10
- package/dist/__tests__/YaverFeedback.test.js +2 -35
- package/dist/__tests__/types.test.js +3 -35
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +36 -3
- package/dist/capture.d.ts +25 -12
- package/dist/capture.js +73 -34
- package/dist/deviceDedup.d.ts +44 -0
- package/dist/deviceDedup.js +184 -0
- 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/Discovery.ts +188 -101
- package/src/FeedbackModal.tsx +409 -564
- package/src/MachinePickerScreen.tsx +6 -1
- package/src/P2PClient.ts +66 -12
- package/src/YaverFeedback.ts +27 -12
- package/src/__tests__/YaverFeedback.test.ts +2 -41
- package/src/__tests__/types.test.ts +3 -39
- package/src/auth.ts +46 -3
- package/src/capture.ts +73 -37
- package/src/deviceDedup.ts +184 -0
- 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/dist/FeedbackModal.js
CHANGED
|
@@ -37,233 +37,253 @@ exports.FeedbackModal = void 0;
|
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
39
|
const YaverFeedback_1 = require("./YaverFeedback");
|
|
40
|
-
const BlackBox_1 = require("./BlackBox");
|
|
41
40
|
const capture_1 = require("./capture");
|
|
42
41
|
const upload_1 = require("./upload");
|
|
43
42
|
const AuthOverlay_1 = require("./AuthOverlay");
|
|
44
|
-
const MODE_LABELS = {
|
|
45
|
-
live: 'Live',
|
|
46
|
-
narrated: 'Narrated',
|
|
47
|
-
batch: 'Batch',
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Full-screen modal for composing and sending a feedback report.
|
|
51
|
-
* Renders when triggered by shake, floating button, or manual call.
|
|
52
|
-
*
|
|
53
|
-
* Supports three feedback modes:
|
|
54
|
-
* - Live: stream events to the agent as they happen
|
|
55
|
-
* - Narrated: record everything, send on stop
|
|
56
|
-
* - Batch: dump everything at end (default)
|
|
57
|
-
*/
|
|
58
43
|
const FeedbackModal = () => {
|
|
59
44
|
const [visible, setVisible] = (0, react_1.useState)(false);
|
|
60
|
-
const [
|
|
61
|
-
const [isRecordingAudio, setIsRecordingAudio] = (0, react_1.useState)(false);
|
|
62
|
-
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
45
|
+
const [action, setAction] = (0, react_1.useState)('idle');
|
|
63
46
|
const [error, setError] = (0, react_1.useState)(null);
|
|
64
|
-
const [
|
|
65
|
-
const [
|
|
66
|
-
const [
|
|
67
|
-
const [isVoiceCommand, setIsVoiceCommand] = (0, react_1.useState)(false);
|
|
68
|
-
const [isReloading, setIsReloading] = (0, react_1.useState)(false);
|
|
47
|
+
const [toast, setToast] = (0, react_1.useState)(null);
|
|
48
|
+
const [isRecordingVideo, setIsRecordingVideo] = (0, react_1.useState)(false);
|
|
49
|
+
const [lastVideo, setLastVideo] = (0, react_1.useState)(null);
|
|
69
50
|
const mountedRef = (0, react_1.useRef)(true);
|
|
70
|
-
const commentaryListRef = (0, react_1.useRef)(null);
|
|
71
51
|
(0, react_1.useEffect)(() => {
|
|
72
52
|
mountedRef.current = true;
|
|
73
53
|
const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
74
54
|
if (YaverFeedback_1.YaverFeedback.isEnabled()) {
|
|
75
55
|
setVisible(true);
|
|
76
|
-
setTimeline([]);
|
|
77
56
|
setError(null);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
setMode(YaverFeedback_1.YaverFeedback.getFeedbackMode());
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
// Listen for agent commentary events
|
|
84
|
-
const commentarySub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:commentary', (event) => {
|
|
85
|
-
if (mountedRef.current) {
|
|
86
|
-
setCommentary((prev) => [...prev, event]);
|
|
57
|
+
setToast(null);
|
|
58
|
+
setAction('idle');
|
|
87
59
|
}
|
|
88
60
|
});
|
|
89
61
|
return () => {
|
|
90
62
|
mountedRef.current = false;
|
|
91
63
|
sub.remove();
|
|
92
|
-
commentarySub.remove();
|
|
93
64
|
};
|
|
94
65
|
}, []);
|
|
95
|
-
const
|
|
66
|
+
const closeSoon = (0, react_1.useCallback)((delayMs = 1200) => {
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
if (mountedRef.current)
|
|
69
|
+
setVisible(false);
|
|
70
|
+
}, delayMs);
|
|
71
|
+
}, []);
|
|
72
|
+
const handleClose = (0, react_1.useCallback)(() => {
|
|
73
|
+
setVisible(false);
|
|
74
|
+
setError(null);
|
|
75
|
+
setToast(null);
|
|
76
|
+
setAction('idle');
|
|
77
|
+
}, []);
|
|
78
|
+
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
79
|
+
// re-query Convex for the fresh IP and retry once. Solves the common
|
|
80
|
+
// case where the Mac's LAN IP rotated while the SDK held a stale URL.
|
|
81
|
+
const runWithReconnect = (0, react_1.useCallback)(async (fn) => {
|
|
82
|
+
let client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
83
|
+
if (!client) {
|
|
84
|
+
const ok = await YaverFeedback_1.YaverFeedback.reconnect();
|
|
85
|
+
if (ok)
|
|
86
|
+
client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
87
|
+
}
|
|
88
|
+
if (!client) {
|
|
89
|
+
throw new Error('Not connected to the agent yet.');
|
|
90
|
+
}
|
|
96
91
|
try {
|
|
97
|
-
|
|
98
|
-
if (mountedRef.current) {
|
|
99
|
-
const event = {
|
|
100
|
-
type: 'screenshot',
|
|
101
|
-
path,
|
|
102
|
-
timestamp: new Date().toISOString(),
|
|
103
|
-
};
|
|
104
|
-
setTimeline((prev) => [...prev, event]);
|
|
105
|
-
// In live mode, stream the event immediately
|
|
106
|
-
if (mode === 'live') {
|
|
107
|
-
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
108
|
-
if (client) {
|
|
109
|
-
try {
|
|
110
|
-
await client.streamFeedback((async function* () {
|
|
111
|
-
yield {
|
|
112
|
-
type: 'screenshot',
|
|
113
|
-
timestamp: event.timestamp,
|
|
114
|
-
data: { path },
|
|
115
|
-
};
|
|
116
|
-
})());
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
console.warn('[YaverFeedback] Live stream failed:', err);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
92
|
+
await fn(client);
|
|
124
93
|
}
|
|
125
94
|
catch (err) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
const transient = /Network request failed|timeout|ECONNREFUSED|Failed to fetch|fetch failed|aborted/i.test(msg);
|
|
97
|
+
if (!transient)
|
|
98
|
+
throw err;
|
|
99
|
+
const ok = await YaverFeedback_1.YaverFeedback.reconnect();
|
|
100
|
+
if (!ok)
|
|
101
|
+
throw err;
|
|
102
|
+
const fresh = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
103
|
+
if (!fresh)
|
|
104
|
+
throw err;
|
|
105
|
+
await fn(fresh);
|
|
129
106
|
}
|
|
130
|
-
}, [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
duration: result.duration,
|
|
142
|
-
};
|
|
143
|
-
setTimeline((prev) => [...prev, event]);
|
|
144
|
-
// In live mode, stream the audio event
|
|
145
|
-
if (mode === 'live') {
|
|
146
|
-
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
147
|
-
if (client) {
|
|
148
|
-
try {
|
|
149
|
-
await client.streamFeedback((async function* () {
|
|
150
|
-
yield {
|
|
151
|
-
type: 'audio',
|
|
152
|
-
timestamp: event.timestamp,
|
|
153
|
-
data: { path: result.path, duration: result.duration },
|
|
154
|
-
};
|
|
155
|
-
})());
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
console.warn('[YaverFeedback] Live stream failed:', err);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
if (mountedRef.current) {
|
|
166
|
-
setIsRecordingAudio(false);
|
|
167
|
-
setError(String(err));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
107
|
+
}, []);
|
|
108
|
+
// ─── 1. Hot reload ─────────────────────────────────────────────────
|
|
109
|
+
const handleHotReload = (0, react_1.useCallback)(async () => {
|
|
110
|
+
setAction('hot-reloading');
|
|
111
|
+
setError(null);
|
|
112
|
+
try {
|
|
113
|
+
await runWithReconnect(async (client) => {
|
|
114
|
+
await client.reloadApp('dev');
|
|
115
|
+
});
|
|
116
|
+
setToast('Reload sent');
|
|
117
|
+
closeSoon(800);
|
|
170
118
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
119
|
+
catch (err) {
|
|
120
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (mountedRef.current)
|
|
124
|
+
setAction('idle');
|
|
125
|
+
}
|
|
126
|
+
}, [closeSoon, runWithReconnect]);
|
|
127
|
+
// ─── 2. Screenshot + Fix ───────────────────────────────────────────
|
|
128
|
+
//
|
|
129
|
+
// Hide the modal first so the screenshot captures the actual screen
|
|
130
|
+
// (the bug) — not the modal card. Await a short animation delay,
|
|
131
|
+
// snapshot, upload the feedback bundle with any buffered errors, then
|
|
132
|
+
// kick `/feedback/{id}/fix` to create the repair task.
|
|
133
|
+
const handleScreenshotAndFix = (0, react_1.useCallback)(async () => {
|
|
134
|
+
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
135
|
+
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
136
|
+
if (!client || !config?.agentUrl) {
|
|
137
|
+
setError('Not connected to the agent yet.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
setAction('capturing');
|
|
141
|
+
setError(null);
|
|
142
|
+
// Step 1: Hide the modal so the screenshot contains the real screen.
|
|
143
|
+
setVisible(false);
|
|
144
|
+
// Wait out the slide-down animation on both platforms.
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
146
|
+
let path = null;
|
|
147
|
+
try {
|
|
148
|
+
path = await (0, capture_1.captureScreenshot)();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
setVisible(true);
|
|
152
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
153
|
+
setAction('idle');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Step 2: Re-show the modal for progress + ack.
|
|
157
|
+
setVisible(true);
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
159
|
+
try {
|
|
160
|
+
const { Dimensions } = require('react-native');
|
|
161
|
+
const { width, height } = Dimensions.get('window');
|
|
162
|
+
const deviceInfo = {
|
|
163
|
+
platform: react_native_1.Platform.OS,
|
|
164
|
+
osVersion: String(react_native_1.Platform.Version),
|
|
165
|
+
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
166
|
+
screenWidth: width,
|
|
167
|
+
screenHeight: height,
|
|
168
|
+
};
|
|
169
|
+
const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
|
|
170
|
+
const bundle = {
|
|
171
|
+
metadata: {
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
device: deviceInfo,
|
|
174
|
+
app: {},
|
|
175
|
+
userNote: '[Screenshot + Fix]',
|
|
176
|
+
},
|
|
177
|
+
screenshots: [path],
|
|
178
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
179
|
+
};
|
|
180
|
+
const uploaded = await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
181
|
+
// The agent returns the new report id as `id` (see
|
|
182
|
+
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
183
|
+
// one back; otherwise just ack the upload.
|
|
184
|
+
const reportId = uploaded?.id ??
|
|
185
|
+
uploaded?.reportId;
|
|
186
|
+
if (reportId) {
|
|
187
|
+
try {
|
|
188
|
+
await client.triggerFix(reportId);
|
|
189
|
+
setToast('Fix task started');
|
|
176
190
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
setError(String(err));
|
|
191
|
+
catch (err) {
|
|
192
|
+
setToast('Report uploaded — fix trigger failed');
|
|
193
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
181
194
|
}
|
|
182
195
|
}
|
|
196
|
+
else {
|
|
197
|
+
setToast('Report uploaded');
|
|
198
|
+
}
|
|
199
|
+
closeSoon(1400);
|
|
183
200
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
201
|
+
catch (err) {
|
|
202
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
if (mountedRef.current)
|
|
206
|
+
setAction('idle');
|
|
207
|
+
}
|
|
208
|
+
}, [closeSoon]);
|
|
209
|
+
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
210
|
+
const handleVibing = (0, react_1.useCallback)(async () => {
|
|
211
|
+
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
212
|
+
if (!client) {
|
|
213
|
+
setError('Not connected to the agent yet.');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
setAction('vibing');
|
|
217
|
+
setError(null);
|
|
218
|
+
try {
|
|
219
|
+
const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
|
|
220
|
+
const errNote = capturedErrors.length > 0
|
|
221
|
+
? `\n\nRecent captured errors:\n` +
|
|
222
|
+
capturedErrors
|
|
223
|
+
.slice(-3)
|
|
224
|
+
.map((e) => `- ${e.message}`)
|
|
225
|
+
.join('\n')
|
|
226
|
+
: '';
|
|
227
|
+
const prompt = 'The user opened the feedback modal on their phone and tapped Vibing. ' +
|
|
228
|
+
'Investigate whatever they are likely to be asking about — pick the ' +
|
|
229
|
+
'next small improvement or fix based on recent activity and the ' +
|
|
230
|
+
'current screen.' +
|
|
231
|
+
errNote;
|
|
232
|
+
await client.vibing(prompt);
|
|
233
|
+
setToast('Vibing task created');
|
|
234
|
+
closeSoon(1200);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
if (mountedRef.current)
|
|
241
|
+
setAction('idle');
|
|
242
|
+
}
|
|
243
|
+
}, [closeSoon]);
|
|
244
|
+
// ─── 4. Toggle screen recording ────────────────────────────────────
|
|
245
|
+
const handleToggleRecording = (0, react_1.useCallback)(async () => {
|
|
246
|
+
setError(null);
|
|
247
|
+
if (isRecordingVideo) {
|
|
188
248
|
try {
|
|
189
|
-
const result = await (0, capture_1.
|
|
249
|
+
const result = await (0, capture_1.stopVideoRecording)();
|
|
190
250
|
if (mountedRef.current) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
await client.streamFeedback((async function* () {
|
|
196
|
-
yield {
|
|
197
|
-
type: 'voice_command',
|
|
198
|
-
timestamp: new Date().toISOString(),
|
|
199
|
-
data: { path: result.path, duration: result.duration },
|
|
200
|
-
};
|
|
201
|
-
})());
|
|
202
|
-
}
|
|
203
|
-
catch (err) {
|
|
204
|
-
console.warn('[YaverFeedback] Voice command send failed:', err);
|
|
205
|
-
if (mountedRef.current) {
|
|
206
|
-
setError('Failed to send voice command.');
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
251
|
+
setIsRecordingVideo(false);
|
|
252
|
+
setLastVideo(result);
|
|
253
|
+
setToast(`Recording stopped — ${Math.round(result.duration)}s`);
|
|
210
254
|
}
|
|
211
255
|
}
|
|
212
256
|
catch (err) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
setError(String(err));
|
|
216
|
-
}
|
|
257
|
+
setIsRecordingVideo(false);
|
|
258
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
217
259
|
}
|
|
218
260
|
}
|
|
219
261
|
else {
|
|
220
262
|
try {
|
|
221
|
-
await (0, capture_1.
|
|
263
|
+
await (0, capture_1.startVideoRecording)();
|
|
222
264
|
if (mountedRef.current) {
|
|
223
|
-
|
|
265
|
+
setIsRecordingVideo(true);
|
|
266
|
+
setToast('Recording…');
|
|
267
|
+
setLastVideo(null);
|
|
224
268
|
}
|
|
225
269
|
}
|
|
226
270
|
catch (err) {
|
|
227
|
-
|
|
228
|
-
setError(String(err));
|
|
229
|
-
}
|
|
271
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
230
272
|
}
|
|
231
273
|
}
|
|
232
|
-
}, [
|
|
233
|
-
|
|
274
|
+
}, [isRecordingVideo]);
|
|
275
|
+
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
276
|
+
const handleSendVideo = (0, react_1.useCallback)(async () => {
|
|
234
277
|
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
235
|
-
if (!config?.agentUrl)
|
|
278
|
+
if (!config?.agentUrl) {
|
|
279
|
+
setError('Not connected to the agent yet.');
|
|
236
280
|
return;
|
|
237
|
-
setIsReloading(true);
|
|
238
|
-
try {
|
|
239
|
-
const response = await fetch(`${config.agentUrl.replace(/\/$/, '')}/dev/reload-app`, {
|
|
240
|
-
method: 'POST',
|
|
241
|
-
headers: {
|
|
242
|
-
Authorization: `Bearer ${config.authToken}`,
|
|
243
|
-
'Content-Type': 'application/json',
|
|
244
|
-
},
|
|
245
|
-
body: JSON.stringify({ mode: 'dev' }),
|
|
246
|
-
});
|
|
247
|
-
if (response.ok) {
|
|
248
|
-
BlackBox_1.BlackBox.lifecycle('Hot reload triggered from feedback SDK');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
catch (err) {
|
|
252
|
-
if (mountedRef.current) {
|
|
253
|
-
setError('Reload failed: ' + String(err));
|
|
254
|
-
}
|
|
255
281
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
setIsReloading(false);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}, []);
|
|
262
|
-
const handleSend = (0, react_1.useCallback)(async () => {
|
|
263
|
-
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
264
|
-
if (!config || !config.agentUrl)
|
|
282
|
+
if (!lastVideo) {
|
|
283
|
+
setError('No video recorded yet.');
|
|
265
284
|
return;
|
|
266
|
-
|
|
285
|
+
}
|
|
286
|
+
setAction('sending-video');
|
|
267
287
|
setError(null);
|
|
268
288
|
try {
|
|
269
289
|
const { Dimensions } = require('react-native');
|
|
@@ -275,349 +295,148 @@ const FeedbackModal = () => {
|
|
|
275
295
|
screenWidth: width,
|
|
276
296
|
screenHeight: height,
|
|
277
297
|
};
|
|
278
|
-
const screenshots = timeline
|
|
279
|
-
.filter((e) => e.type === 'screenshot')
|
|
280
|
-
.map((e) => e.path);
|
|
281
|
-
const audioEvent = timeline.find((e) => e.type === 'audio');
|
|
282
|
-
// Include captured errors from the error buffer
|
|
283
|
-
const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
|
|
284
298
|
const bundle = {
|
|
285
299
|
metadata: {
|
|
286
300
|
timestamp: new Date().toISOString(),
|
|
287
301
|
device: deviceInfo,
|
|
288
302
|
app: {},
|
|
303
|
+
userNote: '[Screen recording]',
|
|
289
304
|
},
|
|
290
|
-
screenshots,
|
|
291
|
-
|
|
292
|
-
errors:
|
|
305
|
+
screenshots: [],
|
|
306
|
+
video: lastVideo.path,
|
|
307
|
+
errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
|
|
308
|
+
? YaverFeedback_1.YaverFeedback.getCapturedErrors()
|
|
309
|
+
: undefined,
|
|
293
310
|
};
|
|
294
|
-
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken, bundle);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
setTimeout(() => {
|
|
299
|
-
if (mountedRef.current) {
|
|
300
|
-
setVisible(false);
|
|
301
|
-
}
|
|
302
|
-
}, 1500);
|
|
303
|
-
}
|
|
311
|
+
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
|
|
312
|
+
setToast('Video sent');
|
|
313
|
+
setLastVideo(null);
|
|
314
|
+
closeSoon(1200);
|
|
304
315
|
}
|
|
305
316
|
catch (err) {
|
|
306
|
-
|
|
307
|
-
setError(String(err));
|
|
308
|
-
}
|
|
317
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
309
318
|
}
|
|
310
319
|
finally {
|
|
311
|
-
if (mountedRef.current)
|
|
312
|
-
|
|
313
|
-
}
|
|
320
|
+
if (mountedRef.current)
|
|
321
|
+
setAction('idle');
|
|
314
322
|
}
|
|
315
|
-
}, [
|
|
316
|
-
const
|
|
317
|
-
setVisible(false);
|
|
318
|
-
setTimeline([]);
|
|
319
|
-
setError(null);
|
|
320
|
-
setSent(false);
|
|
321
|
-
setIsRecordingAudio(false);
|
|
322
|
-
setIsVoiceCommand(false);
|
|
323
|
-
setCommentary([]);
|
|
324
|
-
}, []);
|
|
325
|
-
const renderCommentaryItem = (0, react_1.useCallback)(({ item }) => (<react_native_1.View style={styles.commentaryBubble}>
|
|
326
|
-
<react_native_1.Text style={styles.commentaryType}>{item.type}</react_native_1.Text>
|
|
327
|
-
<react_native_1.Text style={styles.commentaryMessage}>{item.message}</react_native_1.Text>
|
|
328
|
-
</react_native_1.View>), []);
|
|
329
|
-
// The AuthOverlay must stay mounted so it can respond to login / picker
|
|
330
|
-
// events even when the feedback modal itself isn't visible. The wrapping
|
|
331
|
-
// fragment keeps the original return shape intact for the modal branch.
|
|
323
|
+
}, [lastVideo, closeSoon]);
|
|
324
|
+
const busy = action !== 'idle';
|
|
332
325
|
return (<>
|
|
333
326
|
<AuthOverlay_1.AuthOverlay />
|
|
334
|
-
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{MODE_LABELS[m]}
|
|
344
|
-
</react_native_1.Text>
|
|
345
|
-
</react_native_1.TouchableOpacity>))}
|
|
346
|
-
</react_native_1.View>
|
|
347
|
-
|
|
348
|
-
{/* Agent commentary (chat-like view) */}
|
|
349
|
-
{commentary.length > 0 && (<react_native_1.FlatList ref={commentaryListRef} data={commentary} renderItem={renderCommentaryItem} keyExtractor={(item) => item.id} style={styles.commentaryList} onContentSizeChange={() => commentaryListRef.current?.scrollToEnd({ animated: true })}/>)}
|
|
350
|
-
|
|
351
|
-
{/* Timeline of captured items */}
|
|
352
|
-
{timeline.length > 0 && (<react_native_1.ScrollView style={styles.timeline} horizontal>
|
|
353
|
-
{timeline.map((event, idx) => (<react_native_1.View key={idx} style={styles.timelineItem}>
|
|
354
|
-
<react_native_1.Text style={styles.timelineIcon}>
|
|
355
|
-
{event.type === 'screenshot'
|
|
356
|
-
? '[img]'
|
|
357
|
-
: event.type === 'audio'
|
|
358
|
-
? '[mic]'
|
|
359
|
-
: '[vid]'}
|
|
360
|
-
</react_native_1.Text>
|
|
361
|
-
<react_native_1.Text style={styles.timelineLabel}>{event.type}</react_native_1.Text>
|
|
362
|
-
{event.duration != null && (<react_native_1.Text style={styles.timelineDuration}>
|
|
363
|
-
{Math.round(event.duration)}s
|
|
364
|
-
</react_native_1.Text>)}
|
|
365
|
-
</react_native_1.View>))}
|
|
366
|
-
</react_native_1.ScrollView>)}
|
|
367
|
-
|
|
368
|
-
{/* Action buttons */}
|
|
369
|
-
<react_native_1.View style={styles.actions}>
|
|
370
|
-
<react_native_1.TouchableOpacity style={styles.actionButton} onPress={handleScreenshot}>
|
|
371
|
-
<react_native_1.Text style={styles.actionText}>Take Screenshot</react_native_1.Text>
|
|
372
|
-
</react_native_1.TouchableOpacity>
|
|
327
|
+
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
|
|
328
|
+
<react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
|
|
329
|
+
<react_native_1.Pressable style={styles.modal} onPress={(e) => e.stopPropagation()}>
|
|
330
|
+
<react_native_1.View style={styles.header}>
|
|
331
|
+
<react_native_1.Text style={styles.title}>Send Feedback</react_native_1.Text>
|
|
332
|
+
<react_native_1.Pressable onPress={handleClose} hitSlop={12} style={styles.closeBtn} accessibilityRole="button" accessibilityLabel="Close">
|
|
333
|
+
<react_native_1.Text style={styles.closeIcon}>×</react_native_1.Text>
|
|
334
|
+
</react_native_1.Pressable>
|
|
335
|
+
</react_native_1.View>
|
|
373
336
|
|
|
374
|
-
|
|
375
|
-
<
|
|
376
|
-
{isRecordingAudio ? 'Stop Recording' : 'Voice Note'}
|
|
377
|
-
</react_native_1.Text>
|
|
378
|
-
</react_native_1.TouchableOpacity>
|
|
379
|
-
</react_native_1.View>
|
|
337
|
+
{/* 1. Hot Reload — the common path */}
|
|
338
|
+
<ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
|
|
380
339
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{isReloading ? 'Reloading...' : 'Hot Reload'}
|
|
386
|
-
</react_native_1.Text>
|
|
387
|
-
</react_native_1.TouchableOpacity>
|
|
340
|
+
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
341
|
+
<ActionRow label={action === 'capturing'
|
|
342
|
+
? 'Capturing…'
|
|
343
|
+
: 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
|
|
388
344
|
|
|
389
|
-
|
|
390
|
-
<
|
|
391
|
-
<react_native_1.Text style={styles.streamingText}>
|
|
392
|
-
{BlackBox_1.BlackBox.isStreaming ? 'Streaming' : 'Not streaming'}
|
|
393
|
-
</react_native_1.Text>
|
|
394
|
-
</react_native_1.View>
|
|
395
|
-
</react_native_1.View>
|
|
345
|
+
{/* 3. Vibing */}
|
|
346
|
+
<ActionRow label={action === 'vibing' ? 'Starting…' : 'Vibing'} tint="#818cf8" onPress={handleVibing} disabled={busy} busy={action === 'vibing'}/>
|
|
396
347
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
<react_native_1.Text style={styles.voiceCommandText}>
|
|
400
|
-
{isVoiceCommand ? 'Stop & Send Command' : 'Speak to Fix'}
|
|
401
|
-
</react_native_1.Text>
|
|
402
|
-
</react_native_1.TouchableOpacity>)}
|
|
348
|
+
{/* 4. Start/Stop Recording */}
|
|
349
|
+
<ActionRow label={isRecordingVideo ? 'Stop Recording' : 'Start Recording'} tint={isRecordingVideo ? '#ef4444' : '#60a5fa'} onPress={handleToggleRecording} disabled={busy && action !== 'idle' && !isRecordingVideo}/>
|
|
403
350
|
|
|
404
|
-
|
|
405
|
-
|
|
351
|
+
{/* 5. Send Video (only tappable when a clip is ready) */}
|
|
352
|
+
<ActionRow label={action === 'sending-video'
|
|
353
|
+
? 'Sending…'
|
|
354
|
+
: lastVideo
|
|
355
|
+
? `Send Video · ${Math.round(lastVideo.duration)}s`
|
|
356
|
+
: 'Send Video'} tint="#a78bfa" onPress={handleSendVideo} disabled={busy || !lastVideo} busy={action === 'sending-video'}/>
|
|
406
357
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
{sent ? (<react_native_1.View style={styles.sendButton}>
|
|
414
|
-
<react_native_1.Text style={styles.sendText}>Sent!</react_native_1.Text>
|
|
415
|
-
</react_native_1.View>) : (<react_native_1.TouchableOpacity style={[styles.sendButton, isSending && styles.sendButtonDisabled]} onPress={handleSend} disabled={isSending || timeline.length === 0}>
|
|
416
|
-
{isSending ? (<react_native_1.ActivityIndicator color="#fff" size="small"/>) : (<react_native_1.Text style={styles.sendText}>Send Report</react_native_1.Text>)}
|
|
417
|
-
</react_native_1.TouchableOpacity>)}
|
|
418
|
-
</react_native_1.View>
|
|
419
|
-
</react_native_1.View>
|
|
420
|
-
</react_native_1.View>
|
|
421
|
-
</react_native_1.Modal>)}
|
|
358
|
+
{toast && <react_native_1.Text style={styles.toast}>{toast}</react_native_1.Text>}
|
|
359
|
+
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
360
|
+
</react_native_1.Pressable>
|
|
361
|
+
</react_native_1.Pressable>
|
|
362
|
+
</react_native_1.Modal>)}
|
|
422
363
|
</>);
|
|
423
364
|
};
|
|
424
365
|
exports.FeedbackModal = FeedbackModal;
|
|
366
|
+
const ActionRow = ({ label, tint, onPress, disabled, busy, }) => (<react_native_1.Pressable onPress={onPress} disabled={disabled} style={({ pressed }) => [
|
|
367
|
+
styles.actionBtn,
|
|
368
|
+
{
|
|
369
|
+
borderColor: tint + '66',
|
|
370
|
+
backgroundColor: tint + '1f',
|
|
371
|
+
},
|
|
372
|
+
disabled && styles.actionBtnDisabled,
|
|
373
|
+
pressed && !disabled && { opacity: 0.7 },
|
|
374
|
+
]} accessibilityRole="button" accessibilityLabel={label}>
|
|
375
|
+
{busy ? (<react_native_1.ActivityIndicator color={tint} size="small"/>) : (<react_native_1.Text style={[styles.actionText, { color: tint }]}>{label}</react_native_1.Text>)}
|
|
376
|
+
</react_native_1.Pressable>);
|
|
425
377
|
const styles = react_native_1.StyleSheet.create({
|
|
426
378
|
overlay: {
|
|
427
379
|
flex: 1,
|
|
428
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
380
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
429
381
|
justifyContent: 'flex-end',
|
|
430
382
|
},
|
|
431
383
|
modal: {
|
|
432
|
-
backgroundColor: '#
|
|
433
|
-
borderTopLeftRadius:
|
|
434
|
-
borderTopRightRadius:
|
|
435
|
-
padding:
|
|
436
|
-
paddingBottom:
|
|
437
|
-
|
|
384
|
+
backgroundColor: '#141422',
|
|
385
|
+
borderTopLeftRadius: 22,
|
|
386
|
+
borderTopRightRadius: 22,
|
|
387
|
+
padding: 22,
|
|
388
|
+
paddingBottom: 36,
|
|
389
|
+
gap: 12,
|
|
390
|
+
},
|
|
391
|
+
header: {
|
|
392
|
+
flexDirection: 'row',
|
|
393
|
+
alignItems: 'center',
|
|
394
|
+
justifyContent: 'space-between',
|
|
395
|
+
marginBottom: 6,
|
|
438
396
|
},
|
|
439
397
|
title: {
|
|
440
398
|
fontSize: 20,
|
|
441
399
|
fontWeight: '700',
|
|
442
400
|
color: '#fff',
|
|
443
|
-
marginBottom: 12,
|
|
444
401
|
},
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
},
|
|
450
|
-
modeButton: {
|
|
451
|
-
flex: 1,
|
|
452
|
-
paddingVertical: 8,
|
|
453
|
-
borderRadius: 8,
|
|
402
|
+
closeBtn: {
|
|
403
|
+
width: 36,
|
|
404
|
+
height: 36,
|
|
405
|
+
borderRadius: 18,
|
|
454
406
|
alignItems: 'center',
|
|
407
|
+
justifyContent: 'center',
|
|
455
408
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
456
|
-
borderWidth: 1,
|
|
457
|
-
borderColor: 'rgba(255,255,255,0.1)',
|
|
458
|
-
},
|
|
459
|
-
modeButtonActive: {
|
|
460
|
-
backgroundColor: 'rgba(99,102,241,0.3)',
|
|
461
|
-
borderColor: '#6366f1',
|
|
462
|
-
},
|
|
463
|
-
modeButtonText: {
|
|
464
|
-
color: '#999',
|
|
465
|
-
fontSize: 13,
|
|
466
|
-
fontWeight: '600',
|
|
467
|
-
},
|
|
468
|
-
modeButtonTextActive: {
|
|
469
|
-
color: '#c7c8ff',
|
|
470
|
-
},
|
|
471
|
-
commentaryList: {
|
|
472
|
-
maxHeight: 140,
|
|
473
|
-
marginBottom: 12,
|
|
474
|
-
},
|
|
475
|
-
commentaryBubble: {
|
|
476
|
-
backgroundColor: 'rgba(99,102,241,0.15)',
|
|
477
|
-
borderRadius: 10,
|
|
478
|
-
padding: 10,
|
|
479
|
-
marginBottom: 6,
|
|
480
|
-
borderLeftWidth: 3,
|
|
481
|
-
borderLeftColor: '#6366f1',
|
|
482
|
-
},
|
|
483
|
-
commentaryType: {
|
|
484
|
-
color: '#8b8bf5',
|
|
485
|
-
fontSize: 10,
|
|
486
|
-
fontWeight: '700',
|
|
487
|
-
textTransform: 'uppercase',
|
|
488
|
-
marginBottom: 2,
|
|
489
|
-
},
|
|
490
|
-
commentaryMessage: {
|
|
491
|
-
color: '#d0d0e0',
|
|
492
|
-
fontSize: 13,
|
|
493
|
-
lineHeight: 18,
|
|
494
409
|
},
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
410
|
+
closeIcon: {
|
|
411
|
+
color: '#fff',
|
|
412
|
+
fontSize: 22,
|
|
413
|
+
lineHeight: 24,
|
|
414
|
+
fontWeight: '400',
|
|
498
415
|
},
|
|
499
|
-
|
|
416
|
+
actionBtn: {
|
|
417
|
+
paddingVertical: 16,
|
|
418
|
+
borderRadius: 14,
|
|
500
419
|
alignItems: 'center',
|
|
501
|
-
|
|
502
|
-
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
503
|
-
borderRadius: 12,
|
|
504
|
-
padding: 10,
|
|
505
|
-
minWidth: 70,
|
|
506
|
-
},
|
|
507
|
-
timelineIcon: {
|
|
508
|
-
fontSize: 14,
|
|
509
|
-
color: '#ccc',
|
|
510
|
-
fontWeight: '600',
|
|
511
|
-
},
|
|
512
|
-
timelineLabel: {
|
|
513
|
-
color: '#ccc',
|
|
514
|
-
fontSize: 11,
|
|
515
|
-
marginTop: 4,
|
|
516
|
-
},
|
|
517
|
-
timelineDuration: {
|
|
518
|
-
color: '#999',
|
|
519
|
-
fontSize: 10,
|
|
520
|
-
},
|
|
521
|
-
actions: {
|
|
522
|
-
flexDirection: 'row',
|
|
523
|
-
gap: 12,
|
|
524
|
-
marginBottom: 12,
|
|
525
|
-
},
|
|
526
|
-
actionButton: {
|
|
527
|
-
flex: 1,
|
|
528
|
-
backgroundColor: 'rgba(99,102,241,0.2)',
|
|
420
|
+
justifyContent: 'center',
|
|
529
421
|
borderWidth: 1,
|
|
530
|
-
borderColor: 'rgba(99,102,241,0.4)',
|
|
531
|
-
borderRadius: 12,
|
|
532
|
-
paddingVertical: 14,
|
|
533
|
-
alignItems: 'center',
|
|
534
422
|
},
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
borderColor: 'rgba(239,68,68,0.6)',
|
|
423
|
+
actionBtnDisabled: {
|
|
424
|
+
opacity: 0.35,
|
|
538
425
|
},
|
|
539
426
|
actionText: {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
fontWeight: '600',
|
|
543
|
-
},
|
|
544
|
-
voiceCommandButton: {
|
|
545
|
-
backgroundColor: 'rgba(34,197,94,0.2)',
|
|
546
|
-
borderWidth: 1,
|
|
547
|
-
borderColor: 'rgba(34,197,94,0.4)',
|
|
548
|
-
borderRadius: 12,
|
|
549
|
-
paddingVertical: 14,
|
|
550
|
-
alignItems: 'center',
|
|
551
|
-
marginBottom: 12,
|
|
552
|
-
},
|
|
553
|
-
voiceCommandActive: {
|
|
554
|
-
backgroundColor: 'rgba(34,197,94,0.4)',
|
|
555
|
-
borderColor: '#22c55e',
|
|
427
|
+
fontSize: 15,
|
|
428
|
+
fontWeight: '700',
|
|
556
429
|
},
|
|
557
|
-
|
|
430
|
+
toast: {
|
|
558
431
|
color: '#22c55e',
|
|
559
|
-
fontSize:
|
|
560
|
-
|
|
432
|
+
fontSize: 13,
|
|
433
|
+
textAlign: 'center',
|
|
434
|
+
marginTop: 4,
|
|
561
435
|
},
|
|
562
436
|
error: {
|
|
563
437
|
color: '#ef4444',
|
|
564
438
|
fontSize: 12,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
footer: {
|
|
568
|
-
flexDirection: 'row',
|
|
569
|
-
gap: 12,
|
|
570
|
-
},
|
|
571
|
-
cancelButton: {
|
|
572
|
-
flex: 1,
|
|
573
|
-
paddingVertical: 14,
|
|
574
|
-
alignItems: 'center',
|
|
575
|
-
borderRadius: 12,
|
|
576
|
-
borderWidth: 1,
|
|
577
|
-
borderColor: 'rgba(255,255,255,0.2)',
|
|
578
|
-
},
|
|
579
|
-
cancelText: {
|
|
580
|
-
color: '#999',
|
|
581
|
-
fontSize: 16,
|
|
582
|
-
fontWeight: '600',
|
|
583
|
-
},
|
|
584
|
-
sendButton: {
|
|
585
|
-
flex: 2,
|
|
586
|
-
backgroundColor: '#6366f1',
|
|
587
|
-
paddingVertical: 14,
|
|
588
|
-
alignItems: 'center',
|
|
589
|
-
borderRadius: 12,
|
|
590
|
-
},
|
|
591
|
-
sendButtonDisabled: {
|
|
592
|
-
opacity: 0.5,
|
|
593
|
-
},
|
|
594
|
-
sendText: {
|
|
595
|
-
color: '#fff',
|
|
596
|
-
fontSize: 16,
|
|
597
|
-
fontWeight: '700',
|
|
598
|
-
},
|
|
599
|
-
reloadButton: {
|
|
600
|
-
backgroundColor: 'rgba(251,191,36,0.2)',
|
|
601
|
-
borderColor: 'rgba(251,191,36,0.4)',
|
|
602
|
-
},
|
|
603
|
-
streamingIndicator: {
|
|
604
|
-
flexDirection: 'row',
|
|
605
|
-
justifyContent: 'center',
|
|
606
|
-
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
607
|
-
borderColor: 'rgba(255,255,255,0.1)',
|
|
608
|
-
},
|
|
609
|
-
streamingDot: {
|
|
610
|
-
width: 8,
|
|
611
|
-
height: 8,
|
|
612
|
-
borderRadius: 4,
|
|
613
|
-
backgroundColor: '#555',
|
|
614
|
-
marginRight: 6,
|
|
615
|
-
},
|
|
616
|
-
streamingDotActive: {
|
|
617
|
-
backgroundColor: '#22c55e',
|
|
618
|
-
},
|
|
619
|
-
streamingText: {
|
|
620
|
-
color: '#999',
|
|
621
|
-
fontSize: 12,
|
|
439
|
+
textAlign: 'center',
|
|
440
|
+
marginTop: 4,
|
|
622
441
|
},
|
|
623
442
|
});
|