r1-create 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/index.d.ts +125 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +237 -4
- package/dist/llm/index.js.map +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/examples/README.md +36 -0
- package/examples/camera/index.html +160 -0
- package/examples/device-controls/index.html +191 -0
- package/examples/hardware-game/index.html +165 -0
- package/examples/messaging-test/index.html +359 -0
- package/examples/nextjs-test-app/README.md +72 -0
- package/examples/nextjs-test-app/app/globals.css +282 -0
- package/examples/nextjs-test-app/app/layout.jsx +14 -0
- package/examples/nextjs-test-app/app/page.jsx +1265 -0
- package/examples/nextjs-test-app/log-relay/device-console-bridge.js +91 -0
- package/examples/nextjs-test-app/log-relay/server.js +55 -0
- package/examples/nextjs-test-app/next.config.js +8 -0
- package/examples/nextjs-test-app/package-lock.json +1227 -0
- package/examples/nextjs-test-app/package.json +18 -0
- package/examples/text-to-speech/index.html +128 -0
- package/examples/ui-design/index.html +189 -0
- package/examples/voice-recorder/index.html +149 -0
- package/examples/web-search/index.html +147 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { io } from 'socket.io-client';
|
|
5
|
+
import {
|
|
6
|
+
r1,
|
|
7
|
+
Base64Utils,
|
|
8
|
+
R1Storage,
|
|
9
|
+
CSSUtils,
|
|
10
|
+
DOMUtils,
|
|
11
|
+
LayoutUtils,
|
|
12
|
+
PerformanceUtils,
|
|
13
|
+
R1Component,
|
|
14
|
+
MediaUtils,
|
|
15
|
+
R1_DIMENSIONS
|
|
16
|
+
} from '../../../src/index';
|
|
17
|
+
|
|
18
|
+
function now() {
|
|
19
|
+
return new Date().toISOString().slice(11, 19);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseError(error) {
|
|
23
|
+
if (!error) return 'Unknown error';
|
|
24
|
+
if (typeof error === 'string') return error;
|
|
25
|
+
if (error instanceof Error) return error.message;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(error);
|
|
28
|
+
} catch {
|
|
29
|
+
return String(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeSampleBase64() {
|
|
34
|
+
const canvas = document.createElement('canvas');
|
|
35
|
+
canvas.width = 64;
|
|
36
|
+
canvas.height = 64;
|
|
37
|
+
const ctx = canvas.getContext('2d');
|
|
38
|
+
if (!ctx) return '';
|
|
39
|
+
|
|
40
|
+
ctx.fillStyle = '#141414';
|
|
41
|
+
ctx.fillRect(0, 0, 64, 64);
|
|
42
|
+
ctx.fillStyle = '#fe5f00';
|
|
43
|
+
ctx.fillRect(8, 8, 48, 48);
|
|
44
|
+
ctx.fillStyle = '#fff';
|
|
45
|
+
ctx.font = 'bold 10px sans-serif';
|
|
46
|
+
ctx.fillText('R1', 22, 36);
|
|
47
|
+
|
|
48
|
+
return canvas.toDataURL('image/png').split(',')[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeFixed(value) {
|
|
52
|
+
const numeric = Number(value);
|
|
53
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
54
|
+
return Number(numeric.toFixed(3));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeAccelData(payload) {
|
|
58
|
+
if (!payload) {
|
|
59
|
+
return { x: 0, y: 0, z: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let parsedPayload = payload;
|
|
63
|
+
if (typeof payload === 'string') {
|
|
64
|
+
try {
|
|
65
|
+
parsedPayload = JSON.parse(payload);
|
|
66
|
+
} catch {
|
|
67
|
+
return { x: 0, y: 0, z: 0 };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof parsedPayload !== 'object' || parsedPayload === null) {
|
|
72
|
+
return { x: 0, y: 0, z: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const source = parsedPayload.data && typeof parsedPayload.data === 'object'
|
|
76
|
+
? parsedPayload.data
|
|
77
|
+
: parsedPayload.detail && typeof parsedPayload.detail === 'object'
|
|
78
|
+
? parsedPayload.detail
|
|
79
|
+
: parsedPayload;
|
|
80
|
+
|
|
81
|
+
const candidateX = source.x ?? source.accelX ?? source.accelerationX ?? source.tiltX ?? source.pitch ?? source.alpha;
|
|
82
|
+
const candidateY = source.y ?? source.accelY ?? source.accelerationY ?? source.tiltY ?? source.roll ?? source.beta;
|
|
83
|
+
const candidateZ = source.z ?? source.accelZ ?? source.accelerationZ ?? source.tiltZ ?? source.yaw ?? source.gamma;
|
|
84
|
+
|
|
85
|
+
// Final fallback: if no known keys exist, use first 3 numeric values in the object.
|
|
86
|
+
const numericValues = Object.values(source).filter((v) => typeof v === 'number');
|
|
87
|
+
|
|
88
|
+
const x = candidateX ?? numericValues[0] ?? 0;
|
|
89
|
+
const y = candidateY ?? numericValues[1] ?? 0;
|
|
90
|
+
const z = candidateZ ?? numericValues[2] ?? 0;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
x: safeFixed(x),
|
|
94
|
+
y: safeFixed(y),
|
|
95
|
+
z: safeFixed(z)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default function Page() {
|
|
100
|
+
const [logs, setLogs] = useState([]);
|
|
101
|
+
const [deviceLogs, setDeviceLogs] = useState([]);
|
|
102
|
+
const [running, setRunning] = useState(false);
|
|
103
|
+
const [features, setFeatures] = useState(null);
|
|
104
|
+
const [cameraPreview, setCameraPreview] = useState(null);
|
|
105
|
+
const [lastAudioSize, setLastAudioSize] = useState(null);
|
|
106
|
+
const [relayConnected, setRelayConnected] = useState(false);
|
|
107
|
+
const [relayUrl, setRelayUrl] = useState(() => {
|
|
108
|
+
if (typeof window === 'undefined') return 'http://localhost:3031';
|
|
109
|
+
return `${window.location.protocol}//${window.location.hostname}:3031`;
|
|
110
|
+
});
|
|
111
|
+
const [aiPrompt, setAiPrompt] = useState('Generate an image of a futuristic rabbit mascot and return either an image URL or base64 image data.');
|
|
112
|
+
const [hardwareStats, setHardwareStats] = useState({
|
|
113
|
+
sideClick: 0,
|
|
114
|
+
longPressStart: 0,
|
|
115
|
+
longPressEnd: 0,
|
|
116
|
+
scrollUp: 0,
|
|
117
|
+
scrollDown: 0
|
|
118
|
+
});
|
|
119
|
+
const [accelData, setAccelData] = useState({ x: 0, y: 0, z: 0 });
|
|
120
|
+
const [accelActive, setAccelActive] = useState(false);
|
|
121
|
+
const [accelSamples, setAccelSamples] = useState(0);
|
|
122
|
+
const [accelStatus, setAccelStatus] = useState('idle');
|
|
123
|
+
const [accelRaw, setAccelRaw] = useState('n/a');
|
|
124
|
+
const [accelBaseline, setAccelBaseline] = useState({ x: 0, y: 0, z: 0 });
|
|
125
|
+
const [accelDeadzone, setAccelDeadzone] = useState(0.05);
|
|
126
|
+
const [accelScale, setAccelScale] = useState(1);
|
|
127
|
+
const [imageTransformPrompt, setImageTransformPrompt] = useState('Transform this image into a stylized AI art description and include key visual changes.');
|
|
128
|
+
const [imageQuestionPrompt, setImageQuestionPrompt] = useState('What do you see in this image?');
|
|
129
|
+
|
|
130
|
+
const sampleImageBase64 = useMemo(() => {
|
|
131
|
+
if (typeof window === 'undefined') return '';
|
|
132
|
+
return makeSampleBase64();
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const testContainerRef = useRef(null);
|
|
136
|
+
const transitionARef = useRef(null);
|
|
137
|
+
const transitionBRef = useRef(null);
|
|
138
|
+
const gyroXYRef = useRef(null);
|
|
139
|
+
const relaySocketRef = useRef(null);
|
|
140
|
+
const accelWatchdogRef = useRef(null);
|
|
141
|
+
const accelCalibrateTimerRef = useRef(null);
|
|
142
|
+
const accelCalibrateBufferRef = useRef([]);
|
|
143
|
+
const accelLatestRawRef = useRef({ x: 0, y: 0, z: 0 });
|
|
144
|
+
const accelRecentRawRef = useRef([]);
|
|
145
|
+
const accelSmoothRef = useRef({ x: 0, y: 0, z: 0 });
|
|
146
|
+
const accelScaleRef = useRef(1);
|
|
147
|
+
const accelBaselineRef = useRef({ x: 0, y: 0, z: 0 });
|
|
148
|
+
const accelDeadzoneRef = useRef(0.05);
|
|
149
|
+
|
|
150
|
+
const addLog = (status, title, detail) => {
|
|
151
|
+
setLogs((prev) => [{ status, title, detail, time: now() }, ...prev].slice(0, 300));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const addDeviceLog = (entry) => {
|
|
155
|
+
setDeviceLogs((prev) => [entry, ...prev].slice(0, 500));
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const onSideClick = () => setHardwareStats((prev) => ({ ...prev, sideClick: prev.sideClick + 1 }));
|
|
160
|
+
const onLongPressStart = () => setHardwareStats((prev) => ({ ...prev, longPressStart: prev.longPressStart + 1 }));
|
|
161
|
+
const onLongPressEnd = () => setHardwareStats((prev) => ({ ...prev, longPressEnd: prev.longPressEnd + 1 }));
|
|
162
|
+
const onScrollUp = () => setHardwareStats((prev) => ({ ...prev, scrollUp: prev.scrollUp + 1 }));
|
|
163
|
+
const onScrollDown = () => setHardwareStats((prev) => ({ ...prev, scrollDown: prev.scrollDown + 1 }));
|
|
164
|
+
|
|
165
|
+
r1.hardware.on('sideClick', onSideClick);
|
|
166
|
+
r1.hardware.on('longPressStart', onLongPressStart);
|
|
167
|
+
r1.hardware.on('longPressEnd', onLongPressEnd);
|
|
168
|
+
r1.hardware.on('scrollUp', onScrollUp);
|
|
169
|
+
r1.hardware.on('scrollDown', onScrollDown);
|
|
170
|
+
|
|
171
|
+
// Also subscribe directly to raw window events from device.
|
|
172
|
+
window.addEventListener('sideClick', onSideClick);
|
|
173
|
+
window.addEventListener('longPressStart', onLongPressStart);
|
|
174
|
+
window.addEventListener('longPressEnd', onLongPressEnd);
|
|
175
|
+
window.addEventListener('scrollUp', onScrollUp);
|
|
176
|
+
window.addEventListener('scrollDown', onScrollDown);
|
|
177
|
+
|
|
178
|
+
r1.deviceControls.init({ sideButtonEnabled: true, scrollWheelEnabled: true, keyboardFallback: true });
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
r1.hardware.off('sideClick', onSideClick);
|
|
182
|
+
r1.hardware.off('longPressStart', onLongPressStart);
|
|
183
|
+
r1.hardware.off('longPressEnd', onLongPressEnd);
|
|
184
|
+
r1.hardware.off('scrollUp', onScrollUp);
|
|
185
|
+
r1.hardware.off('scrollDown', onScrollDown);
|
|
186
|
+
window.removeEventListener('sideClick', onSideClick);
|
|
187
|
+
window.removeEventListener('longPressStart', onLongPressStart);
|
|
188
|
+
window.removeEventListener('longPressEnd', onLongPressEnd);
|
|
189
|
+
window.removeEventListener('scrollUp', onScrollUp);
|
|
190
|
+
window.removeEventListener('scrollDown', onScrollDown);
|
|
191
|
+
if (accelWatchdogRef.current) {
|
|
192
|
+
clearTimeout(accelWatchdogRef.current);
|
|
193
|
+
accelWatchdogRef.current = null;
|
|
194
|
+
}
|
|
195
|
+
if (accelCalibrateTimerRef.current) {
|
|
196
|
+
clearTimeout(accelCalibrateTimerRef.current);
|
|
197
|
+
accelCalibrateTimerRef.current = null;
|
|
198
|
+
}
|
|
199
|
+
r1.accelerometer.stop();
|
|
200
|
+
};
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
const connectRelay = () => {
|
|
204
|
+
if (relaySocketRef.current) {
|
|
205
|
+
relaySocketRef.current.disconnect();
|
|
206
|
+
relaySocketRef.current = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const normalizedUrl = relayUrl.trim();
|
|
210
|
+
const socket = io(normalizedUrl, {
|
|
211
|
+
transports: ['websocket', 'polling']
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
relaySocketRef.current = socket;
|
|
215
|
+
|
|
216
|
+
socket.on('connect', () => {
|
|
217
|
+
setRelayConnected(true);
|
|
218
|
+
socket.emit('register', { role: 'dashboard', deviceId: 'nextjs-test-app' });
|
|
219
|
+
addLog('pass', 'Relay', `Connected to ${normalizedUrl}`);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
socket.on('disconnect', (reason) => {
|
|
223
|
+
setRelayConnected(false);
|
|
224
|
+
addLog('skip', 'Relay', `Disconnected (${reason})`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
socket.on('connect_error', (error) => {
|
|
228
|
+
setRelayConnected(false);
|
|
229
|
+
addLog('fail', 'Relay', parseError(error));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
socket.on('relay_log', (payload) => {
|
|
233
|
+
const args = Array.isArray(payload?.args) ? payload.args.join(' ') : '';
|
|
234
|
+
addDeviceLog({
|
|
235
|
+
time: payload?.time ? String(payload.time).slice(11, 19) : now(),
|
|
236
|
+
level: payload?.level || 'log',
|
|
237
|
+
deviceId: payload?.deviceId || 'unknown',
|
|
238
|
+
message: args,
|
|
239
|
+
url: payload?.url || ''
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const disconnectRelay = () => {
|
|
245
|
+
if (relaySocketRef.current) {
|
|
246
|
+
relaySocketRef.current.disconnect();
|
|
247
|
+
relaySocketRef.current = null;
|
|
248
|
+
}
|
|
249
|
+
setRelayConnected(false);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const useCurrentRelayAddress = () => {
|
|
253
|
+
if (typeof window === 'undefined') return;
|
|
254
|
+
const current = `${window.location.protocol}//${window.location.hostname}:3031`;
|
|
255
|
+
setRelayUrl(current);
|
|
256
|
+
addLog('skip', 'Relay', `Relay URL set to ${current}`);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const startAccelerometerMonitor = async () => {
|
|
260
|
+
await assertPass('Hardware: start live accelerometer monitor', async () => {
|
|
261
|
+
r1.accelerometer.stop();
|
|
262
|
+
if (accelWatchdogRef.current) {
|
|
263
|
+
clearTimeout(accelWatchdogRef.current);
|
|
264
|
+
accelWatchdogRef.current = null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setAccelSamples(0);
|
|
268
|
+
setAccelStatus('starting');
|
|
269
|
+
|
|
270
|
+
const available = await r1.accelerometer.isAvailable();
|
|
271
|
+
if (!available) {
|
|
272
|
+
setAccelStatus('unavailable');
|
|
273
|
+
return 'accelerometer unavailable';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
r1.accelerometer.start((data) => {
|
|
277
|
+
setAccelSamples((prev) => prev + 1);
|
|
278
|
+
setAccelStatus('streaming');
|
|
279
|
+
try {
|
|
280
|
+
setAccelRaw(JSON.stringify(data));
|
|
281
|
+
} catch {
|
|
282
|
+
setAccelRaw(String(data));
|
|
283
|
+
}
|
|
284
|
+
const normalized = normalizeAccelData(data);
|
|
285
|
+
accelLatestRawRef.current = normalized;
|
|
286
|
+
|
|
287
|
+
if (accelStatus === 'calibrating') {
|
|
288
|
+
accelCalibrateBufferRef.current.push(normalized);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
accelRecentRawRef.current.push(normalized);
|
|
292
|
+
if (accelRecentRawRef.current.length > 30) {
|
|
293
|
+
accelRecentRawRef.current.shift();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const filtered = {
|
|
297
|
+
x: applyAccelFilter(normalized.x, accelBaselineRef.current.x, accelDeadzoneRef.current),
|
|
298
|
+
y: applyAccelFilter(normalized.y, accelBaselineRef.current.y, accelDeadzoneRef.current),
|
|
299
|
+
z: applyAccelFilter(normalized.z, accelBaselineRef.current.z, accelDeadzoneRef.current)
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Low-pass smoothing to damp micro-jitter while preserving motion.
|
|
303
|
+
const alpha = 0.25;
|
|
304
|
+
const prev = accelSmoothRef.current;
|
|
305
|
+
const smoothed = {
|
|
306
|
+
x: safeFixed(prev.x + alpha * (filtered.x - prev.x)),
|
|
307
|
+
y: safeFixed(prev.y + alpha * (filtered.y - prev.y)),
|
|
308
|
+
z: safeFixed(prev.z + alpha * (filtered.z - prev.z))
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const peak = Math.max(Math.abs(smoothed.x), Math.abs(smoothed.y), Math.abs(smoothed.z));
|
|
312
|
+
if (peak > accelScaleRef.current) {
|
|
313
|
+
const nextScale = Math.ceil(peak);
|
|
314
|
+
accelScaleRef.current = nextScale;
|
|
315
|
+
setAccelScale(nextScale);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
accelSmoothRef.current = smoothed;
|
|
319
|
+
setAccelData(smoothed);
|
|
320
|
+
}, { frequency: 30 });
|
|
321
|
+
|
|
322
|
+
accelWatchdogRef.current = setTimeout(() => {
|
|
323
|
+
setAccelStatus((current) => (current === 'streaming' ? current : 'no-samples-yet'));
|
|
324
|
+
}, 2500);
|
|
325
|
+
|
|
326
|
+
setAccelActive(true);
|
|
327
|
+
return 'monitoring via r1.accelerometer';
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const stopAccelerometerMonitor = () => {
|
|
332
|
+
r1.accelerometer.stop();
|
|
333
|
+
if (accelWatchdogRef.current) {
|
|
334
|
+
clearTimeout(accelWatchdogRef.current);
|
|
335
|
+
accelWatchdogRef.current = null;
|
|
336
|
+
}
|
|
337
|
+
if (accelCalibrateTimerRef.current) {
|
|
338
|
+
clearTimeout(accelCalibrateTimerRef.current);
|
|
339
|
+
accelCalibrateTimerRef.current = null;
|
|
340
|
+
}
|
|
341
|
+
setAccelActive(false);
|
|
342
|
+
setAccelStatus('stopped');
|
|
343
|
+
accelSmoothRef.current = { x: 0, y: 0, z: 0 };
|
|
344
|
+
accelCalibrateBufferRef.current = [];
|
|
345
|
+
accelRecentRawRef.current = [];
|
|
346
|
+
accelScaleRef.current = 1;
|
|
347
|
+
setAccelScale(1);
|
|
348
|
+
addLog('skip', 'Hardware: accelerometer monitor', 'stopped');
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const resetHardwareCounters = () => {
|
|
352
|
+
setHardwareStats({
|
|
353
|
+
sideClick: 0,
|
|
354
|
+
longPressStart: 0,
|
|
355
|
+
longPressEnd: 0,
|
|
356
|
+
scrollUp: 0,
|
|
357
|
+
scrollDown: 0
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const applyAccelFilter = (value, baseline, deadzone) => {
|
|
362
|
+
const delta = value - baseline;
|
|
363
|
+
if (Math.abs(delta) < deadzone) return 0;
|
|
364
|
+
return safeFixed(delta);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const calibrateGyroBaseline = () => {
|
|
368
|
+
if (!accelActive) {
|
|
369
|
+
addLog('skip', 'Hardware: gyro calibration', 'Start Gyro first, then keep device still and calibrate.');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (accelCalibrateTimerRef.current) {
|
|
374
|
+
clearTimeout(accelCalibrateTimerRef.current);
|
|
375
|
+
accelCalibrateTimerRef.current = null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
setAccelStatus('calibrating');
|
|
379
|
+
accelCalibrateBufferRef.current = [];
|
|
380
|
+
accelRecentRawRef.current = [];
|
|
381
|
+
|
|
382
|
+
accelCalibrateTimerRef.current = setTimeout(() => {
|
|
383
|
+
const samples = accelCalibrateBufferRef.current;
|
|
384
|
+
const baseline = samples.length
|
|
385
|
+
? {
|
|
386
|
+
x: safeFixed(samples.reduce((sum, s) => sum + s.x, 0) / samples.length),
|
|
387
|
+
y: safeFixed(samples.reduce((sum, s) => sum + s.y, 0) / samples.length),
|
|
388
|
+
z: safeFixed(samples.reduce((sum, s) => sum + s.z, 0) / samples.length)
|
|
389
|
+
}
|
|
390
|
+
: { ...accelLatestRawRef.current };
|
|
391
|
+
|
|
392
|
+
setAccelBaseline(baseline);
|
|
393
|
+
accelBaselineRef.current = baseline;
|
|
394
|
+
accelSmoothRef.current = { x: 0, y: 0, z: 0 };
|
|
395
|
+
setAccelData({ x: 0, y: 0, z: 0 });
|
|
396
|
+
accelScaleRef.current = 1;
|
|
397
|
+
setAccelScale(1);
|
|
398
|
+
setAccelStatus('streaming');
|
|
399
|
+
accelCalibrateBufferRef.current = [];
|
|
400
|
+
accelCalibrateTimerRef.current = null;
|
|
401
|
+
addLog('pass', 'Hardware: gyro calibration', `baseline(fresh ${samples.length || 1}) x=${baseline.x}, y=${baseline.y}, z=${baseline.z}`);
|
|
402
|
+
}, 900);
|
|
403
|
+
|
|
404
|
+
addLog('skip', 'Hardware: gyro calibration', 'Collecting fresh still samples for 0.9s... keep device still');
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const getAxisDotStyle = () => {
|
|
408
|
+
if (!accelActive) {
|
|
409
|
+
return { left: '50%', top: '50%' };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const size = gyroXYRef.current?.clientWidth || 140;
|
|
413
|
+
const half = size / 2;
|
|
414
|
+
const range = accelScale || 1;
|
|
415
|
+
const clamp = (v) => Math.max(-1, Math.min(1, v / range));
|
|
416
|
+
const x = clamp(accelData.x) * (half - 8);
|
|
417
|
+
const y = clamp(accelData.y) * (half - 8);
|
|
418
|
+
return {
|
|
419
|
+
left: `${half + x}px`,
|
|
420
|
+
top: `${half - y}px`
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const getRawAxisDotStyle = () => {
|
|
425
|
+
if (!accelActive) {
|
|
426
|
+
return { left: '50%', top: '50%' };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const size = gyroXYRef.current?.clientWidth || 140;
|
|
430
|
+
const half = size / 2;
|
|
431
|
+
const range = accelScale || 1;
|
|
432
|
+
const raw = accelLatestRawRef.current;
|
|
433
|
+
const dx = raw.x - accelBaseline.x;
|
|
434
|
+
const dy = raw.y - accelBaseline.y;
|
|
435
|
+
const clamp = (v) => Math.max(-1, Math.min(1, v / range));
|
|
436
|
+
const x = clamp(dx) * (half - 8);
|
|
437
|
+
const y = clamp(dy) * (half - 8);
|
|
438
|
+
return {
|
|
439
|
+
left: `${half + x}px`,
|
|
440
|
+
top: `${half - y}px`
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const getZFill = () => {
|
|
445
|
+
const range = accelScale || 1;
|
|
446
|
+
const normalized = Math.max(-1, Math.min(1, accelData.z / range));
|
|
447
|
+
return `${((normalized + 1) / 2) * 100}%`;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const assertPass = async (title, fn) => {
|
|
451
|
+
try {
|
|
452
|
+
const result = await fn();
|
|
453
|
+
addLog('pass', title, result ? String(result) : 'OK');
|
|
454
|
+
return true;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
addLog('fail', title, parseError(error));
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const assertSkip = (title, detail) => {
|
|
462
|
+
addLog('skip', title, detail);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const runCoreTests = async () => {
|
|
466
|
+
await assertPass('Core: initialize()', async () => {
|
|
467
|
+
await r1.initialize();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
await assertPass('Core: getAvailableFeatures()', async () => {
|
|
471
|
+
const available = await r1.getAvailableFeatures();
|
|
472
|
+
setFeatures(available);
|
|
473
|
+
return JSON.stringify(available);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await assertPass('Core: dimensions constant', async () => {
|
|
477
|
+
if (R1_DIMENSIONS.width !== 240 || R1_DIMENSIONS.height !== 282) {
|
|
478
|
+
throw new Error('Unexpected dimensions');
|
|
479
|
+
}
|
|
480
|
+
return `${R1_DIMENSIONS.width}x${R1_DIMENSIONS.height}`;
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const runHardwareTests = async () => {
|
|
485
|
+
await assertPass('Hardware: accelerometer.isAvailable()', async () => {
|
|
486
|
+
return String(await r1.accelerometer.isAvailable());
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await assertPass('Hardware: accelerometer start/stop state', async () => {
|
|
490
|
+
if (await r1.accelerometer.isAvailable()) {
|
|
491
|
+
r1.accelerometer.start(() => undefined, { frequency: 20 });
|
|
492
|
+
const active = r1.accelerometer.isActive();
|
|
493
|
+
r1.accelerometer.stop();
|
|
494
|
+
if (!active) throw new Error('Expected accelerometer active after start');
|
|
495
|
+
return 'started/stopped';
|
|
496
|
+
}
|
|
497
|
+
return 'not available';
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await assertPass('Hardware: touch synthetic events', async () => {
|
|
501
|
+
r1.touch.tap(10, 10);
|
|
502
|
+
r1.touch.touchDown(10, 11);
|
|
503
|
+
r1.touch.touchMove(11, 12);
|
|
504
|
+
r1.touch.touchUp(11, 12);
|
|
505
|
+
r1.touch.touchCancel(11, 12);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await assertPass('Hardware: event on/off', async () => {
|
|
509
|
+
const cb = () => undefined;
|
|
510
|
+
r1.hardware.on('sideClick', cb);
|
|
511
|
+
r1.hardware.off('sideClick', cb);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
assertSkip('Hardware: removeAllListeners', 'Not run in full smoke test to preserve live hardware diagnostics listeners');
|
|
515
|
+
|
|
516
|
+
await assertPass('DeviceControls: init/on/off/toggle/listeners', async () => {
|
|
517
|
+
r1.deviceControls.init({ sideButtonEnabled: true, scrollWheelEnabled: true, keyboardFallback: true });
|
|
518
|
+
const sideCb = () => undefined;
|
|
519
|
+
const wheelCb = () => undefined;
|
|
520
|
+
r1.deviceControls.on('sideButton', sideCb);
|
|
521
|
+
r1.deviceControls.on('scrollWheel', wheelCb);
|
|
522
|
+
r1.deviceControls.setSideButtonEnabled(true);
|
|
523
|
+
r1.deviceControls.setScrollWheelEnabled(true);
|
|
524
|
+
r1.deviceControls.triggerSideButton();
|
|
525
|
+
const listeners = r1.deviceControls.getEventListeners();
|
|
526
|
+
r1.deviceControls.off('sideButton', sideCb);
|
|
527
|
+
r1.deviceControls.off('scrollWheel', wheelCb);
|
|
528
|
+
return JSON.stringify(listeners);
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const runStorageTests = async () => {
|
|
533
|
+
await assertPass('Storage: Base64Utils encode/decode/safeDecode', async () => {
|
|
534
|
+
const encoded = Base64Utils.encode({ hello: 'world' });
|
|
535
|
+
const decoded = Base64Utils.decode(encoded);
|
|
536
|
+
const safe = Base64Utils.safeDecode(encoded);
|
|
537
|
+
if (!decoded.hello || !safe.hello) throw new Error('Decode mismatch');
|
|
538
|
+
return encoded.slice(0, 12) + '...';
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
await assertPass('Storage: R1Storage static checks', async () => {
|
|
542
|
+
return JSON.stringify({
|
|
543
|
+
available: R1Storage.isAvailable(),
|
|
544
|
+
secureAvailable: R1Storage.isSecureAvailable()
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
if (!R1Storage.isAvailable()) {
|
|
549
|
+
assertSkip('Storage: plain read/write', 'creationStorage unavailable in this environment');
|
|
550
|
+
assertSkip('Storage: preferences helpers', 'creationStorage unavailable in this environment');
|
|
551
|
+
assertSkip('Storage: secret helpers', 'secure storage unavailable in this environment');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
await assertPass('Storage: plain set/get/remove', async () => {
|
|
556
|
+
await r1.storage.plain.setItem('sdk_test_plain', { n: 1, t: Date.now() });
|
|
557
|
+
const value = await r1.storage.plain.getItem('sdk_test_plain');
|
|
558
|
+
await r1.storage.plain.removeItem('sdk_test_plain');
|
|
559
|
+
if (!value || value.n !== 1) throw new Error('Plain storage value mismatch');
|
|
560
|
+
return JSON.stringify(value);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await assertPass('Storage: plain setRaw/getRaw', async () => {
|
|
564
|
+
const encoded = Base64Utils.encode({ raw: true, ts: Date.now() });
|
|
565
|
+
await r1.storage.plain.setRaw('sdk_test_raw', encoded);
|
|
566
|
+
const raw = await r1.storage.plain.getRaw('sdk_test_raw');
|
|
567
|
+
await r1.storage.plain.removeItem('sdk_test_raw');
|
|
568
|
+
if (raw !== encoded) throw new Error('Raw storage mismatch');
|
|
569
|
+
return raw.slice(0, 12) + '...';
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await assertPass('Storage: set/get preferences', async () => {
|
|
573
|
+
await r1.storage.setPreferences({ mode: 'test', ts: Date.now() });
|
|
574
|
+
const prefs = await r1.storage.getPreferences();
|
|
575
|
+
if (!prefs) throw new Error('Preferences missing');
|
|
576
|
+
return JSON.stringify(prefs);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
if (!R1Storage.isSecureAvailable()) {
|
|
580
|
+
assertSkip('Storage: secure set/get secret', 'secure storage unavailable');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await assertPass('Storage: set/get secret', async () => {
|
|
585
|
+
await r1.storage.setSecret('token', 'abc123');
|
|
586
|
+
const token = await r1.storage.getSecret('token');
|
|
587
|
+
if (token !== 'abc123') throw new Error('Secret mismatch');
|
|
588
|
+
return token;
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const runMessagingTests = async () => {
|
|
593
|
+
await assertPass('Messaging: runtime capabilities', async () => {
|
|
594
|
+
return JSON.stringify(r1.messaging.getRuntimeCapabilities());
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await assertPass('Messaging: onMessage/offMessage', async () => {
|
|
598
|
+
const cb = () => undefined;
|
|
599
|
+
r1.messaging.onMessage(cb);
|
|
600
|
+
r1.messaging.offMessage(cb);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await assertPass('Messaging: removeAllHandlers', async () => {
|
|
604
|
+
const cb1 = () => undefined;
|
|
605
|
+
const cb2 = () => undefined;
|
|
606
|
+
r1.messaging.onMessage(cb1);
|
|
607
|
+
r1.messaging.onMessage(cb2);
|
|
608
|
+
r1.messaging.removeAllHandlers();
|
|
609
|
+
r1.messaging.onMessage(() => undefined);
|
|
610
|
+
return 'handlers reset';
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await assertPass('Messaging: STT state and toggles', async () => {
|
|
614
|
+
const caps = r1.messaging.getRuntimeCapabilities();
|
|
615
|
+
const before = r1.messaging.isSTTListening();
|
|
616
|
+
if (caps.creationVoiceHandler) {
|
|
617
|
+
r1.messaging.startSTTListening();
|
|
618
|
+
r1.messaging.stopSTTListening();
|
|
619
|
+
}
|
|
620
|
+
return `before=${before} after=${r1.messaging.isSTTListening()}`;
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
await assertPass('Messaging: enable/disable push-to-talk', async () => {
|
|
624
|
+
const caps = r1.messaging.getRuntimeCapabilities();
|
|
625
|
+
if (!caps.creationVoiceHandler) return 'voice bridge unavailable';
|
|
626
|
+
const disable = r1.messaging.enablePushToTalk({
|
|
627
|
+
onTranscript: () => undefined
|
|
628
|
+
});
|
|
629
|
+
disable();
|
|
630
|
+
r1.messaging.disablePushToTalk();
|
|
631
|
+
return 'enabled/disabled';
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
await assertPass('Messaging: STT transcript path simulation', async () => {
|
|
635
|
+
let observed = '';
|
|
636
|
+
const disable = r1.messaging.enablePushToTalk({
|
|
637
|
+
autoForwardTranscript: false,
|
|
638
|
+
onTranscript: (text) => {
|
|
639
|
+
observed = text;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
if (typeof window.onPluginMessage === 'function') {
|
|
644
|
+
window.onPluginMessage({
|
|
645
|
+
message: 'stt payload',
|
|
646
|
+
pluginId: 'sdk-test',
|
|
647
|
+
type: 'sttEnded',
|
|
648
|
+
transcript: 'hello from simulated stt'
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
disable();
|
|
653
|
+
|
|
654
|
+
if (observed !== 'hello from simulated stt') {
|
|
655
|
+
throw new Error('Did not observe simulated STT transcript');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return observed;
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const caps = r1.messaging.getRuntimeCapabilities();
|
|
662
|
+
if (!caps.pluginMessageHandler) {
|
|
663
|
+
assertSkip('Messaging: sendMessage/askLLM/search/email/etc', 'PluginMessageHandler unavailable');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
await assertPass('Messaging: sendMessage', async () => {
|
|
668
|
+
await r1.messaging.sendMessage('SDK smoke test ping', { useLLM: false });
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
await assertPass('Messaging: askLLM', async () => {
|
|
672
|
+
await r1.messaging.askLLM('Respond with OK only.');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
await assertPass('Messaging: askLLMJSON', async () => {
|
|
676
|
+
await r1.messaging.askLLMJSON('Return JSON: {"ok": true}');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await assertPass('Messaging: askLLMSpeak', async () => {
|
|
680
|
+
await r1.messaging.askLLMSpeak('Say: smoke test for askLLMSpeak', false);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await assertPass('Messaging: speakText', async () => {
|
|
684
|
+
await r1.messaging.speakText('This is a smoke test message.');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await assertPass('Messaging: searchWeb advanced options', async () => {
|
|
688
|
+
await r1.messaging.searchWeb('weather in Berlin', { tag: 'weather', useLocation: false });
|
|
689
|
+
await r1.messaging.searchWeb('rabbit logo', { tag: 'image', useLocation: false });
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
await assertPass('Messaging: searchWeb all SERP tags', async () => {
|
|
693
|
+
const tags = ['search', 'image', 'finance', 'jobs', 'weather', 'hotels'];
|
|
694
|
+
for (const tag of tags) {
|
|
695
|
+
await r1.messaging.searchWeb(`sdk smoke ${tag}`, { tag, useLocation: false });
|
|
696
|
+
}
|
|
697
|
+
return tags.join(',');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
await assertPass('Messaging: emailUser helper', async () => {
|
|
701
|
+
await r1.messaging.emailUser('Smoke test content from Next.js app');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
await assertPass('Messaging: waitForNextMessage timeout behavior', async () => {
|
|
705
|
+
try {
|
|
706
|
+
await r1.messaging.waitForNextMessage({ timeoutMs: 25 });
|
|
707
|
+
return 'resolved';
|
|
708
|
+
} catch {
|
|
709
|
+
return 'timeout expected';
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
await assertPass('Messaging: askLLMWithTimeout', async () => {
|
|
714
|
+
try {
|
|
715
|
+
const res = await r1.messaging.askLLMWithTimeout(
|
|
716
|
+
'Respond with short text only',
|
|
717
|
+
{ wantsR1Response: false },
|
|
718
|
+
{ timeoutMs: 3000 }
|
|
719
|
+
);
|
|
720
|
+
return res.message || 'resolved';
|
|
721
|
+
} catch {
|
|
722
|
+
return 'timed out or no response (environment-dependent)';
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await assertPass('Messaging: llm helper analyzeData', async () => {
|
|
727
|
+
await r1.llm.analyzeData('Analyze this object', { source: 'smoke-test', ok: true });
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
await assertPass('Messaging: llm helper getUserMemories', async () => {
|
|
731
|
+
await r1.llm.getUserMemories();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
await assertPass('Messaging: llm helper analyzeImageBase64', async () => {
|
|
735
|
+
await r1.llm.analyzeImageBase64('Describe this image', sampleImageBase64);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
await assertPass('Messaging: llm helper getUISuggestions', async () => {
|
|
739
|
+
await r1.llm.getUISuggestions('A list with selected item and action button');
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
await assertPass('Messaging: llm helper performTask', async () => {
|
|
743
|
+
await r1.llm.performTask('Say: smoke test performTask', false);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
await assertPass('Messaging: llm helper textToSpeech', async () => {
|
|
747
|
+
await r1.llm.textToSpeech('Smoke test textToSpeech', false);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
await assertPass('Messaging: llm helper textToSpeechAudio', async () => {
|
|
751
|
+
try {
|
|
752
|
+
const result = await r1.llm.textToSpeechAudio('Smoke test audio generation', { rate: 1 });
|
|
753
|
+
return result === null ? 'returned null (expected for current Web Speech limitation)' : 'blob returned';
|
|
754
|
+
} catch {
|
|
755
|
+
return 'not supported in this environment';
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
await assertPass('Messaging: AI action-calling JSON test', async () => {
|
|
760
|
+
await r1.messaging.askLLMJSON(
|
|
761
|
+
'Return JSON in this schema: {"action":"create_note","args":{"title":"string","body":"string"}}'
|
|
762
|
+
);
|
|
763
|
+
return 'sent';
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
await assertPass('Messaging: AI image-generation prompt test', async () => {
|
|
767
|
+
await r1.messaging.askLLM(aiPrompt, { wantsR1Response: false, wantsJournalEntry: false });
|
|
768
|
+
return 'prompt sent';
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const runUITests = async () => {
|
|
773
|
+
await assertPass('UI: LayoutUtils basic methods', async () => {
|
|
774
|
+
const inside = LayoutUtils.isWithinBounds(10, 10);
|
|
775
|
+
const clamped = LayoutUtils.clampToBounds(-1, 999);
|
|
776
|
+
const font = LayoutUtils.calculateFontSize(120);
|
|
777
|
+
const css = LayoutUtils.createR1Container();
|
|
778
|
+
if (!inside || clamped.x !== 0 || clamped.y !== 282 || !css.includes('240px')) {
|
|
779
|
+
throw new Error('Layout utils mismatch');
|
|
780
|
+
}
|
|
781
|
+
return `font=${font}`;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
await assertPass('UI: DOMUtils methods', async () => {
|
|
785
|
+
const host = document.createElement('div');
|
|
786
|
+
const el = DOMUtils.createElement('div', { 'data-test': 'x' }, 'abc');
|
|
787
|
+
DOMUtils.batchOperations((frag) => {
|
|
788
|
+
frag.appendChild(el);
|
|
789
|
+
}, host);
|
|
790
|
+
DOMUtils.updateContent(el, 'xyz');
|
|
791
|
+
DOMUtils.toggleClass(el, 'active', true);
|
|
792
|
+
const debounced = DOMUtils.debounce(() => undefined, 5);
|
|
793
|
+
debounced();
|
|
794
|
+
return `children=${host.children.length}`;
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
await assertPass('UI: CSSUtils methods', async () => {
|
|
798
|
+
const el = document.createElement('div');
|
|
799
|
+
CSSUtils.setTransform(el, 'translateX(2px)');
|
|
800
|
+
CSSUtils.setOpacity(el, 0.9);
|
|
801
|
+
CSSUtils.addTransition(el, 'transform', 100);
|
|
802
|
+
CSSUtils.createAnimation('sdkSmokePulse', '0%{opacity:.5;}100%{opacity:1;}', 120);
|
|
803
|
+
CSSUtils.resetWillChange(el);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
await assertPass('UI: R1UI methods and transitions', async () => {
|
|
807
|
+
const host = testContainerRef.current;
|
|
808
|
+
const a = transitionARef.current;
|
|
809
|
+
const b = transitionBRef.current;
|
|
810
|
+
if (!host || !a || !b) throw new Error('UI refs missing');
|
|
811
|
+
|
|
812
|
+
r1.ui.setupViewport();
|
|
813
|
+
r1.ui.createContainer(host, { background: '#101010' });
|
|
814
|
+
r1.ui.createText(a, { size: 'small', color: '#ffffff', align: 'left' });
|
|
815
|
+
r1.ui.createText(b, { size: 'small', color: '#ffffff', align: 'left' });
|
|
816
|
+
|
|
817
|
+
const btn = document.createElement('button');
|
|
818
|
+
btn.textContent = 'x';
|
|
819
|
+
r1.ui.createButton(btn, { type: 'small' });
|
|
820
|
+
|
|
821
|
+
const grid = document.createElement('div');
|
|
822
|
+
r1.ui.createGrid(grid, { columns: 2 });
|
|
823
|
+
host.appendChild(grid);
|
|
824
|
+
grid.appendChild(btn);
|
|
825
|
+
|
|
826
|
+
const colors = r1.ui.getColors();
|
|
827
|
+
const sizes = r1.ui.getFontSizes();
|
|
828
|
+
const spacing = r1.ui.getSpacing();
|
|
829
|
+
const buttons = r1.ui.getButtonSizes();
|
|
830
|
+
const vw = r1.ui.pxToVw(24);
|
|
831
|
+
if (!colors.primary || !sizes.body || !spacing.md || !buttons.wide || !vw.includes('vw')) {
|
|
832
|
+
throw new Error('R1UI getter mismatch');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
r1.ui.transition(a, b, 'fade', 120);
|
|
836
|
+
return `dims=${r1.ui.dimensions.width}x${r1.ui.dimensions.height}`;
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
await assertPass('UI: PerformanceUtils start/end', async () => {
|
|
840
|
+
PerformanceUtils.startMeasure('sdk-ui-measure');
|
|
841
|
+
for (let i = 0; i < 2000; i++) {
|
|
842
|
+
Math.sqrt(i);
|
|
843
|
+
}
|
|
844
|
+
const duration = PerformanceUtils.endMeasure('sdk-ui-measure', false);
|
|
845
|
+
return `duration=${duration.toFixed(2)}ms`;
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
await assertPass('UI: PerformanceUtils monitorFPS', async () => {
|
|
849
|
+
await new Promise((resolve) => {
|
|
850
|
+
PerformanceUtils.monitorFPS(0.2, () => resolve());
|
|
851
|
+
});
|
|
852
|
+
return 'fps callback received';
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
await assertPass('UI: R1Component mount lifecycle', async () => {
|
|
856
|
+
class SmokeComponent extends R1Component {
|
|
857
|
+
onMount() {
|
|
858
|
+
this.getElement().textContent = 'mounted';
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
onUnmount() {
|
|
862
|
+
this.getElement().textContent = 'unmounted';
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const host = document.createElement('div');
|
|
867
|
+
const comp = new SmokeComponent('div', 'smoke-component');
|
|
868
|
+
comp.mount(host);
|
|
869
|
+
const mounted = comp.isMounted();
|
|
870
|
+
const node = comp.getElement();
|
|
871
|
+
comp.unmount();
|
|
872
|
+
if (!mounted || node.textContent !== 'unmounted') {
|
|
873
|
+
throw new Error('component lifecycle failed');
|
|
874
|
+
}
|
|
875
|
+
return 'mounted/unmounted';
|
|
876
|
+
});
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
const runMediaUtilsTests = async () => {
|
|
880
|
+
await assertPass('MediaUtils: getDevices', async () => {
|
|
881
|
+
const devices = await MediaUtils.getDevices();
|
|
882
|
+
return `devices=${devices.length}`;
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
await assertPass('MediaUtils: isSupported all', async () => {
|
|
886
|
+
const camera = await MediaUtils.isSupported('camera');
|
|
887
|
+
const microphone = await MediaUtils.isSupported('microphone');
|
|
888
|
+
const speaker = await MediaUtils.isSupported('speaker');
|
|
889
|
+
return JSON.stringify({ camera, microphone, speaker });
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
await assertPass('MediaUtils: base64<->blob conversion', async () => {
|
|
893
|
+
const blob = new Blob(['hello'], { type: 'text/plain' });
|
|
894
|
+
const b64 = await MediaUtils.blobToBase64(blob);
|
|
895
|
+
const roundtrip = MediaUtils.base64ToBlob(b64, 'text/plain');
|
|
896
|
+
if (!b64 || roundtrip.size === 0) throw new Error('conversion failed');
|
|
897
|
+
return `size=${roundtrip.size}`;
|
|
898
|
+
});
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const runMediaApiStateTests = async () => {
|
|
902
|
+
await assertPass('Media: camera.getStream initial', async () => {
|
|
903
|
+
const stream = r1.camera.getStream();
|
|
904
|
+
return stream ? 'has stream' : 'null stream';
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
await assertPass('Media: microphone state/getStream initial', async () => {
|
|
908
|
+
const state = r1.microphone.getRecordingState();
|
|
909
|
+
const stream = r1.microphone.getStream();
|
|
910
|
+
return `state=${state} stream=${stream ? 'set' : 'null'}`;
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
await assertPass('Media: speaker isPlaying initial', async () => {
|
|
914
|
+
return `isPlaying=${r1.speaker.isPlaying()}`;
|
|
915
|
+
});
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const runAllTests = async () => {
|
|
919
|
+
setRunning(true);
|
|
920
|
+
setLogs([]);
|
|
921
|
+
|
|
922
|
+
addLog('skip', 'Start', 'Running complete SDK smoke test suite');
|
|
923
|
+
|
|
924
|
+
await runCoreTests();
|
|
925
|
+
await runHardwareTests();
|
|
926
|
+
await runStorageTests();
|
|
927
|
+
await runMessagingTests();
|
|
928
|
+
await runUITests();
|
|
929
|
+
await runMediaUtilsTests();
|
|
930
|
+
await runMediaApiStateTests();
|
|
931
|
+
|
|
932
|
+
addLog('skip', 'Done', 'Smoke test run complete. Use manual tests below for camera/mic/speaker and closePlugin.');
|
|
933
|
+
setRunning(false);
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
const runCameraManualTest = async () => {
|
|
937
|
+
await assertPass('Media: camera full flow', async () => {
|
|
938
|
+
const available = await r1.camera.isAvailable();
|
|
939
|
+
if (!available) return 'camera unavailable';
|
|
940
|
+
|
|
941
|
+
const stream = await r1.camera.start({ width: 240, height: 282, facingMode: 'user' });
|
|
942
|
+
if (!stream) throw new Error('no camera stream');
|
|
943
|
+
|
|
944
|
+
const video = r1.camera.createVideoElement(true, true);
|
|
945
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
946
|
+
const shot = r1.camera.capturePhoto(120, 141);
|
|
947
|
+
setCameraPreview(shot);
|
|
948
|
+
await r1.camera.switchCamera().catch(() => undefined);
|
|
949
|
+
r1.camera.stop();
|
|
950
|
+
return 'camera started/captured/stopped';
|
|
951
|
+
});
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const getCapturedImagePayload = () => {
|
|
955
|
+
if (!cameraPreview || typeof cameraPreview !== 'string') {
|
|
956
|
+
throw new Error('No captured image available. Run Manual Camera Test first.');
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (!cameraPreview.startsWith('data:image/')) {
|
|
960
|
+
throw new Error('Captured image is not a valid image data URL.');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return cameraPreview;
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const runImageToAIManualTest = async () => {
|
|
967
|
+
await assertPass('Media+LLM: transform image (take a photo and make it ...)', async () => {
|
|
968
|
+
const imagePayload = getCapturedImagePayload();
|
|
969
|
+
|
|
970
|
+
await r1.llm.transformPhoto(imagePayload, imageTransformPrompt, {
|
|
971
|
+
wantsR1Response: false,
|
|
972
|
+
wantsJournalEntry: false
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
return 'sent captured image via transformPhoto';
|
|
976
|
+
});
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const runAskImageManualTest = async () => {
|
|
980
|
+
await assertPass('Media+LLM: ask about image (image + prompt)', async () => {
|
|
981
|
+
const imagePayload = getCapturedImagePayload();
|
|
982
|
+
|
|
983
|
+
await r1.llm.askImage(imagePayload, imageQuestionPrompt, {
|
|
984
|
+
wantsR1Response: false,
|
|
985
|
+
wantsJournalEntry: false
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
return 'sent captured image via askImage';
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const runImageNoPromptManualTest = async () => {
|
|
993
|
+
await assertPass('Media+LLM: image with no prompt', async () => {
|
|
994
|
+
const imagePayload = getCapturedImagePayload();
|
|
995
|
+
|
|
996
|
+
await r1.llm.imageToAI(imagePayload, '', {
|
|
997
|
+
wantsR1Response: false,
|
|
998
|
+
wantsJournalEntry: false
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
return 'sent captured image via imageToAI (no prompt)';
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const runMicrophoneManualTest = async () => {
|
|
1006
|
+
await assertPass('Media: microphone record flow', async () => {
|
|
1007
|
+
const available = await r1.microphone.isAvailable();
|
|
1008
|
+
if (!available) return 'microphone unavailable';
|
|
1009
|
+
|
|
1010
|
+
await r1.microphone.startRecording({ mimeType: 'audio/webm', audioBitsPerSecond: 64000 });
|
|
1011
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
1012
|
+
const audio = await r1.microphone.stopRecording();
|
|
1013
|
+
setLastAudioSize(audio.size);
|
|
1014
|
+
r1.microphone.stop();
|
|
1015
|
+
return `recorded bytes=${audio.size}`;
|
|
1016
|
+
});
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const runSpeakerManualTest = async () => {
|
|
1020
|
+
await assertPass('Media: speaker tone + state', async () => {
|
|
1021
|
+
await r1.speaker.playTone(440, 200, 0.2);
|
|
1022
|
+
const isPlaying = r1.speaker.isPlaying();
|
|
1023
|
+
r1.speaker.setVolume(0.5);
|
|
1024
|
+
r1.speaker.stop();
|
|
1025
|
+
return `isPlaying=${isPlaying}`;
|
|
1026
|
+
});
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const runClosePluginManual = () => {
|
|
1030
|
+
addLog('skip', 'Messaging: closePlugin', 'Attempting to close webview (if running in R1 this may close app).');
|
|
1031
|
+
try {
|
|
1032
|
+
r1.messaging.closePlugin();
|
|
1033
|
+
addLog('pass', 'Messaging: closePlugin', 'Called');
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
addLog('fail', 'Messaging: closePlugin', parseError(error));
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
const runSimulatedSTTEvent = () => {
|
|
1040
|
+
addLog('skip', 'Manual STT simulation', 'Dispatching a simulated sttEnded payload through onPluginMessage');
|
|
1041
|
+
try {
|
|
1042
|
+
const disable = r1.messaging.enablePushToTalk({
|
|
1043
|
+
autoForwardTranscript: false,
|
|
1044
|
+
onTranscript: (text) => addLog('pass', 'Manual STT transcript callback', text)
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
if (typeof window.onPluginMessage === 'function') {
|
|
1048
|
+
window.onPluginMessage({
|
|
1049
|
+
message: 'manual simulation',
|
|
1050
|
+
pluginId: 'manual',
|
|
1051
|
+
type: 'sttEnded',
|
|
1052
|
+
transcript: 'manual simulated transcript'
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
disable();
|
|
1057
|
+
addLog('pass', 'Manual STT simulation', 'Simulation dispatched');
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
addLog('fail', 'Manual STT simulation', parseError(error));
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const runAIGeneratedImagePromptManual = async () => {
|
|
1064
|
+
await assertPass('Manual AI generated image prompt', async () => {
|
|
1065
|
+
await r1.messaging.askLLM(aiPrompt, {
|
|
1066
|
+
wantsR1Response: false,
|
|
1067
|
+
wantsJournalEntry: false
|
|
1068
|
+
});
|
|
1069
|
+
return aiPrompt;
|
|
1070
|
+
});
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const runAICallingManual = async () => {
|
|
1074
|
+
await assertPass('Manual AI calling-style JSON prompt', async () => {
|
|
1075
|
+
await r1.messaging.askLLMJSON(
|
|
1076
|
+
'Return ONLY JSON: {"tool":"send_email","args":{"subject":"Smoke Test","body":"Hello from function-style test"}}'
|
|
1077
|
+
);
|
|
1078
|
+
return 'sent';
|
|
1079
|
+
});
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
return (
|
|
1083
|
+
<main className="stack">
|
|
1084
|
+
<div className="panel stack">
|
|
1085
|
+
<h1>R1 Create Full Test App (Next.js)</h1>
|
|
1086
|
+
<p>
|
|
1087
|
+
This test harness covers core, hardware, storage, messaging/LLM, UI, and media APIs. Some features
|
|
1088
|
+
require R1 runtime bridges or user permissions.
|
|
1089
|
+
</p>
|
|
1090
|
+
</div>
|
|
1091
|
+
|
|
1092
|
+
<div className="grid">
|
|
1093
|
+
<div className="panel stack">
|
|
1094
|
+
<button className="primary" onClick={runAllTests} disabled={running}>
|
|
1095
|
+
{running ? 'Running Full Suite...' : 'Run Full SDK Smoke Test'}
|
|
1096
|
+
</button>
|
|
1097
|
+
<button onClick={runCameraManualTest}>Manual Camera Test</button>
|
|
1098
|
+
<button onClick={runImageToAIManualTest}>Manual Image Transform Test</button>
|
|
1099
|
+
<button onClick={runAskImageManualTest}>Manual Ask About Image</button>
|
|
1100
|
+
<button onClick={runImageNoPromptManualTest}>Manual Image No Prompt</button>
|
|
1101
|
+
<button onClick={runMicrophoneManualTest}>Manual Microphone Test</button>
|
|
1102
|
+
<button onClick={runSpeakerManualTest}>Manual Speaker Test</button>
|
|
1103
|
+
<button onClick={runSimulatedSTTEvent}>Manual STT Simulation</button>
|
|
1104
|
+
<button onClick={runAIGeneratedImagePromptManual}>Manual AI Image Generation Prompt</button>
|
|
1105
|
+
<button onClick={runAICallingManual}>Manual AI Calling JSON Prompt</button>
|
|
1106
|
+
<button className="warn" onClick={runClosePluginManual}>Manual closePlugin Test</button>
|
|
1107
|
+
<button onClick={() => setLogs([])}>Clear Logs</button>
|
|
1108
|
+
<textarea
|
|
1109
|
+
className="relay-input code"
|
|
1110
|
+
rows={3}
|
|
1111
|
+
value={aiPrompt}
|
|
1112
|
+
onChange={(e) => setAiPrompt(e.target.value)}
|
|
1113
|
+
/>
|
|
1114
|
+
<textarea
|
|
1115
|
+
className="relay-input code"
|
|
1116
|
+
rows={3}
|
|
1117
|
+
value={imageTransformPrompt}
|
|
1118
|
+
onChange={(e) => setImageTransformPrompt(e.target.value)}
|
|
1119
|
+
/>
|
|
1120
|
+
<textarea
|
|
1121
|
+
className="relay-input code"
|
|
1122
|
+
rows={2}
|
|
1123
|
+
value={imageQuestionPrompt}
|
|
1124
|
+
onChange={(e) => setImageQuestionPrompt(e.target.value)}
|
|
1125
|
+
/>
|
|
1126
|
+
<div className="small code">
|
|
1127
|
+
Features: {features ? JSON.stringify(features) : 'Not loaded yet'}
|
|
1128
|
+
</div>
|
|
1129
|
+
<div className="small code">
|
|
1130
|
+
Last audio blob size: {lastAudioSize == null ? 'n/a' : String(lastAudioSize)}
|
|
1131
|
+
</div>
|
|
1132
|
+
|
|
1133
|
+
<div className="small" style={{ marginTop: 10 }}>Socket relay</div>
|
|
1134
|
+
<input
|
|
1135
|
+
className="relay-input"
|
|
1136
|
+
value={relayUrl}
|
|
1137
|
+
onChange={(e) => setRelayUrl(e.target.value)}
|
|
1138
|
+
placeholder="http(s)://current-host:3031"
|
|
1139
|
+
/>
|
|
1140
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
|
1141
|
+
<button onClick={connectRelay} disabled={relayConnected}>Connect Relay</button>
|
|
1142
|
+
<button onClick={disconnectRelay} disabled={!relayConnected}>Disconnect</button>
|
|
1143
|
+
</div>
|
|
1144
|
+
<button onClick={useCurrentRelayAddress}>Use Current Address</button>
|
|
1145
|
+
<div className="small code">
|
|
1146
|
+
Relay status: {relayConnected ? 'connected' : 'disconnected'}
|
|
1147
|
+
</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
|
|
1150
|
+
<div className="panel stack">
|
|
1151
|
+
<div className="small">Camera capture preview</div>
|
|
1152
|
+
{cameraPreview ? (
|
|
1153
|
+
<img className="preview" src={cameraPreview} alt="Captured" />
|
|
1154
|
+
) : (
|
|
1155
|
+
<div className="preview small" style={{ display: 'grid', placeItems: 'center' }}>
|
|
1156
|
+
No capture yet
|
|
1157
|
+
</div>
|
|
1158
|
+
)}
|
|
1159
|
+
|
|
1160
|
+
<div className="small">UI transition sandbox</div>
|
|
1161
|
+
<div ref={testContainerRef} style={{ border: '1px solid #2a2a2a', borderRadius: 8, minHeight: 96, padding: 8 }}>
|
|
1162
|
+
<div ref={transitionARef}>Panel A</div>
|
|
1163
|
+
<div ref={transitionBRef} style={{ display: 'none' }}>Panel B</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
|
|
1166
|
+
<div className="small">Hardware live status (gyro/PTT/scroll)</div>
|
|
1167
|
+
<div className="small code">
|
|
1168
|
+
sideClick={hardwareStats.sideClick} | longStart={hardwareStats.longPressStart} | longEnd={hardwareStats.longPressEnd}
|
|
1169
|
+
</div>
|
|
1170
|
+
<div className="small code">
|
|
1171
|
+
scrollUp={hardwareStats.scrollUp} | scrollDown={hardwareStats.scrollDown}
|
|
1172
|
+
</div>
|
|
1173
|
+
<div className="small code">
|
|
1174
|
+
accelActive={String(accelActive)} x={accelData.x} y={accelData.y} z={accelData.z}
|
|
1175
|
+
</div>
|
|
1176
|
+
<div className="small code">
|
|
1177
|
+
accelStatus={accelStatus} samples={accelSamples}
|
|
1178
|
+
</div>
|
|
1179
|
+
<div className="small code">
|
|
1180
|
+
baseline x={accelBaseline.x} y={accelBaseline.y} z={accelBaseline.z} deadzone={accelDeadzone} scale={accelScale}
|
|
1181
|
+
</div>
|
|
1182
|
+
<div className="gyro-visual-row">
|
|
1183
|
+
<div className="gyro-xy" ref={gyroXYRef}>
|
|
1184
|
+
<div className="gyro-cross-h" />
|
|
1185
|
+
<div className="gyro-cross-v" />
|
|
1186
|
+
<div className="gyro-dot-raw" style={getRawAxisDotStyle()} />
|
|
1187
|
+
<div className="gyro-dot" style={getAxisDotStyle()} />
|
|
1188
|
+
</div>
|
|
1189
|
+
<div className="gyro-z-wrap">
|
|
1190
|
+
<div className="gyro-z-track">
|
|
1191
|
+
<div className="gyro-z-fill" style={{ height: getZFill() }} />
|
|
1192
|
+
</div>
|
|
1193
|
+
<div className="small code">Z</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
</div>
|
|
1196
|
+
<div className="small code" style={{ maxHeight: 40, overflow: 'auto' }}>
|
|
1197
|
+
accelRaw={accelRaw}
|
|
1198
|
+
</div>
|
|
1199
|
+
<div className="small code">
|
|
1200
|
+
parsedRaw x={accelLatestRawRef.current.x} y={accelLatestRawRef.current.y} z={accelLatestRawRef.current.z}
|
|
1201
|
+
</div>
|
|
1202
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
|
1203
|
+
<button onClick={startAccelerometerMonitor}>Start Gyro</button>
|
|
1204
|
+
<button onClick={stopAccelerometerMonitor}>Stop Gyro</button>
|
|
1205
|
+
<button onClick={calibrateGyroBaseline}>Calibrate Gyro</button>
|
|
1206
|
+
<button onClick={resetHardwareCounters}>Reset Counters</button>
|
|
1207
|
+
<button onClick={() => { setAccelDeadzone(0); accelDeadzoneRef.current = 0; }}>Deadzone 0</button>
|
|
1208
|
+
<button onClick={() => { setAccelDeadzone(0.05); accelDeadzoneRef.current = 0.05; }}>Deadzone Default</button>
|
|
1209
|
+
</div>
|
|
1210
|
+
<input
|
|
1211
|
+
className="relay-input code"
|
|
1212
|
+
type="number"
|
|
1213
|
+
min="0"
|
|
1214
|
+
step="0.005"
|
|
1215
|
+
value={accelDeadzone}
|
|
1216
|
+
onChange={(e) => {
|
|
1217
|
+
const next = Math.max(0, Number(e.target.value || 0));
|
|
1218
|
+
setAccelDeadzone(next);
|
|
1219
|
+
accelDeadzoneRef.current = next;
|
|
1220
|
+
}}
|
|
1221
|
+
/>
|
|
1222
|
+
<div className="small code">
|
|
1223
|
+
Hardware counters update from real device events: sideClick, longPressStart/End, scrollUp/Down.
|
|
1224
|
+
</div>
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
|
|
1228
|
+
<div className="panel">
|
|
1229
|
+
<div className="small" style={{ marginBottom: 8 }}>
|
|
1230
|
+
Live test log
|
|
1231
|
+
</div>
|
|
1232
|
+
<div className="log">
|
|
1233
|
+
{logs.length === 0 && <div className="small">No logs yet.</div>}
|
|
1234
|
+
{logs.map((item, index) => (
|
|
1235
|
+
<div key={`${item.time}-${index}`} className="entry">
|
|
1236
|
+
<div className={item.status === 'pass' ? 'pass' : item.status === 'fail' ? 'fail' : 'skip'}>
|
|
1237
|
+
[{item.time}] {item.status.toUpperCase()} - {item.title}
|
|
1238
|
+
</div>
|
|
1239
|
+
<div className="small code">{item.detail}</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
))}
|
|
1242
|
+
</div>
|
|
1243
|
+
</div>
|
|
1244
|
+
|
|
1245
|
+
<div className="panel">
|
|
1246
|
+
<div className="small" style={{ marginBottom: 8 }}>
|
|
1247
|
+
Device console logs (Socket.IO relay)
|
|
1248
|
+
</div>
|
|
1249
|
+
<button onClick={() => setDeviceLogs([])} style={{ marginBottom: 8 }}>Clear Device Logs</button>
|
|
1250
|
+
<div className="log">
|
|
1251
|
+
{deviceLogs.length === 0 && <div className="small">No device logs yet.</div>}
|
|
1252
|
+
{deviceLogs.map((item, index) => (
|
|
1253
|
+
<div key={`${item.time}-${index}`} className="entry">
|
|
1254
|
+
<div className={item.level === 'error' ? 'fail' : item.level === 'warn' ? 'skip' : 'pass'}>
|
|
1255
|
+
[{item.time}] {item.level.toUpperCase()} [{item.deviceId}]
|
|
1256
|
+
</div>
|
|
1257
|
+
<div className="small code">{item.message}</div>
|
|
1258
|
+
{item.url && <div className="small code">{item.url}</div>}
|
|
1259
|
+
</div>
|
|
1260
|
+
))}
|
|
1261
|
+
</div>
|
|
1262
|
+
</div>
|
|
1263
|
+
</main>
|
|
1264
|
+
);
|
|
1265
|
+
}
|