yaver-feedback-react-native 0.5.3 → 0.5.5
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 +30 -0
- package/dist/AuthOverlay.d.ts +16 -0
- package/dist/AuthOverlay.js +104 -0
- package/dist/BlackBox.d.ts +154 -0
- package/dist/BlackBox.js +395 -0
- package/dist/ConnectionScreen.d.ts +13 -0
- package/dist/ConnectionScreen.js +373 -0
- package/dist/Discovery.d.ts +59 -0
- package/dist/Discovery.js +293 -0
- package/dist/FeedbackModal.d.ts +11 -0
- package/dist/FeedbackModal.js +623 -0
- package/dist/FixReport.d.ts +23 -0
- package/dist/FixReport.js +282 -0
- package/dist/FloatingButton.d.ts +71 -0
- package/dist/FloatingButton.js +778 -0
- package/dist/LoginScreen.d.ts +14 -0
- package/dist/LoginScreen.js +317 -0
- package/dist/MachinePickerScreen.d.ts +19 -0
- package/dist/MachinePickerScreen.js +175 -0
- package/dist/P2PClient.d.ts +136 -0
- package/dist/P2PClient.js +357 -0
- package/dist/ShakeDetector.d.ts +39 -0
- package/dist/ShakeDetector.js +133 -0
- package/dist/YaverFeedback.d.ts +198 -0
- package/dist/YaverFeedback.js +707 -0
- package/dist/YaverUpdates.d.ts +78 -0
- package/dist/YaverUpdates.js +272 -0
- package/dist/__tests__/Discovery.test.d.ts +1 -0
- package/dist/__tests__/Discovery.test.js +164 -0
- package/dist/__tests__/P2PClient.test.d.ts +1 -0
- package/dist/__tests__/P2PClient.test.js +169 -0
- package/dist/__tests__/SDKToken.test.d.ts +1 -0
- package/dist/__tests__/SDKToken.test.js +215 -0
- package/dist/__tests__/YaverFeedback.test.d.ts +1 -0
- package/dist/__tests__/YaverFeedback.test.js +161 -0
- package/dist/__tests__/types.test.d.ts +1 -0
- package/dist/__tests__/types.test.js +219 -0
- package/dist/auth.d.ts +105 -0
- package/dist/auth.js +282 -0
- package/dist/capture.d.ts +27 -0
- package/dist/capture.js +74 -0
- package/dist/expo.d.ts +15 -0
- package/dist/expo.js +62 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +80 -0
- package/dist/types.d.ts +282 -0
- package/dist/types.js +2 -0
- package/dist/upload.d.ts +13 -0
- package/dist/upload.js +59 -0
- package/package.json +6 -3
- package/src/ShakeDetector.ts +22 -1
- package/src/YaverFeedback.ts +29 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FeedbackModal = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const YaverFeedback_1 = require("./YaverFeedback");
|
|
40
|
+
const BlackBox_1 = require("./BlackBox");
|
|
41
|
+
const capture_1 = require("./capture");
|
|
42
|
+
const upload_1 = require("./upload");
|
|
43
|
+
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
|
+
const FeedbackModal = () => {
|
|
59
|
+
const [visible, setVisible] = (0, react_1.useState)(false);
|
|
60
|
+
const [timeline, setTimeline] = (0, react_1.useState)([]);
|
|
61
|
+
const [isRecordingAudio, setIsRecordingAudio] = (0, react_1.useState)(false);
|
|
62
|
+
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
63
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
64
|
+
const [sent, setSent] = (0, react_1.useState)(false);
|
|
65
|
+
const [mode, setMode] = (0, react_1.useState)('batch');
|
|
66
|
+
const [commentary, setCommentary] = (0, react_1.useState)([]);
|
|
67
|
+
const [isVoiceCommand, setIsVoiceCommand] = (0, react_1.useState)(false);
|
|
68
|
+
const [isReloading, setIsReloading] = (0, react_1.useState)(false);
|
|
69
|
+
const mountedRef = (0, react_1.useRef)(true);
|
|
70
|
+
const commentaryListRef = (0, react_1.useRef)(null);
|
|
71
|
+
(0, react_1.useEffect)(() => {
|
|
72
|
+
mountedRef.current = true;
|
|
73
|
+
const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
74
|
+
if (YaverFeedback_1.YaverFeedback.isEnabled()) {
|
|
75
|
+
setVisible(true);
|
|
76
|
+
setTimeline([]);
|
|
77
|
+
setError(null);
|
|
78
|
+
setSent(false);
|
|
79
|
+
setCommentary([]);
|
|
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]);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
mountedRef.current = false;
|
|
91
|
+
sub.remove();
|
|
92
|
+
commentarySub.remove();
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
const handleScreenshot = (0, react_1.useCallback)(async () => {
|
|
96
|
+
try {
|
|
97
|
+
const path = await (0, capture_1.captureScreenshot)();
|
|
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
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (mountedRef.current) {
|
|
127
|
+
setError(String(err));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, [mode]);
|
|
131
|
+
const handleToggleAudio = (0, react_1.useCallback)(async () => {
|
|
132
|
+
if (isRecordingAudio) {
|
|
133
|
+
try {
|
|
134
|
+
const result = await (0, capture_1.stopAudioRecording)();
|
|
135
|
+
if (mountedRef.current) {
|
|
136
|
+
setIsRecordingAudio(false);
|
|
137
|
+
const event = {
|
|
138
|
+
type: 'audio',
|
|
139
|
+
path: result.path,
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
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
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
try {
|
|
173
|
+
await (0, capture_1.startAudioRecording)();
|
|
174
|
+
if (mountedRef.current) {
|
|
175
|
+
setIsRecordingAudio(true);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (mountedRef.current) {
|
|
180
|
+
setError(String(err));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, [isRecordingAudio, mode]);
|
|
185
|
+
const handleVoiceCommand = (0, react_1.useCallback)(async () => {
|
|
186
|
+
if (isVoiceCommand) {
|
|
187
|
+
// Stop voice command and send as a task
|
|
188
|
+
try {
|
|
189
|
+
const result = await (0, capture_1.stopAudioRecording)();
|
|
190
|
+
if (mountedRef.current) {
|
|
191
|
+
setIsVoiceCommand(false);
|
|
192
|
+
const client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
193
|
+
if (client) {
|
|
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
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
if (mountedRef.current) {
|
|
214
|
+
setIsVoiceCommand(false);
|
|
215
|
+
setError(String(err));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
try {
|
|
221
|
+
await (0, capture_1.startAudioRecording)();
|
|
222
|
+
if (mountedRef.current) {
|
|
223
|
+
setIsVoiceCommand(true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (mountedRef.current) {
|
|
228
|
+
setError(String(err));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, [isVoiceCommand]);
|
|
233
|
+
const handleReload = (0, react_1.useCallback)(async () => {
|
|
234
|
+
const config = YaverFeedback_1.YaverFeedback.getConfig();
|
|
235
|
+
if (!config?.agentUrl)
|
|
236
|
+
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
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
if (mountedRef.current) {
|
|
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)
|
|
265
|
+
return;
|
|
266
|
+
setIsSending(true);
|
|
267
|
+
setError(null);
|
|
268
|
+
try {
|
|
269
|
+
const { Dimensions } = require('react-native');
|
|
270
|
+
const { width, height } = Dimensions.get('window');
|
|
271
|
+
const deviceInfo = {
|
|
272
|
+
platform: react_native_1.Platform.OS,
|
|
273
|
+
osVersion: String(react_native_1.Platform.Version),
|
|
274
|
+
model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
275
|
+
screenWidth: width,
|
|
276
|
+
screenHeight: height,
|
|
277
|
+
};
|
|
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
|
+
const bundle = {
|
|
285
|
+
metadata: {
|
|
286
|
+
timestamp: new Date().toISOString(),
|
|
287
|
+
device: deviceInfo,
|
|
288
|
+
app: {},
|
|
289
|
+
},
|
|
290
|
+
screenshots,
|
|
291
|
+
audio: audioEvent?.path,
|
|
292
|
+
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
293
|
+
};
|
|
294
|
+
await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken, bundle);
|
|
295
|
+
if (mountedRef.current) {
|
|
296
|
+
setSent(true);
|
|
297
|
+
// Auto-close after a short delay
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (mountedRef.current) {
|
|
300
|
+
setVisible(false);
|
|
301
|
+
}
|
|
302
|
+
}, 1500);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
if (mountedRef.current) {
|
|
307
|
+
setError(String(err));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
if (mountedRef.current) {
|
|
312
|
+
setIsSending(false);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}, [timeline]);
|
|
316
|
+
const handleCancel = (0, react_1.useCallback)(() => {
|
|
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.
|
|
332
|
+
return (<>
|
|
333
|
+
<AuthOverlay_1.AuthOverlay />
|
|
334
|
+
{visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleCancel}>
|
|
335
|
+
<react_native_1.View style={styles.overlay}>
|
|
336
|
+
<react_native_1.View style={styles.modal}>
|
|
337
|
+
<react_native_1.Text style={styles.title}>Send Feedback</react_native_1.Text>
|
|
338
|
+
|
|
339
|
+
{/* Mode selector */}
|
|
340
|
+
<react_native_1.View style={styles.modeSelector}>
|
|
341
|
+
{['live', 'narrated', 'batch'].map((m) => (<react_native_1.TouchableOpacity key={m} style={[styles.modeButton, mode === m && styles.modeButtonActive]} onPress={() => setMode(m)}>
|
|
342
|
+
<react_native_1.Text style={[styles.modeButtonText, mode === m && styles.modeButtonTextActive]}>
|
|
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>
|
|
373
|
+
|
|
374
|
+
<react_native_1.TouchableOpacity style={[styles.actionButton, isRecordingAudio && styles.actionButtonActive]} onPress={handleToggleAudio} disabled={isVoiceCommand}>
|
|
375
|
+
<react_native_1.Text style={styles.actionText}>
|
|
376
|
+
{isRecordingAudio ? 'Stop Recording' : 'Voice Note'}
|
|
377
|
+
</react_native_1.Text>
|
|
378
|
+
</react_native_1.TouchableOpacity>
|
|
379
|
+
</react_native_1.View>
|
|
380
|
+
|
|
381
|
+
{/* Hot Reload + Streaming status */}
|
|
382
|
+
<react_native_1.View style={styles.actions}>
|
|
383
|
+
<react_native_1.TouchableOpacity style={[styles.actionButton, styles.reloadButton]} onPress={handleReload} disabled={isReloading}>
|
|
384
|
+
<react_native_1.Text style={styles.actionText}>
|
|
385
|
+
{isReloading ? 'Reloading...' : 'Hot Reload'}
|
|
386
|
+
</react_native_1.Text>
|
|
387
|
+
</react_native_1.TouchableOpacity>
|
|
388
|
+
|
|
389
|
+
<react_native_1.View style={[styles.actionButton, styles.streamingIndicator]}>
|
|
390
|
+
<react_native_1.View style={[styles.streamingDot, BlackBox_1.BlackBox.isStreaming && styles.streamingDotActive]}/>
|
|
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>
|
|
396
|
+
|
|
397
|
+
{/* Voice command button */}
|
|
398
|
+
{mode === 'live' && (<react_native_1.TouchableOpacity style={[styles.voiceCommandButton, isVoiceCommand && styles.voiceCommandActive]} onPress={handleVoiceCommand} disabled={isRecordingAudio}>
|
|
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>)}
|
|
403
|
+
|
|
404
|
+
{/* Error display */}
|
|
405
|
+
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
406
|
+
|
|
407
|
+
{/* Send / Cancel */}
|
|
408
|
+
<react_native_1.View style={styles.footer}>
|
|
409
|
+
<react_native_1.TouchableOpacity style={styles.cancelButton} onPress={handleCancel}>
|
|
410
|
+
<react_native_1.Text style={styles.cancelText}>Cancel</react_native_1.Text>
|
|
411
|
+
</react_native_1.TouchableOpacity>
|
|
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>)}
|
|
422
|
+
</>);
|
|
423
|
+
};
|
|
424
|
+
exports.FeedbackModal = FeedbackModal;
|
|
425
|
+
const styles = react_native_1.StyleSheet.create({
|
|
426
|
+
overlay: {
|
|
427
|
+
flex: 1,
|
|
428
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
429
|
+
justifyContent: 'flex-end',
|
|
430
|
+
},
|
|
431
|
+
modal: {
|
|
432
|
+
backgroundColor: '#1a1a2e',
|
|
433
|
+
borderTopLeftRadius: 20,
|
|
434
|
+
borderTopRightRadius: 20,
|
|
435
|
+
padding: 24,
|
|
436
|
+
paddingBottom: 40,
|
|
437
|
+
maxHeight: '90%',
|
|
438
|
+
},
|
|
439
|
+
title: {
|
|
440
|
+
fontSize: 20,
|
|
441
|
+
fontWeight: '700',
|
|
442
|
+
color: '#fff',
|
|
443
|
+
marginBottom: 12,
|
|
444
|
+
},
|
|
445
|
+
modeSelector: {
|
|
446
|
+
flexDirection: 'row',
|
|
447
|
+
gap: 8,
|
|
448
|
+
marginBottom: 16,
|
|
449
|
+
},
|
|
450
|
+
modeButton: {
|
|
451
|
+
flex: 1,
|
|
452
|
+
paddingVertical: 8,
|
|
453
|
+
borderRadius: 8,
|
|
454
|
+
alignItems: 'center',
|
|
455
|
+
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
|
+
},
|
|
495
|
+
timeline: {
|
|
496
|
+
maxHeight: 80,
|
|
497
|
+
marginBottom: 16,
|
|
498
|
+
},
|
|
499
|
+
timelineItem: {
|
|
500
|
+
alignItems: 'center',
|
|
501
|
+
marginRight: 16,
|
|
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)',
|
|
529
|
+
borderWidth: 1,
|
|
530
|
+
borderColor: 'rgba(99,102,241,0.4)',
|
|
531
|
+
borderRadius: 12,
|
|
532
|
+
paddingVertical: 14,
|
|
533
|
+
alignItems: 'center',
|
|
534
|
+
},
|
|
535
|
+
actionButtonActive: {
|
|
536
|
+
backgroundColor: 'rgba(239,68,68,0.3)',
|
|
537
|
+
borderColor: 'rgba(239,68,68,0.6)',
|
|
538
|
+
},
|
|
539
|
+
actionText: {
|
|
540
|
+
color: '#fff',
|
|
541
|
+
fontSize: 14,
|
|
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',
|
|
556
|
+
},
|
|
557
|
+
voiceCommandText: {
|
|
558
|
+
color: '#22c55e',
|
|
559
|
+
fontSize: 14,
|
|
560
|
+
fontWeight: '600',
|
|
561
|
+
},
|
|
562
|
+
error: {
|
|
563
|
+
color: '#ef4444',
|
|
564
|
+
fontSize: 12,
|
|
565
|
+
marginBottom: 12,
|
|
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,
|
|
622
|
+
},
|
|
623
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TestSession } from './types';
|
|
3
|
+
export interface FixReportProps {
|
|
4
|
+
/** Test session data with fixes list */
|
|
5
|
+
session: TestSession | null;
|
|
6
|
+
/** Whether the modal is visible */
|
|
7
|
+
visible: boolean;
|
|
8
|
+
/** Called when the user closes the report */
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
/** Accent color (matches FloatingButton) */
|
|
11
|
+
color?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fix Report viewer — shows a markdown-style list of all fixes
|
|
15
|
+
* applied by the AI agent during a test session.
|
|
16
|
+
*
|
|
17
|
+
* Each fix shows the file, description, and error that triggered it.
|
|
18
|
+
* Tap a fix to expand and see the diff/code snippet.
|
|
19
|
+
*
|
|
20
|
+
* Fixes are NOT committed — they're staged changes the developer
|
|
21
|
+
* can review, accept, or revert.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FixReport: React.FC<FixReportProps>;
|