yaver-feedback-react-native 0.8.0 → 0.8.2
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/AuthOverlay.js +1 -1
- package/dist/FeedbackModal.js +475 -265
- package/dist/P2PClient.d.ts +9 -3
- package/dist/P2PClient.js +30 -2
- package/dist/QuickActionIcon.d.ts +10 -2
- package/dist/QuickActionIcon.js +49 -12
- package/dist/YaverFeedback.d.ts +5 -0
- package/dist/YaverFeedback.js +24 -0
- package/dist/__tests__/P2PClient.test.js +30 -0
- package/dist/__tests__/YaverFeedback.test.js +39 -0
- package/dist/capture.d.ts +11 -0
- package/dist/capture.js +59 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.js +9 -7
- package/dist/preferences.d.ts +11 -0
- package/dist/preferences.js +82 -0
- package/dist/types.d.ts +29 -4
- package/dist/upload.d.ts +1 -0
- package/dist/upload.js +8 -0
- package/package.json +11 -5
- package/src/AuthOverlay.tsx +1 -0
- package/src/FeedbackModal.tsx +561 -319
- package/src/P2PClient.ts +44 -3
- package/src/QuickActionIcon.tsx +76 -13
- package/src/YaverFeedback.ts +31 -0
- package/src/__tests__/P2PClient.test.ts +40 -0
- package/src/__tests__/YaverFeedback.test.ts +42 -0
- package/src/capture.ts +74 -0
- package/src/index.ts +8 -6
- package/src/preferences.ts +96 -0
- package/src/types.ts +29 -4
- package/src/upload.ts +9 -0
package/src/FeedbackModal.tsx
CHANGED
|
@@ -2,9 +2,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
4
|
DeviceEventEmitter,
|
|
5
|
+
Keyboard,
|
|
6
|
+
KeyboardAvoidingView,
|
|
5
7
|
Modal,
|
|
6
8
|
Platform,
|
|
7
9
|
Pressable,
|
|
10
|
+
ScrollView,
|
|
8
11
|
StyleSheet,
|
|
9
12
|
Text,
|
|
10
13
|
TextInput,
|
|
@@ -13,47 +16,50 @@ import {
|
|
|
13
16
|
import { YaverFeedback } from './YaverFeedback';
|
|
14
17
|
import {
|
|
15
18
|
captureScreenshot,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Launch scope for the feedback test SDK is intentionally smaller for now.
|
|
20
|
+
// Keep the dormant file-upload and screen-recording helpers nearby, but
|
|
21
|
+
// comment them out until we bring them back with stronger test coverage.
|
|
22
|
+
// pickFeedbackFile,
|
|
23
|
+
// startVideoRecording,
|
|
24
|
+
// stopVideoRecording,
|
|
21
25
|
} from './capture';
|
|
22
26
|
import { uploadFeedback } from './upload';
|
|
23
27
|
import { DeviceInfo, FeedbackBundle } from './types';
|
|
24
28
|
import { AuthOverlay } from './AuthOverlay';
|
|
25
29
|
import { QuickActionIcon } from './QuickActionIcon';
|
|
30
|
+
import { listReachableDevices, RemoteDevice } from './auth';
|
|
31
|
+
import {
|
|
32
|
+
QUICK_ICON_COLOR_PRESETS,
|
|
33
|
+
QuickIconColorPreset,
|
|
34
|
+
} from './preferences';
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
|
-
* Simplified feedback modal —
|
|
37
|
+
* Simplified feedback modal — launch scope is 3 actions:
|
|
29
38
|
*
|
|
30
39
|
* 1. Hot Reload — instant JS reload (most common use case)
|
|
31
|
-
* 2.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 4. Start / Stop Recording — screen-recording toggle
|
|
36
|
-
* 5. Send Video — submit the last recorded video
|
|
40
|
+
* 2. Vibing — open a vibing session on the agent
|
|
41
|
+
* 3. Screenshot & Fix — capture the underlying app (modal hidden
|
|
42
|
+
* during capture), upload it, and trigger
|
|
43
|
+
* the fix loop
|
|
37
44
|
*
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
40
|
-
* were removed in 0.7.0 — those flows never worked end-to-end against
|
|
41
|
-
* the Go agent (see MISSINGS_FEEDBACK_SDK.md).
|
|
45
|
+
* The footer also has an explicit Cancel button so the icon tap path
|
|
46
|
+
* feels like a standard action sheet rather than a hidden modal.
|
|
42
47
|
*/
|
|
43
48
|
|
|
44
|
-
interface LastVideo {
|
|
45
|
-
path: string;
|
|
46
|
-
duration: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
49
|
type ActionState =
|
|
50
50
|
| 'idle'
|
|
51
51
|
| 'hot-reloading'
|
|
52
52
|
| 'capturing'
|
|
53
|
-
| 'vibing'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
53
|
+
| 'vibing';
|
|
54
|
+
|
|
55
|
+
type MachineCardState = {
|
|
56
|
+
device: RemoteDevice | null;
|
|
57
|
+
reachable: boolean | null;
|
|
58
|
+
loading: boolean;
|
|
59
|
+
status: 'none' | 'live' | 'attention' | 'offline';
|
|
60
|
+
title: string;
|
|
61
|
+
detail: string;
|
|
62
|
+
};
|
|
57
63
|
|
|
58
64
|
export const FeedbackModal: React.FC = () => {
|
|
59
65
|
const [visible, setVisible] = useState(false);
|
|
@@ -61,12 +67,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
61
67
|
const [error, setError] = useState<string | null>(null);
|
|
62
68
|
const [toast, setToast] = useState<string | null>(null);
|
|
63
69
|
const [progress, setProgress] = useState<number | null>(null);
|
|
64
|
-
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
|
|
65
|
-
const [isRecordingVoice, setIsRecordingVoice] = useState(false);
|
|
66
|
-
// Cached once on mount — bare-RN apps without expo-av get a clean
|
|
67
|
-
// hidden button instead of a runtime error.
|
|
68
|
-
const voiceSupported = useRef<boolean>(isVoiceCaptureSupported()).current;
|
|
69
|
-
const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
|
|
70
70
|
// Tracks whether the user has hidden the QuickActionIcon via its
|
|
71
71
|
// long-press menu. Shake is always available, so the feedback modal
|
|
72
72
|
// is our guaranteed UI for bringing the icon back — we surface a
|
|
@@ -80,8 +80,119 @@ export const FeedbackModal: React.FC = () => {
|
|
|
80
80
|
const [showVibeInput, setShowVibeInput] = useState(false);
|
|
81
81
|
const [vibePrompt, setVibePrompt] = useState('');
|
|
82
82
|
const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
|
|
83
|
+
const [quickIconColorPreset, setQuickIconColorPreset] =
|
|
84
|
+
useState<QuickIconColorPreset | null>(null);
|
|
85
|
+
const [machineCard, setMachineCard] = useState<MachineCardState>({
|
|
86
|
+
device: null,
|
|
87
|
+
reachable: null,
|
|
88
|
+
loading: false,
|
|
89
|
+
status: 'none',
|
|
90
|
+
title: 'No machine selected',
|
|
91
|
+
detail: 'Pick a remote dev machine before using the feedback actions.',
|
|
92
|
+
});
|
|
83
93
|
const mountedRef = useRef(true);
|
|
84
94
|
|
|
95
|
+
const loadSelectedMachine = useCallback(async () => {
|
|
96
|
+
const cfg = YaverFeedback.getConfig();
|
|
97
|
+
if (!cfg?.authToken) {
|
|
98
|
+
if (mountedRef.current) {
|
|
99
|
+
setMachineCard({
|
|
100
|
+
device: null,
|
|
101
|
+
reachable: null,
|
|
102
|
+
loading: false,
|
|
103
|
+
status: 'none',
|
|
104
|
+
title: 'Not signed in',
|
|
105
|
+
detail: 'Sign in to pick and monitor a remote dev machine.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!cfg.preferredDeviceId) {
|
|
111
|
+
if (mountedRef.current) {
|
|
112
|
+
setMachineCard({
|
|
113
|
+
device: null,
|
|
114
|
+
reachable: null,
|
|
115
|
+
loading: false,
|
|
116
|
+
status: 'none',
|
|
117
|
+
title: 'No machine selected',
|
|
118
|
+
detail: 'Choose which machine this SDK should talk to.',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (mountedRef.current) {
|
|
125
|
+
setMachineCard((prev) => ({ ...prev, loading: true }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const devices = await listReachableDevices(cfg.authToken);
|
|
130
|
+
const all = [...devices.owned, ...devices.shared];
|
|
131
|
+
const device =
|
|
132
|
+
all.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
|
|
133
|
+
|
|
134
|
+
if (!device) {
|
|
135
|
+
if (mountedRef.current) {
|
|
136
|
+
setMachineCard({
|
|
137
|
+
device: null,
|
|
138
|
+
reachable: null,
|
|
139
|
+
loading: false,
|
|
140
|
+
status: 'offline',
|
|
141
|
+
title: 'Selected machine missing',
|
|
142
|
+
detail: 'The saved machine was not returned by the device list. Re-select it.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let reachable: boolean | null = null;
|
|
149
|
+
const client = YaverFeedback.getP2PClient();
|
|
150
|
+
if (device.isOnline && !device.needsAuth && client) {
|
|
151
|
+
reachable = await client.health();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hostHint = device.hostEmail ? ` via ${device.hostEmail}` : '';
|
|
155
|
+
let status: MachineCardState['status'] = 'live';
|
|
156
|
+
let detail = `${device.platform}${hostHint}`;
|
|
157
|
+
|
|
158
|
+
if (!device.isOnline) {
|
|
159
|
+
status = 'offline';
|
|
160
|
+
detail = 'Machine offline. Start `yaver serve` on the selected machine.';
|
|
161
|
+
} else if (device.needsAuth) {
|
|
162
|
+
status = 'attention';
|
|
163
|
+
detail = 'Machine needs pairing again before feedback actions can run.';
|
|
164
|
+
} else if (device.runnerDown) {
|
|
165
|
+
status = 'attention';
|
|
166
|
+
detail = 'Machine is online but the coding agent is down.';
|
|
167
|
+
} else if (reachable === false) {
|
|
168
|
+
status = 'offline';
|
|
169
|
+
detail = 'Machine selected, but the agent is not responding.';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (mountedRef.current) {
|
|
173
|
+
setMachineCard({
|
|
174
|
+
device,
|
|
175
|
+
reachable,
|
|
176
|
+
loading: false,
|
|
177
|
+
status,
|
|
178
|
+
title: device.name || device.deviceId,
|
|
179
|
+
detail,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (mountedRef.current) {
|
|
184
|
+
setMachineCard({
|
|
185
|
+
device: null,
|
|
186
|
+
reachable: null,
|
|
187
|
+
loading: false,
|
|
188
|
+
status: 'offline',
|
|
189
|
+
title: 'Machine status unavailable',
|
|
190
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, []);
|
|
195
|
+
|
|
85
196
|
useEffect(() => {
|
|
86
197
|
mountedRef.current = true;
|
|
87
198
|
const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
@@ -89,7 +200,10 @@ export const FeedbackModal: React.FC = () => {
|
|
|
89
200
|
setVisible(true);
|
|
90
201
|
setError(null);
|
|
91
202
|
setToast(null);
|
|
203
|
+
setProgress(null);
|
|
92
204
|
setAction('idle');
|
|
205
|
+
setShowVibeInput(false);
|
|
206
|
+
setVibePrompt('');
|
|
93
207
|
// Re-read the "user hid the quick icon" flag on every open so
|
|
94
208
|
// the re-enable row reflects the latest preference (the user
|
|
95
209
|
// might have hidden or shown it between opens).
|
|
@@ -98,6 +212,12 @@ export const FeedbackModal: React.FC = () => {
|
|
|
98
212
|
if (mountedRef.current) setQuickIconHidden(v);
|
|
99
213
|
})
|
|
100
214
|
.catch(() => {});
|
|
215
|
+
YaverFeedback.getQuickIconColorPreset()
|
|
216
|
+
.then((preset) => {
|
|
217
|
+
if (mountedRef.current) setQuickIconColorPreset(preset);
|
|
218
|
+
})
|
|
219
|
+
.catch(() => {});
|
|
220
|
+
void loadSelectedMachine();
|
|
101
221
|
}
|
|
102
222
|
});
|
|
103
223
|
// Agent streams build / compile progress through the BlackBox
|
|
@@ -126,7 +246,15 @@ export const FeedbackModal: React.FC = () => {
|
|
|
126
246
|
sub.remove();
|
|
127
247
|
statusSub.remove();
|
|
128
248
|
};
|
|
129
|
-
}, []);
|
|
249
|
+
}, [loadSelectedMachine]);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!visible) return;
|
|
253
|
+
const interval = setInterval(() => {
|
|
254
|
+
void loadSelectedMachine();
|
|
255
|
+
}, 5000);
|
|
256
|
+
return () => clearInterval(interval);
|
|
257
|
+
}, [loadSelectedMachine, visible]);
|
|
130
258
|
|
|
131
259
|
const closeSoon = useCallback((delayMs = 1200) => {
|
|
132
260
|
setTimeout(() => {
|
|
@@ -138,7 +266,10 @@ export const FeedbackModal: React.FC = () => {
|
|
|
138
266
|
setVisible(false);
|
|
139
267
|
setError(null);
|
|
140
268
|
setToast(null);
|
|
269
|
+
setProgress(null);
|
|
141
270
|
setAction('idle');
|
|
271
|
+
setShowVibeInput(false);
|
|
272
|
+
setVibePrompt('');
|
|
142
273
|
}, []);
|
|
143
274
|
|
|
144
275
|
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
@@ -196,51 +327,104 @@ export const FeedbackModal: React.FC = () => {
|
|
|
196
327
|
setAction('hot-reloading');
|
|
197
328
|
setError(null);
|
|
198
329
|
setProgress(0);
|
|
199
|
-
setToast('
|
|
330
|
+
setToast('Contacting selected machine…');
|
|
200
331
|
try {
|
|
332
|
+
await loadSelectedMachine();
|
|
333
|
+
const selected = await YaverFeedback.getSelectedRemoteDevice();
|
|
334
|
+
if (!selected) {
|
|
335
|
+
YaverFeedback.showMachinePicker();
|
|
336
|
+
throw new Error('No machine selected. Pick a machine and try again.');
|
|
337
|
+
}
|
|
338
|
+
if (selected.needsAuth) {
|
|
339
|
+
YaverFeedback.showMachinePicker();
|
|
340
|
+
throw new Error('Selected machine needs pairing again.');
|
|
341
|
+
}
|
|
342
|
+
if (!selected.isOnline) {
|
|
343
|
+
throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
|
|
344
|
+
}
|
|
345
|
+
|
|
201
346
|
// Default mode: bundle. Always rebuilds via the agent regardless
|
|
202
347
|
// of Metro state. P2PClient.reloadApp auto-resolves projectName +
|
|
203
348
|
// bundleId from expo-constants / NativeModules so the agent can
|
|
204
349
|
// map this app to its MobileProject scan entry without needing
|
|
205
350
|
// `yaver dev start` to have been run.
|
|
351
|
+
let ackMessage = 'Reload request acknowledged.';
|
|
206
352
|
await runWithReconnect(async (client) => {
|
|
207
|
-
await client.reloadApp('bundle');
|
|
353
|
+
const ack = await client.reloadApp('bundle');
|
|
354
|
+
ackMessage = ack.message;
|
|
355
|
+
setToast(ack.message);
|
|
356
|
+
setProgress(0.2);
|
|
208
357
|
});
|
|
209
358
|
// We don't auto-close here — the agent's BlackBox status pings
|
|
210
359
|
// will keep the modal updated, and the on-device YaverBundleLoader
|
|
211
360
|
// will reload the JS once the fresh bundle arrives. Modal stays
|
|
212
361
|
// up for a beat so the user sees the final progress state.
|
|
362
|
+
setToast(ackMessage);
|
|
213
363
|
closeSoon(2500);
|
|
214
364
|
} catch (err: unknown) {
|
|
215
|
-
|
|
365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
+
setError(message);
|
|
367
|
+
setToast(
|
|
368
|
+
message.toLowerCase().indexOf('session expired') >= 0
|
|
369
|
+
? 'Session expired. Sign in again.'
|
|
370
|
+
: 'Hot reload did not start.',
|
|
371
|
+
);
|
|
372
|
+
await loadSelectedMachine();
|
|
216
373
|
setProgress(null);
|
|
217
374
|
} finally {
|
|
218
375
|
if (mountedRef.current) setAction('idle');
|
|
219
376
|
}
|
|
220
|
-
}, [closeSoon, runWithReconnect]);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const handleScreenshotAndFix = useCallback(async () => {
|
|
377
|
+
}, [closeSoon, loadSelectedMachine, runWithReconnect]);
|
|
378
|
+
|
|
379
|
+
const uploadBundleWithOptionalFix = useCallback(async (
|
|
380
|
+
bundle: FeedbackBundle,
|
|
381
|
+
fixOnUpload: boolean,
|
|
382
|
+
successToast: string,
|
|
383
|
+
failureToast?: string,
|
|
384
|
+
) => {
|
|
229
385
|
const client = YaverFeedback.getP2PClient();
|
|
230
386
|
const config = YaverFeedback.getConfig();
|
|
231
387
|
if (!client || !config?.agentUrl) {
|
|
232
388
|
setError('Not connected to the agent yet.');
|
|
233
389
|
return;
|
|
234
390
|
}
|
|
391
|
+
try {
|
|
392
|
+
const uploaded = await uploadFeedback(
|
|
393
|
+
config.agentUrl,
|
|
394
|
+
config.authToken ?? '',
|
|
395
|
+
bundle,
|
|
396
|
+
);
|
|
397
|
+
// The agent returns the new report id as `id` (see
|
|
398
|
+
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
399
|
+
// one back; otherwise just ack the upload.
|
|
400
|
+
const reportId =
|
|
401
|
+
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
402
|
+
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
403
|
+
if (reportId && fixOnUpload) {
|
|
404
|
+
try {
|
|
405
|
+
await client.triggerFix(reportId);
|
|
406
|
+
setToast(successToast);
|
|
407
|
+
} catch (err: unknown) {
|
|
408
|
+
setToast(failureToast ?? 'Report uploaded — fix trigger failed');
|
|
409
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
setToast(successToast);
|
|
413
|
+
}
|
|
414
|
+
closeSoon(1400);
|
|
415
|
+
} catch (err: unknown) {
|
|
416
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
417
|
+
}
|
|
418
|
+
}, [closeSoon]);
|
|
419
|
+
|
|
420
|
+
const handleScreenshotAndFix = useCallback(async () => {
|
|
235
421
|
setAction('capturing');
|
|
236
422
|
setError(null);
|
|
237
423
|
|
|
238
|
-
// Step 1: Hide the modal so the screenshot contains the real screen.
|
|
239
424
|
setVisible(false);
|
|
240
|
-
// Wait out the slide-down animation on both platforms.
|
|
241
425
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
242
426
|
|
|
243
|
-
let path: string
|
|
427
|
+
let path: string;
|
|
244
428
|
try {
|
|
245
429
|
path = await captureScreenshot();
|
|
246
430
|
} catch (err: unknown) {
|
|
@@ -250,7 +434,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
250
434
|
return;
|
|
251
435
|
}
|
|
252
436
|
|
|
253
|
-
// Step 2: Re-show the modal for progress + ack.
|
|
254
437
|
setVisible(true);
|
|
255
438
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
256
439
|
|
|
@@ -264,7 +447,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
264
447
|
screenWidth: width,
|
|
265
448
|
screenHeight: height,
|
|
266
449
|
};
|
|
267
|
-
|
|
268
450
|
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
269
451
|
const bundle: FeedbackBundle = {
|
|
270
452
|
metadata: {
|
|
@@ -276,36 +458,21 @@ export const FeedbackModal: React.FC = () => {
|
|
|
276
458
|
screenshots: [path],
|
|
277
459
|
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
278
460
|
};
|
|
279
|
-
|
|
280
|
-
const uploaded = await uploadFeedback(
|
|
281
|
-
config.agentUrl,
|
|
282
|
-
config.authToken ?? '',
|
|
461
|
+
await uploadBundleWithOptionalFix(
|
|
283
462
|
bundle,
|
|
463
|
+
true,
|
|
464
|
+
'Fix task started',
|
|
284
465
|
);
|
|
285
|
-
// The agent returns the new report id as `id` (see
|
|
286
|
-
// feedback_http.go::ReceiveFeedback). Trigger the fix loop if we got
|
|
287
|
-
// one back; otherwise just ack the upload.
|
|
288
|
-
const reportId =
|
|
289
|
-
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
290
|
-
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
291
|
-
if (reportId) {
|
|
292
|
-
try {
|
|
293
|
-
await client.triggerFix(reportId);
|
|
294
|
-
setToast('Fix task started');
|
|
295
|
-
} catch (err: unknown) {
|
|
296
|
-
setToast('Report uploaded — fix trigger failed');
|
|
297
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
setToast('Report uploaded');
|
|
301
|
-
}
|
|
302
|
-
closeSoon(1400);
|
|
303
|
-
} catch (err: unknown) {
|
|
304
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
305
466
|
} finally {
|
|
306
467
|
if (mountedRef.current) setAction('idle');
|
|
307
468
|
}
|
|
308
|
-
}, [
|
|
469
|
+
}, [uploadBundleWithOptionalFix]);
|
|
470
|
+
|
|
471
|
+
/*
|
|
472
|
+
const handleFileUpload = useCallback(async () => {
|
|
473
|
+
...
|
|
474
|
+
}, [uploadBundleWithOptionalFix]);
|
|
475
|
+
*/
|
|
309
476
|
|
|
310
477
|
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
311
478
|
// First tap expands the input; second submit fires the actual
|
|
@@ -359,172 +526,11 @@ export const FeedbackModal: React.FC = () => {
|
|
|
359
526
|
}
|
|
360
527
|
}, [vibePrompt]);
|
|
361
528
|
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const result = await stopVideoRecording();
|
|
368
|
-
if (mountedRef.current) {
|
|
369
|
-
setIsRecordingVideo(false);
|
|
370
|
-
setLastVideo(result);
|
|
371
|
-
setToast(`Recording stopped — ${Math.round(result.duration)}s`);
|
|
372
|
-
}
|
|
373
|
-
} catch (err: unknown) {
|
|
374
|
-
setIsRecordingVideo(false);
|
|
375
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
376
|
-
}
|
|
377
|
-
} else {
|
|
378
|
-
try {
|
|
379
|
-
await startVideoRecording();
|
|
380
|
-
if (mountedRef.current) {
|
|
381
|
-
setIsRecordingVideo(true);
|
|
382
|
-
setToast('Recording…');
|
|
383
|
-
setLastVideo(null);
|
|
384
|
-
}
|
|
385
|
-
} catch (err: unknown) {
|
|
386
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}, [isRecordingVideo]);
|
|
390
|
-
|
|
391
|
-
// ─── Voice note: record → transcribe → send as feedback ───────────
|
|
392
|
-
// Tap once to start; tap again to stop. On stop: upload the audio
|
|
393
|
-
// to the agent's /voice/transcribe (which routes through whichever
|
|
394
|
-
// STT provider is configured — Whisper / Deepgram / OpenAI / etc.),
|
|
395
|
-
// then file the transcript as a bug report. The audio file itself
|
|
396
|
-
// is also attached to the feedback bundle so the agent can re-play
|
|
397
|
-
// it if the transcript is wrong.
|
|
398
|
-
const handleToggleVoice = useCallback(async () => {
|
|
399
|
-
setError(null);
|
|
400
|
-
if (!isRecordingVoice) {
|
|
401
|
-
try {
|
|
402
|
-
await startAudioRecording();
|
|
403
|
-
if (mountedRef.current) {
|
|
404
|
-
setIsRecordingVoice(true);
|
|
405
|
-
setAction('recording-voice');
|
|
406
|
-
setToast('Recording voice note…');
|
|
407
|
-
}
|
|
408
|
-
} catch (err: unknown) {
|
|
409
|
-
setIsRecordingVoice(false);
|
|
410
|
-
setAction('idle');
|
|
411
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
412
|
-
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Stopping → transcribe → send.
|
|
417
|
-
try {
|
|
418
|
-
const audio = await stopAudioRecording();
|
|
419
|
-
setIsRecordingVoice(false);
|
|
420
|
-
if (!audio) {
|
|
421
|
-
setAction('idle');
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
setAction('transcribing-voice');
|
|
425
|
-
setToast('Transcribing…');
|
|
426
|
-
|
|
427
|
-
const config = YaverFeedback.getConfig();
|
|
428
|
-
if (!config?.agentUrl) {
|
|
429
|
-
setError('Not connected to the agent yet.');
|
|
430
|
-
setAction('idle');
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
let transcript = '';
|
|
434
|
-
try {
|
|
435
|
-
const client = YaverFeedback.getP2PClient();
|
|
436
|
-
if (client) {
|
|
437
|
-
const res = await client.transcribeVoice(audio.path);
|
|
438
|
-
transcript = res.text ?? '';
|
|
439
|
-
}
|
|
440
|
-
} catch {
|
|
441
|
-
// Transcription can fail (no STT provider configured on the
|
|
442
|
-
// agent, network blip, etc.). Don't block the flow — ship
|
|
443
|
-
// the raw audio file with a "[no transcript]" note so the
|
|
444
|
-
// agent + human reviewer can still play it back.
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const { Dimensions } = require('react-native');
|
|
448
|
-
const { width, height } = Dimensions.get('window');
|
|
449
|
-
const deviceInfo: DeviceInfo = {
|
|
450
|
-
platform: Platform.OS,
|
|
451
|
-
osVersion: String(Platform.Version),
|
|
452
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
453
|
-
screenWidth: width,
|
|
454
|
-
screenHeight: height,
|
|
455
|
-
};
|
|
456
|
-
const bundle: FeedbackBundle = {
|
|
457
|
-
metadata: {
|
|
458
|
-
timestamp: new Date().toISOString(),
|
|
459
|
-
device: deviceInfo,
|
|
460
|
-
app: {},
|
|
461
|
-
userNote:
|
|
462
|
-
transcript.length > 0
|
|
463
|
-
? `[Voice note] ${transcript}`
|
|
464
|
-
: `[Voice note · ${Math.round(audio.duration)}s — transcription unavailable]`,
|
|
465
|
-
},
|
|
466
|
-
screenshots: [],
|
|
467
|
-
audio: audio.path,
|
|
468
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
469
|
-
? YaverFeedback.getCapturedErrors()
|
|
470
|
-
: undefined,
|
|
471
|
-
};
|
|
472
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
473
|
-
setToast(transcript ? `Sent: "${transcript.slice(0, 60)}${transcript.length > 60 ? '…' : ''}"` : 'Voice note sent');
|
|
474
|
-
closeSoon(1800);
|
|
475
|
-
} catch (err: unknown) {
|
|
476
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
477
|
-
} finally {
|
|
478
|
-
if (mountedRef.current) setAction('idle');
|
|
479
|
-
}
|
|
480
|
-
}, [isRecordingVoice, closeSoon]);
|
|
481
|
-
|
|
482
|
-
// ─── 5. Send the last recorded video ───────────────────────────────
|
|
483
|
-
const handleSendVideo = useCallback(async () => {
|
|
484
|
-
const config = YaverFeedback.getConfig();
|
|
485
|
-
if (!config?.agentUrl) {
|
|
486
|
-
setError('Not connected to the agent yet.');
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (!lastVideo) {
|
|
490
|
-
setError('No video recorded yet.');
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
setAction('sending-video');
|
|
494
|
-
setError(null);
|
|
495
|
-
try {
|
|
496
|
-
const { Dimensions } = require('react-native');
|
|
497
|
-
const { width, height } = Dimensions.get('window');
|
|
498
|
-
const deviceInfo: DeviceInfo = {
|
|
499
|
-
platform: Platform.OS,
|
|
500
|
-
osVersion: String(Platform.Version),
|
|
501
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
502
|
-
screenWidth: width,
|
|
503
|
-
screenHeight: height,
|
|
504
|
-
};
|
|
505
|
-
const bundle: FeedbackBundle = {
|
|
506
|
-
metadata: {
|
|
507
|
-
timestamp: new Date().toISOString(),
|
|
508
|
-
device: deviceInfo,
|
|
509
|
-
app: {},
|
|
510
|
-
userNote: '[Screen recording]',
|
|
511
|
-
},
|
|
512
|
-
screenshots: [],
|
|
513
|
-
video: lastVideo.path,
|
|
514
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
515
|
-
? YaverFeedback.getCapturedErrors()
|
|
516
|
-
: undefined,
|
|
517
|
-
};
|
|
518
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
519
|
-
setToast('Video sent');
|
|
520
|
-
setLastVideo(null);
|
|
521
|
-
closeSoon(1200);
|
|
522
|
-
} catch (err: unknown) {
|
|
523
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
524
|
-
} finally {
|
|
525
|
-
if (mountedRef.current) setAction('idle');
|
|
526
|
-
}
|
|
527
|
-
}, [lastVideo, closeSoon]);
|
|
529
|
+
/*
|
|
530
|
+
const handleScreenRecording = useCallback(async () => {
|
|
531
|
+
...
|
|
532
|
+
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
533
|
+
*/
|
|
528
534
|
|
|
529
535
|
const busy = action !== 'idle';
|
|
530
536
|
|
|
@@ -540,7 +546,23 @@ export const FeedbackModal: React.FC = () => {
|
|
|
540
546
|
onRequestClose={handleClose}
|
|
541
547
|
>
|
|
542
548
|
<Pressable style={styles.overlay} onPress={handleClose}>
|
|
543
|
-
<
|
|
549
|
+
<KeyboardAvoidingView
|
|
550
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
551
|
+
style={styles.kbAvoider}
|
|
552
|
+
pointerEvents="box-none"
|
|
553
|
+
>
|
|
554
|
+
<Pressable
|
|
555
|
+
style={styles.modal}
|
|
556
|
+
onPress={(e) => {
|
|
557
|
+
e.stopPropagation();
|
|
558
|
+
Keyboard.dismiss();
|
|
559
|
+
}}
|
|
560
|
+
>
|
|
561
|
+
<ScrollView
|
|
562
|
+
style={styles.scroll}
|
|
563
|
+
contentContainerStyle={styles.scrollContent}
|
|
564
|
+
keyboardShouldPersistTaps="handled"
|
|
565
|
+
>
|
|
544
566
|
<View style={styles.header}>
|
|
545
567
|
<Text style={styles.title}>Send Feedback</Text>
|
|
546
568
|
<Pressable
|
|
@@ -554,6 +576,111 @@ export const FeedbackModal: React.FC = () => {
|
|
|
554
576
|
</Pressable>
|
|
555
577
|
</View>
|
|
556
578
|
|
|
579
|
+
<Pressable
|
|
580
|
+
onPress={() => {
|
|
581
|
+
if (!YaverFeedback.isAuthed()) {
|
|
582
|
+
YaverFeedback.showLogin();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
YaverFeedback.showMachinePicker();
|
|
586
|
+
}}
|
|
587
|
+
style={[
|
|
588
|
+
styles.machineCard,
|
|
589
|
+
machineCard.status === 'live' && styles.machineCardLive,
|
|
590
|
+
machineCard.status === 'attention' && styles.machineCardAttention,
|
|
591
|
+
machineCard.status === 'offline' && styles.machineCardOffline,
|
|
592
|
+
]}
|
|
593
|
+
>
|
|
594
|
+
<View style={styles.machineHeader}>
|
|
595
|
+
<View style={styles.machineTitleWrap}>
|
|
596
|
+
<View
|
|
597
|
+
style={[
|
|
598
|
+
styles.machineDot,
|
|
599
|
+
machineCard.status === 'live' && styles.machineDotLive,
|
|
600
|
+
machineCard.status === 'attention' && styles.machineDotAttention,
|
|
601
|
+
machineCard.status === 'offline' && styles.machineDotOffline,
|
|
602
|
+
]}
|
|
603
|
+
/>
|
|
604
|
+
<Text style={styles.machineLabel}>Selected Machine</Text>
|
|
605
|
+
</View>
|
|
606
|
+
<Text style={styles.machineAction}>
|
|
607
|
+
{machineCard.loading ? 'Refreshing…' : 'Change'}
|
|
608
|
+
</Text>
|
|
609
|
+
</View>
|
|
610
|
+
<Text style={styles.machineName}>
|
|
611
|
+
{machineCard.loading ? 'Checking machine…' : machineCard.title}
|
|
612
|
+
</Text>
|
|
613
|
+
<Text style={styles.machineMeta}>{machineCard.detail}</Text>
|
|
614
|
+
</Pressable>
|
|
615
|
+
|
|
616
|
+
{quickIconHidden && (
|
|
617
|
+
<View style={styles.quickIconNote}>
|
|
618
|
+
<Text style={styles.quickIconNoteText}>
|
|
619
|
+
Quick access icon is hidden. Shake the phone if you want feedback back fast.
|
|
620
|
+
</Text>
|
|
621
|
+
<Pressable
|
|
622
|
+
onPress={() => {
|
|
623
|
+
void YaverFeedback.setQuickIconVisible(true);
|
|
624
|
+
setQuickIconHidden(false);
|
|
625
|
+
}}
|
|
626
|
+
style={({ pressed }) => [
|
|
627
|
+
styles.quickIconToggle,
|
|
628
|
+
pressed && styles.buttonPressed,
|
|
629
|
+
]}
|
|
630
|
+
>
|
|
631
|
+
<Text style={styles.quickIconToggleText}>Show quick icon again</Text>
|
|
632
|
+
</Pressable>
|
|
633
|
+
</View>
|
|
634
|
+
)}
|
|
635
|
+
|
|
636
|
+
<View style={styles.iconSelector}>
|
|
637
|
+
<Text style={styles.iconSelectorTitle}>Quick Icon Color</Text>
|
|
638
|
+
<Text style={styles.iconSelectorText}>
|
|
639
|
+
Pick a runtime color so the floating y icon does not overlap with your app UI.
|
|
640
|
+
</Text>
|
|
641
|
+
<View style={styles.iconSelectorGrid}>
|
|
642
|
+
{(Object.entries(QUICK_ICON_COLOR_PRESETS) as Array<
|
|
643
|
+
[QuickIconColorPreset, (typeof QUICK_ICON_COLOR_PRESETS)[QuickIconColorPreset]]
|
|
644
|
+
>).map(([preset, colors]) => {
|
|
645
|
+
const selected = quickIconColorPreset === preset;
|
|
646
|
+
return (
|
|
647
|
+
<Pressable
|
|
648
|
+
key={preset}
|
|
649
|
+
onPress={() => {
|
|
650
|
+
setQuickIconColorPreset(preset);
|
|
651
|
+
void YaverFeedback.setQuickIconColorPreset(preset);
|
|
652
|
+
}}
|
|
653
|
+
style={[
|
|
654
|
+
styles.iconOption,
|
|
655
|
+
selected && styles.iconOptionSelected,
|
|
656
|
+
]}
|
|
657
|
+
>
|
|
658
|
+
<View
|
|
659
|
+
style={[
|
|
660
|
+
styles.iconOptionCircle,
|
|
661
|
+
{
|
|
662
|
+
backgroundColor: colors.backgroundColor,
|
|
663
|
+
borderColor: colors.borderColor,
|
|
664
|
+
shadowColor: colors.shadowColor,
|
|
665
|
+
},
|
|
666
|
+
]}
|
|
667
|
+
>
|
|
668
|
+
<Text
|
|
669
|
+
style={[
|
|
670
|
+
styles.iconOptionLabel,
|
|
671
|
+
{ color: colors.foregroundColor },
|
|
672
|
+
]}
|
|
673
|
+
>
|
|
674
|
+
y
|
|
675
|
+
</Text>
|
|
676
|
+
</View>
|
|
677
|
+
<Text style={styles.iconOptionText}>{colors.label}</Text>
|
|
678
|
+
</Pressable>
|
|
679
|
+
);
|
|
680
|
+
})}
|
|
681
|
+
</View>
|
|
682
|
+
</View>
|
|
683
|
+
|
|
557
684
|
{/* 1. Hot Reload — the common path */}
|
|
558
685
|
<ActionRow
|
|
559
686
|
label={
|
|
@@ -565,19 +692,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
565
692
|
busy={action === 'hot-reloading'}
|
|
566
693
|
/>
|
|
567
694
|
|
|
568
|
-
{/* 2. Screenshot + Fix — for bug fixes */}
|
|
569
|
-
<ActionRow
|
|
570
|
-
label={
|
|
571
|
-
action === 'capturing'
|
|
572
|
-
? 'Capturing…'
|
|
573
|
-
: 'Screenshot & Fix'
|
|
574
|
-
}
|
|
575
|
-
tint="#22c55e"
|
|
576
|
-
onPress={handleScreenshotAndFix}
|
|
577
|
-
disabled={busy}
|
|
578
|
-
busy={action === 'capturing'}
|
|
579
|
-
/>
|
|
580
|
-
|
|
581
695
|
{/* 3. Vibing — expands to an input box on first tap
|
|
582
696
|
so the user says WHAT they want to vibe on, just
|
|
583
697
|
like the Yaver mobile app's Vibing tab. Second
|
|
@@ -637,46 +751,13 @@ export const FeedbackModal: React.FC = () => {
|
|
|
637
751
|
</Text>
|
|
638
752
|
)}
|
|
639
753
|
|
|
640
|
-
{/*
|
|
641
|
-
Tap to start, tap again to stop → transcribes via
|
|
642
|
-
the agent and files as a feedback report. */}
|
|
643
|
-
{voiceSupported && (
|
|
644
|
-
<ActionRow
|
|
645
|
-
label={
|
|
646
|
-
action === 'transcribing-voice'
|
|
647
|
-
? 'Transcribing…'
|
|
648
|
-
: isRecordingVoice
|
|
649
|
-
? 'Stop & Send Voice'
|
|
650
|
-
: 'Voice Note'
|
|
651
|
-
}
|
|
652
|
-
tint={isRecordingVoice ? '#ef4444' : '#f472b6'}
|
|
653
|
-
onPress={handleToggleVoice}
|
|
654
|
-
disabled={busy && action !== 'recording-voice' && action !== 'idle'}
|
|
655
|
-
busy={action === 'transcribing-voice'}
|
|
656
|
-
/>
|
|
657
|
-
)}
|
|
658
|
-
|
|
659
|
-
{/* 4. Start/Stop Recording */}
|
|
754
|
+
{/* Screenshot & Fix */}
|
|
660
755
|
<ActionRow
|
|
661
|
-
label={
|
|
662
|
-
tint=
|
|
663
|
-
onPress={
|
|
664
|
-
disabled={busy
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
{/* 5. Send Video (only tappable when a clip is ready) */}
|
|
668
|
-
<ActionRow
|
|
669
|
-
label={
|
|
670
|
-
action === 'sending-video'
|
|
671
|
-
? 'Sending…'
|
|
672
|
-
: lastVideo
|
|
673
|
-
? `Send Video · ${Math.round(lastVideo.duration)}s`
|
|
674
|
-
: 'Send Video'
|
|
675
|
-
}
|
|
676
|
-
tint="#a78bfa"
|
|
677
|
-
onPress={handleSendVideo}
|
|
678
|
-
disabled={busy || !lastVideo}
|
|
679
|
-
busy={action === 'sending-video'}
|
|
756
|
+
label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'}
|
|
757
|
+
tint="#22c55e"
|
|
758
|
+
onPress={handleScreenshotAndFix}
|
|
759
|
+
disabled={busy}
|
|
760
|
+
busy={action === 'capturing'}
|
|
680
761
|
/>
|
|
681
762
|
|
|
682
763
|
{progress !== null && (
|
|
@@ -692,34 +773,20 @@ export const FeedbackModal: React.FC = () => {
|
|
|
692
773
|
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
693
774
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
694
775
|
|
|
695
|
-
{/* Quick-icon toggle. The user's three ways to control
|
|
696
|
-
the floating icon are: (1) long-press the icon →
|
|
697
|
-
Hide, (2) tap this row to toggle it on/off, (3) shake
|
|
698
|
-
→ this modal → tap this row. Shake is the unkillable
|
|
699
|
-
back-door when the icon is hidden and the dev hasn't
|
|
700
|
-
exposed their own settings UI. */}
|
|
701
776
|
<Pressable
|
|
702
|
-
onPress={
|
|
703
|
-
const next = !quickIconHidden;
|
|
704
|
-
setQuickIconHidden(next);
|
|
705
|
-
await YaverFeedback.setQuickIconVisible(!next);
|
|
706
|
-
}}
|
|
777
|
+
onPress={handleClose}
|
|
707
778
|
style={({ pressed }) => [
|
|
708
|
-
styles.
|
|
709
|
-
pressed &&
|
|
779
|
+
styles.cancelBtn,
|
|
780
|
+
pressed && styles.buttonPressed,
|
|
710
781
|
]}
|
|
711
782
|
accessibilityRole="button"
|
|
712
|
-
accessibilityLabel=
|
|
713
|
-
quickIconHidden ? 'Show quick icon' : 'Hide quick icon'
|
|
714
|
-
}
|
|
783
|
+
accessibilityLabel="Cancel"
|
|
715
784
|
>
|
|
716
|
-
<Text style={styles.
|
|
717
|
-
{quickIconHidden
|
|
718
|
-
? '◯ Show quick-access icon'
|
|
719
|
-
: '● Hide quick-access icon'}
|
|
720
|
-
</Text>
|
|
785
|
+
<Text style={styles.cancelBtnText}>Cancel</Text>
|
|
721
786
|
</Pressable>
|
|
787
|
+
</ScrollView>
|
|
722
788
|
</Pressable>
|
|
789
|
+
</KeyboardAvoidingView>
|
|
723
790
|
</Pressable>
|
|
724
791
|
</Modal>
|
|
725
792
|
)}
|
|
@@ -821,6 +888,10 @@ const styles = StyleSheet.create({
|
|
|
821
888
|
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
822
889
|
justifyContent: 'flex-end',
|
|
823
890
|
},
|
|
891
|
+
kbAvoider: {
|
|
892
|
+
width: '100%',
|
|
893
|
+
justifyContent: 'flex-end',
|
|
894
|
+
},
|
|
824
895
|
modal: {
|
|
825
896
|
backgroundColor: '#141422',
|
|
826
897
|
borderTopLeftRadius: 22,
|
|
@@ -828,6 +899,14 @@ const styles = StyleSheet.create({
|
|
|
828
899
|
padding: 22,
|
|
829
900
|
paddingBottom: 36,
|
|
830
901
|
gap: 12,
|
|
902
|
+
maxHeight: '92%',
|
|
903
|
+
},
|
|
904
|
+
scroll: {
|
|
905
|
+
maxHeight: '100%',
|
|
906
|
+
},
|
|
907
|
+
scrollContent: {
|
|
908
|
+
gap: 12,
|
|
909
|
+
paddingBottom: 8,
|
|
831
910
|
},
|
|
832
911
|
header: {
|
|
833
912
|
flexDirection: 'row',
|
|
@@ -868,6 +947,77 @@ const styles = StyleSheet.create({
|
|
|
868
947
|
fontSize: 15,
|
|
869
948
|
fontWeight: '700',
|
|
870
949
|
},
|
|
950
|
+
machineCard: {
|
|
951
|
+
borderRadius: 14,
|
|
952
|
+
borderWidth: 1,
|
|
953
|
+
padding: 14,
|
|
954
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
955
|
+
borderColor: 'rgba(255,255,255,0.12)',
|
|
956
|
+
},
|
|
957
|
+
machineCardLive: {
|
|
958
|
+
backgroundColor: 'rgba(34,197,94,0.10)',
|
|
959
|
+
borderColor: 'rgba(34,197,94,0.35)',
|
|
960
|
+
},
|
|
961
|
+
machineCardAttention: {
|
|
962
|
+
backgroundColor: 'rgba(245,158,11,0.10)',
|
|
963
|
+
borderColor: 'rgba(245,158,11,0.35)',
|
|
964
|
+
},
|
|
965
|
+
machineCardOffline: {
|
|
966
|
+
backgroundColor: 'rgba(239,68,68,0.10)',
|
|
967
|
+
borderColor: 'rgba(239,68,68,0.35)',
|
|
968
|
+
},
|
|
969
|
+
machineHeader: {
|
|
970
|
+
flexDirection: 'row',
|
|
971
|
+
alignItems: 'center',
|
|
972
|
+
justifyContent: 'space-between',
|
|
973
|
+
marginBottom: 6,
|
|
974
|
+
},
|
|
975
|
+
machineTitleWrap: {
|
|
976
|
+
flexDirection: 'row',
|
|
977
|
+
alignItems: 'center',
|
|
978
|
+
gap: 8,
|
|
979
|
+
},
|
|
980
|
+
machineDot: {
|
|
981
|
+
width: 10,
|
|
982
|
+
height: 10,
|
|
983
|
+
borderRadius: 5,
|
|
984
|
+
backgroundColor: '#6b7280',
|
|
985
|
+
},
|
|
986
|
+
machineDotLive: {
|
|
987
|
+
backgroundColor: '#22c55e',
|
|
988
|
+
},
|
|
989
|
+
machineDotAttention: {
|
|
990
|
+
backgroundColor: '#f59e0b',
|
|
991
|
+
},
|
|
992
|
+
machineDotOffline: {
|
|
993
|
+
backgroundColor: '#ef4444',
|
|
994
|
+
},
|
|
995
|
+
machineLabel: {
|
|
996
|
+
color: '#cbd5e1',
|
|
997
|
+
fontSize: 12,
|
|
998
|
+
fontWeight: '700',
|
|
999
|
+
textTransform: 'uppercase',
|
|
1000
|
+
letterSpacing: 0.8,
|
|
1001
|
+
},
|
|
1002
|
+
machineAction: {
|
|
1003
|
+
color: '#a5b4fc',
|
|
1004
|
+
fontSize: 12,
|
|
1005
|
+
fontWeight: '700',
|
|
1006
|
+
},
|
|
1007
|
+
machineName: {
|
|
1008
|
+
color: '#fff',
|
|
1009
|
+
fontSize: 16,
|
|
1010
|
+
fontWeight: '700',
|
|
1011
|
+
},
|
|
1012
|
+
machineMeta: {
|
|
1013
|
+
color: '#cbd5e1',
|
|
1014
|
+
fontSize: 12,
|
|
1015
|
+
marginTop: 4,
|
|
1016
|
+
lineHeight: 17,
|
|
1017
|
+
},
|
|
1018
|
+
captureChoices: {
|
|
1019
|
+
gap: 10,
|
|
1020
|
+
},
|
|
871
1021
|
progressTrack: {
|
|
872
1022
|
height: 6,
|
|
873
1023
|
borderRadius: 3,
|
|
@@ -893,14 +1043,106 @@ const styles = StyleSheet.create({
|
|
|
893
1043
|
marginTop: 4,
|
|
894
1044
|
},
|
|
895
1045
|
quickIconToggle: {
|
|
896
|
-
marginTop:
|
|
897
|
-
alignSelf: '
|
|
1046
|
+
marginTop: 6,
|
|
1047
|
+
alignSelf: 'flex-start',
|
|
898
1048
|
paddingVertical: 6,
|
|
899
|
-
paddingHorizontal:
|
|
1049
|
+
paddingHorizontal: 10,
|
|
1050
|
+
borderRadius: 10,
|
|
1051
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
900
1052
|
},
|
|
901
1053
|
quickIconToggleText: {
|
|
902
|
-
color: '#
|
|
1054
|
+
color: '#cbd5e1',
|
|
903
1055
|
fontSize: 12,
|
|
904
|
-
fontWeight: '
|
|
1056
|
+
fontWeight: '700',
|
|
1057
|
+
},
|
|
1058
|
+
quickIconNote: {
|
|
1059
|
+
borderRadius: 12,
|
|
1060
|
+
borderWidth: 1,
|
|
1061
|
+
borderColor: 'rgba(251,191,36,0.28)',
|
|
1062
|
+
backgroundColor: 'rgba(251,191,36,0.08)',
|
|
1063
|
+
padding: 12,
|
|
1064
|
+
},
|
|
1065
|
+
quickIconNoteText: {
|
|
1066
|
+
color: '#fde68a',
|
|
1067
|
+
fontSize: 12,
|
|
1068
|
+
lineHeight: 17,
|
|
1069
|
+
},
|
|
1070
|
+
iconSelector: {
|
|
1071
|
+
borderRadius: 12,
|
|
1072
|
+
borderWidth: 1,
|
|
1073
|
+
borderColor: 'rgba(255,255,255,0.09)',
|
|
1074
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
1075
|
+
padding: 12,
|
|
1076
|
+
gap: 10,
|
|
1077
|
+
},
|
|
1078
|
+
iconSelectorTitle: {
|
|
1079
|
+
color: '#f8fafc',
|
|
1080
|
+
fontSize: 13,
|
|
1081
|
+
fontWeight: '700',
|
|
1082
|
+
},
|
|
1083
|
+
iconSelectorText: {
|
|
1084
|
+
color: '#cbd5e1',
|
|
1085
|
+
fontSize: 12,
|
|
1086
|
+
lineHeight: 17,
|
|
1087
|
+
},
|
|
1088
|
+
iconSelectorGrid: {
|
|
1089
|
+
flexDirection: 'row',
|
|
1090
|
+
flexWrap: 'wrap',
|
|
1091
|
+
gap: 10,
|
|
1092
|
+
},
|
|
1093
|
+
iconOption: {
|
|
1094
|
+
width: '31%',
|
|
1095
|
+
minWidth: 84,
|
|
1096
|
+
borderRadius: 12,
|
|
1097
|
+
borderWidth: 1,
|
|
1098
|
+
borderColor: 'rgba(255,255,255,0.08)',
|
|
1099
|
+
backgroundColor: 'rgba(255,255,255,0.02)',
|
|
1100
|
+
paddingVertical: 10,
|
|
1101
|
+
paddingHorizontal: 8,
|
|
1102
|
+
alignItems: 'center',
|
|
1103
|
+
gap: 8,
|
|
1104
|
+
},
|
|
1105
|
+
iconOptionSelected: {
|
|
1106
|
+
borderColor: 'rgba(129,140,248,0.72)',
|
|
1107
|
+
backgroundColor: 'rgba(129,140,248,0.12)',
|
|
1108
|
+
},
|
|
1109
|
+
iconOptionCircle: {
|
|
1110
|
+
width: 38,
|
|
1111
|
+
height: 38,
|
|
1112
|
+
borderRadius: 19,
|
|
1113
|
+
alignItems: 'center',
|
|
1114
|
+
justifyContent: 'center',
|
|
1115
|
+
borderWidth: 2,
|
|
1116
|
+
shadowOffset: { width: 0, height: 2 },
|
|
1117
|
+
shadowOpacity: 0.28,
|
|
1118
|
+
shadowRadius: 5,
|
|
1119
|
+
elevation: 5,
|
|
1120
|
+
},
|
|
1121
|
+
iconOptionLabel: {
|
|
1122
|
+
fontSize: 18,
|
|
1123
|
+
fontWeight: '700',
|
|
1124
|
+
},
|
|
1125
|
+
iconOptionText: {
|
|
1126
|
+
color: '#e2e8f0',
|
|
1127
|
+
fontSize: 11,
|
|
1128
|
+
fontWeight: '600',
|
|
1129
|
+
},
|
|
1130
|
+
cancelBtn: {
|
|
1131
|
+
marginTop: 4,
|
|
1132
|
+
borderRadius: 14,
|
|
1133
|
+
borderWidth: 1,
|
|
1134
|
+
borderColor: 'rgba(255,255,255,0.1)',
|
|
1135
|
+
paddingVertical: 15,
|
|
1136
|
+
alignItems: 'center',
|
|
1137
|
+
justifyContent: 'center',
|
|
1138
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
1139
|
+
},
|
|
1140
|
+
cancelBtnText: {
|
|
1141
|
+
color: '#e5e7eb',
|
|
1142
|
+
fontSize: 15,
|
|
1143
|
+
fontWeight: '700',
|
|
1144
|
+
},
|
|
1145
|
+
buttonPressed: {
|
|
1146
|
+
opacity: 0.7,
|
|
905
1147
|
},
|
|
906
1148
|
});
|