@elizaos/plugin-facewear 2.0.3-beta.5 → 2.0.3-beta.7

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.
Files changed (191) hide show
  1. package/dist/actions/display-text.d.ts +4 -0
  2. package/dist/actions/display-text.d.ts.map +1 -0
  3. package/dist/actions/display-text.js +90 -0
  4. package/dist/actions/display-text.js.map +1 -0
  5. package/dist/actions/facewear-connect.d.ts +3 -0
  6. package/dist/actions/facewear-connect.d.ts.map +1 -0
  7. package/dist/actions/facewear-connect.js +70 -0
  8. package/dist/actions/facewear-connect.js.map +1 -0
  9. package/dist/actions/facewear-control.d.ts +4 -0
  10. package/dist/actions/facewear-control.d.ts.map +1 -0
  11. package/dist/actions/facewear-control.js +627 -0
  12. package/dist/actions/facewear-control.js.map +1 -0
  13. package/dist/actions/facewear-debug.d.ts +3 -0
  14. package/dist/actions/facewear-debug.d.ts.map +1 -0
  15. package/dist/actions/facewear-debug.js +62 -0
  16. package/dist/actions/facewear-debug.js.map +1 -0
  17. package/dist/actions/facewear-status.d.ts +4 -0
  18. package/dist/actions/facewear-status.d.ts.map +1 -0
  19. package/dist/actions/facewear-status.js +66 -0
  20. package/dist/actions/facewear-status.js.map +1 -0
  21. package/dist/actions/microphone.d.ts +4 -0
  22. package/dist/actions/microphone.d.ts.map +1 -0
  23. package/dist/actions/microphone.js +63 -0
  24. package/dist/actions/microphone.js.map +1 -0
  25. package/dist/actions/view-actions.d.ts +23 -0
  26. package/dist/actions/view-actions.d.ts.map +1 -0
  27. package/dist/actions/view-actions.js +314 -0
  28. package/dist/actions/view-actions.js.map +1 -0
  29. package/dist/actions/vision-query.d.ts +4 -0
  30. package/dist/actions/vision-query.d.ts.map +1 -0
  31. package/dist/actions/vision-query.js +41 -0
  32. package/dist/actions/vision-query.js.map +1 -0
  33. package/dist/actions/xr-view-actions.d.ts +14 -0
  34. package/dist/actions/xr-view-actions.d.ts.map +1 -0
  35. package/dist/actions/xr-view-actions.js +29 -0
  36. package/dist/actions/xr-view-actions.js.map +1 -0
  37. package/dist/components/FacewearSpatialView.d.ts +50 -0
  38. package/dist/components/FacewearSpatialView.d.ts.map +1 -0
  39. package/dist/components/FacewearSpatialView.js +129 -0
  40. package/dist/components/FacewearSpatialView.js.map +1 -0
  41. package/dist/components/FacewearView.d.ts +17 -0
  42. package/dist/components/FacewearView.d.ts.map +1 -0
  43. package/dist/components/FacewearView.js +88 -0
  44. package/dist/components/FacewearView.js.map +1 -0
  45. package/dist/components/SmartglassesPanelView.d.ts +22 -0
  46. package/dist/components/SmartglassesPanelView.d.ts.map +1 -0
  47. package/dist/components/SmartglassesPanelView.js +140 -0
  48. package/dist/components/SmartglassesPanelView.js.map +1 -0
  49. package/dist/components/SmartglassesSpatialView.d.ts +46 -0
  50. package/dist/components/SmartglassesSpatialView.d.ts.map +1 -0
  51. package/dist/components/SmartglassesSpatialView.js +240 -0
  52. package/dist/components/SmartglassesSpatialView.js.map +1 -0
  53. package/dist/components/facewear-profiles.d.ts +27 -0
  54. package/dist/components/facewear-profiles.d.ts.map +1 -0
  55. package/dist/components/facewear-profiles.js +40 -0
  56. package/dist/components/facewear-profiles.js.map +1 -0
  57. package/dist/devices/apple-vision-pro.d.ts +7 -0
  58. package/dist/devices/apple-vision-pro.d.ts.map +1 -0
  59. package/dist/devices/apple-vision-pro.js +21 -0
  60. package/dist/devices/apple-vision-pro.js.map +1 -0
  61. package/dist/devices/even-realities.d.ts +7 -0
  62. package/dist/devices/even-realities.d.ts.map +1 -0
  63. package/dist/devices/even-realities.js +13 -0
  64. package/dist/devices/even-realities.js.map +1 -0
  65. package/dist/devices/meta-quest.d.ts +5 -0
  66. package/dist/devices/meta-quest.d.ts.map +1 -0
  67. package/dist/devices/meta-quest.js +21 -0
  68. package/dist/devices/meta-quest.js.map +1 -0
  69. package/dist/devices/registry.d.ts +19 -0
  70. package/dist/devices/registry.d.ts.map +1 -0
  71. package/dist/devices/registry.js +96 -0
  72. package/dist/devices/registry.js.map +1 -0
  73. package/dist/devices/xreal.d.ts +7 -0
  74. package/dist/devices/xreal.d.ts.map +1 -0
  75. package/dist/devices/xreal.js +19 -0
  76. package/dist/devices/xreal.js.map +1 -0
  77. package/dist/index.d.ts +28 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +260 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/protocol/smartglasses.d.ts +306 -0
  82. package/dist/protocol/smartglasses.d.ts.map +1 -0
  83. package/dist/protocol/smartglasses.js +1485 -0
  84. package/dist/protocol/smartglasses.js.map +1 -0
  85. package/dist/protocol/xr.d.ts +137 -0
  86. package/dist/protocol/xr.d.ts.map +1 -0
  87. package/dist/protocol/xr.js +18 -0
  88. package/dist/protocol/xr.js.map +1 -0
  89. package/dist/providers/facewear-context.d.ts +3 -0
  90. package/dist/providers/facewear-context.d.ts.map +1 -0
  91. package/dist/providers/facewear-context.js +59 -0
  92. package/dist/providers/facewear-context.js.map +1 -0
  93. package/dist/providers/smartglasses-status.d.ts +3 -0
  94. package/dist/providers/smartglasses-status.d.ts.map +1 -0
  95. package/dist/providers/smartglasses-status.js +33 -0
  96. package/dist/providers/smartglasses-status.js.map +1 -0
  97. package/dist/register-terminal-view.d.ts +21 -0
  98. package/dist/register-terminal-view.d.ts.map +1 -0
  99. package/dist/register-terminal-view.js +70 -0
  100. package/dist/register-terminal-view.js.map +1 -0
  101. package/dist/register.d.ts +8 -0
  102. package/dist/register.d.ts.map +1 -0
  103. package/dist/register.js +116 -0
  104. package/dist/register.js.map +1 -0
  105. package/dist/routes/connect.d.ts +3 -0
  106. package/dist/routes/connect.d.ts.map +1 -0
  107. package/dist/routes/connect.js +86 -0
  108. package/dist/routes/connect.js.map +1 -0
  109. package/dist/routes/device-config.d.ts +5 -0
  110. package/dist/routes/device-config.d.ts.map +1 -0
  111. package/dist/routes/device-config.js +56 -0
  112. package/dist/routes/device-config.js.map +1 -0
  113. package/dist/routes/simulator-route.d.ts +8 -0
  114. package/dist/routes/simulator-route.d.ts.map +1 -0
  115. package/dist/routes/simulator-route.js +32 -0
  116. package/dist/routes/simulator-route.js.map +1 -0
  117. package/dist/routes/status.d.ts +3 -0
  118. package/dist/routes/status.d.ts.map +1 -0
  119. package/dist/routes/status.js +34 -0
  120. package/dist/routes/status.js.map +1 -0
  121. package/dist/routes/view-host.d.ts +24 -0
  122. package/dist/routes/view-host.d.ts.map +1 -0
  123. package/dist/routes/view-host.js +339 -0
  124. package/dist/routes/view-host.js.map +1 -0
  125. package/dist/routes/views.d.ts +8 -0
  126. package/dist/routes/views.d.ts.map +1 -0
  127. package/dist/routes/views.js +31 -0
  128. package/dist/routes/views.js.map +1 -0
  129. package/dist/services/audio-pipeline.d.ts +20 -0
  130. package/dist/services/audio-pipeline.d.ts.map +1 -0
  131. package/dist/services/audio-pipeline.js +87 -0
  132. package/dist/services/audio-pipeline.js.map +1 -0
  133. package/dist/services/facewear-service.d.ts +26 -0
  134. package/dist/services/facewear-service.d.ts.map +1 -0
  135. package/dist/services/facewear-service.js +45 -0
  136. package/dist/services/facewear-service.js.map +1 -0
  137. package/dist/services/smartglasses-service.d.ts +244 -0
  138. package/dist/services/smartglasses-service.d.ts.map +1 -0
  139. package/dist/services/smartglasses-service.js +821 -0
  140. package/dist/services/smartglasses-service.js.map +1 -0
  141. package/dist/services/vision-pipeline.d.ts +16 -0
  142. package/dist/services/vision-pipeline.d.ts.map +1 -0
  143. package/dist/services/vision-pipeline.js +39 -0
  144. package/dist/services/vision-pipeline.js.map +1 -0
  145. package/dist/services/xr-session-service.d.ts +54 -0
  146. package/dist/services/xr-session-service.d.ts.map +1 -0
  147. package/dist/services/xr-session-service.js +345 -0
  148. package/dist/services/xr-session-service.js.map +1 -0
  149. package/dist/status-format.d.ts +15 -0
  150. package/dist/status-format.d.ts.map +1 -0
  151. package/dist/status-format.js +89 -0
  152. package/dist/status-format.js.map +1 -0
  153. package/dist/transport/even-bridge.d.ts +69 -0
  154. package/dist/transport/even-bridge.d.ts.map +1 -0
  155. package/dist/transport/even-bridge.js +510 -0
  156. package/dist/transport/even-bridge.js.map +1 -0
  157. package/dist/transport/mock.d.ts +42 -0
  158. package/dist/transport/mock.d.ts.map +1 -0
  159. package/dist/transport/mock.js +124 -0
  160. package/dist/transport/mock.js.map +1 -0
  161. package/dist/transport/noble.d.ts +62 -0
  162. package/dist/transport/noble.d.ts.map +1 -0
  163. package/dist/transport/noble.js +256 -0
  164. package/dist/transport/noble.js.map +1 -0
  165. package/dist/transport/types.d.ts +36 -0
  166. package/dist/transport/types.d.ts.map +1 -0
  167. package/dist/transport/types.js +1 -0
  168. package/dist/transport/types.js.map +1 -0
  169. package/dist/transport/web-bluetooth.d.ts +58 -0
  170. package/dist/transport/web-bluetooth.d.ts.map +1 -0
  171. package/dist/transport/web-bluetooth.js +164 -0
  172. package/dist/transport/web-bluetooth.js.map +1 -0
  173. package/dist/ui/FacewearAppView.d.ts +4 -0
  174. package/dist/ui/FacewearAppView.d.ts.map +1 -0
  175. package/dist/ui/FacewearAppView.js +257 -0
  176. package/dist/ui/FacewearAppView.js.map +1 -0
  177. package/dist/ui/SmartglassesView.d.ts +10 -0
  178. package/dist/ui/SmartglassesView.d.ts.map +1 -0
  179. package/dist/ui/SmartglassesView.helpers.d.ts +104 -0
  180. package/dist/ui/SmartglassesView.helpers.d.ts.map +1 -0
  181. package/dist/ui/SmartglassesView.helpers.js +261 -0
  182. package/dist/ui/SmartglassesView.helpers.js.map +1 -0
  183. package/dist/ui/SmartglassesView.js +1189 -0
  184. package/dist/ui/SmartglassesView.js.map +1 -0
  185. package/dist/ui/facewear-view-bundle.d.ts +5 -0
  186. package/dist/ui/facewear-view-bundle.d.ts.map +1 -0
  187. package/dist/ui/facewear-view-bundle.js +17 -0
  188. package/dist/ui/facewear-view-bundle.js.map +1 -0
  189. package/dist/views/bundle.js +2950 -0
  190. package/dist/views/bundle.js.map +1 -0
  191. package/package.json +5 -5
@@ -0,0 +1,1189 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useAgentElement } from "@elizaos/ui/agent-surface";
3
+ import {
4
+ BatteryCharging,
5
+ Bluetooth,
6
+ CheckCircle2,
7
+ Circle,
8
+ Clipboard,
9
+ Download,
10
+ Glasses,
11
+ Mic,
12
+ RefreshCw,
13
+ Settings2,
14
+ Wifi,
15
+ XCircle
16
+ } from "lucide-react";
17
+ import { useMemo, useRef, useState } from "react";
18
+ import {
19
+ encodeBatteryStatusRequest,
20
+ encodeBrightness,
21
+ encodeClearScreen,
22
+ encodeConnectionReady,
23
+ encodeGetSerial,
24
+ encodeSilentMode
25
+ } from "../protocol/smartglasses.js";
26
+ import { EvenBridgeTransport } from "../transport/even-bridge.js";
27
+ import {
28
+ getWebBluetoothG1Transport,
29
+ WebBluetoothG1Transport
30
+ } from "../transport/web-bluetooth.js";
31
+ import {
32
+ buildViewDisplayPackets,
33
+ callWifiBridge,
34
+ formatWifiStatus,
35
+ headsetValidationBlocker,
36
+ isCradleOrChargingState,
37
+ isMicDisableTap,
38
+ isMicEnableTap,
39
+ missingViewEvidence,
40
+ parseWifiNetworks,
41
+ viewCommandName,
42
+ viewNextAction,
43
+ viewPhysicalBlocker,
44
+ viewScanDiagnosis,
45
+ viewSetupHint
46
+ } from "./SmartglassesView.helpers.js";
47
+ const VISIBLE_TEST_LIMIT = 8;
48
+ const VISIBLE_EVENT_LIMIT = 12;
49
+ const VISIBLE_WIFI_LIMIT = 5;
50
+ const PLATFORM_COPY = {
51
+ desktop: {
52
+ label: "Desktop",
53
+ primary: "Chrome/Edge Web Bluetooth",
54
+ secondary: "Pair both lenses."
55
+ },
56
+ ios: {
57
+ label: "iOS",
58
+ primary: "Native bridge required",
59
+ secondary: "Use the host bridge."
60
+ },
61
+ android: {
62
+ label: "Android",
63
+ primary: "Native bridge preferred",
64
+ secondary: "Pair and configure in the host."
65
+ }
66
+ };
67
+ const DISPLAY_PRESETS = [
68
+ {
69
+ id: "status",
70
+ label: "Status",
71
+ text: "elizaOS smartglasses link online.",
72
+ className: "border-green-500/40 bg-green-500/10 text-green-700"
73
+ },
74
+ {
75
+ id: "ping",
76
+ label: "Ping",
77
+ text: "Display ping. Confirm both lenses render this page.",
78
+ className: "border-accent/45 bg-accent/10 text-accent"
79
+ },
80
+ {
81
+ id: "nav",
82
+ label: "Nav",
83
+ text: "Navigation card ready. Keep eyes forward.",
84
+ className: "border-amber-500/40 bg-amber-500/10 text-amber-700"
85
+ }
86
+ ];
87
+ function now() {
88
+ return (/* @__PURE__ */ new Date()).toISOString();
89
+ }
90
+ function timeout(promise, timeoutMs, message) {
91
+ return new Promise((resolve, reject) => {
92
+ const id = window.setTimeout(() => reject(new Error(message)), timeoutMs);
93
+ promise.then(
94
+ (value) => {
95
+ window.clearTimeout(id);
96
+ resolve(value);
97
+ },
98
+ (err) => {
99
+ window.clearTimeout(id);
100
+ reject(err);
101
+ }
102
+ );
103
+ });
104
+ }
105
+ function normalizeError(error) {
106
+ return error instanceof Error ? error.message : String(error);
107
+ }
108
+ function sleep(ms) {
109
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
110
+ }
111
+ function getBridge() {
112
+ if (typeof window === "undefined") return null;
113
+ return window.__mentraBridge ?? window.__evenBridge ?? null;
114
+ }
115
+ function bytesToHex(data) {
116
+ return [...data].map((byte) => byte.toString(16).padStart(2, "0")).join("");
117
+ }
118
+ function SmartglassesView() {
119
+ const [transport, setTransport] = useState(
120
+ null
121
+ );
122
+ const [lenses, setLenses] = useState({
123
+ left: "idle",
124
+ right: "idle"
125
+ });
126
+ const [events, setEvents] = useState([]);
127
+ const [writes, setWrites] = useState([]);
128
+ const [audioChunks, setAudioChunks] = useState([]);
129
+ const [busy, setBusy] = useState(null);
130
+ const [error, setError] = useState(null);
131
+ const [testText, setTestText] = useState("Smartglasses display test.");
132
+ const [micEnabled, setMicEnabled] = useState(false);
133
+ const [wifiSsid, setWifiSsid] = useState("");
134
+ const [wifiPassword, setWifiPassword] = useState("");
135
+ const [wifiStatus, setWifiStatus] = useState("Not checked");
136
+ const [wifiNetworks, setWifiNetworks] = useState([]);
137
+ const [activePlatform, setActivePlatform] = useState("desktop");
138
+ const [physicalState, setPhysicalState] = useState(null);
139
+ const [batteryState, setBatteryState] = useState(null);
140
+ const [batteryLevels, setBatteryLevels] = useState({});
141
+ const [deviceState, setDeviceState] = useState(null);
142
+ const [serialNumber, setSerialNumber] = useState(null);
143
+ const [tests, setTests] = useState({
144
+ headsetConnected: false,
145
+ init: false,
146
+ display: false,
147
+ serial: false,
148
+ serialObserved: false,
149
+ settings: false,
150
+ microphone: false,
151
+ micEnableWrite: false,
152
+ micDisableWrite: false,
153
+ tapMicEnable: false,
154
+ tapMicDisable: false,
155
+ audio: false,
156
+ transcript: false,
157
+ eventStream: false
158
+ });
159
+ const testsRef = useRef(tests);
160
+ const displaySeqRef = useRef(0);
161
+ const bridge = getBridge();
162
+ const webBluetoothAvailable = Boolean(getWebBluetoothG1Transport());
163
+ const headsetConnected = lenses.left === "connected" && lenses.right === "connected";
164
+ const missingEvidence = useMemo(
165
+ () => missingViewEvidence(
166
+ tests,
167
+ lenses,
168
+ physicalState,
169
+ batteryState,
170
+ events,
171
+ writes
172
+ ),
173
+ [batteryState, events, lenses, physicalState, tests, writes]
174
+ );
175
+ const physicalBlocker = useMemo(
176
+ () => viewPhysicalBlocker(
177
+ tests,
178
+ lenses,
179
+ physicalState,
180
+ batteryState,
181
+ events,
182
+ writes
183
+ ),
184
+ [batteryState, events, lenses, physicalState, tests, writes]
185
+ );
186
+ const report = useMemo(
187
+ () => ({
188
+ ok: missingEvidence.length === 0,
189
+ generatedAt: now(),
190
+ transport: transport?.name ?? (bridge ? "native-bridge" : null),
191
+ connected: headsetConnected,
192
+ lenses,
193
+ scanDiagnosis: viewScanDiagnosis(lenses),
194
+ physicalBlocker,
195
+ setupHint: viewSetupHint(physicalBlocker, physicalState, batteryState),
196
+ nextAction: viewNextAction(physicalBlocker),
197
+ serialNumber,
198
+ tests,
199
+ missingEvidence,
200
+ events,
201
+ writes,
202
+ audio: audioChunks,
203
+ wifi: {
204
+ available: Boolean(bridge),
205
+ status: wifiStatus,
206
+ networks: wifiNetworks
207
+ },
208
+ headsetState: {
209
+ physical: physicalState,
210
+ battery: batteryState,
211
+ batteryLevels,
212
+ device: deviceState
213
+ }
214
+ }),
215
+ [
216
+ bridge,
217
+ audioChunks,
218
+ events,
219
+ headsetConnected,
220
+ lenses,
221
+ missingEvidence,
222
+ physicalBlocker,
223
+ physicalState,
224
+ serialNumber,
225
+ tests,
226
+ transport,
227
+ batteryState,
228
+ batteryLevels,
229
+ deviceState,
230
+ wifiNetworks,
231
+ wifiStatus,
232
+ writes
233
+ ]
234
+ );
235
+ const { ref: connectRef, agentProps: connectAgentProps } = useAgentElement({
236
+ id: "setup-connect-headset",
237
+ role: "button",
238
+ label: "Connect",
239
+ group: "setup",
240
+ status: headsetConnected ? "active" : "inactive",
241
+ description: "Pair both left and right smartglasses lenses as one headset"
242
+ });
243
+ const { ref: runCheckRef, agentProps: runCheckAgentProps } = useAgentElement({
244
+ id: "test-run-check",
245
+ role: "button",
246
+ label: "Run Check",
247
+ group: "test",
248
+ description: "Request serial/battery and send display/settings packets"
249
+ });
250
+ const { ref: wifiSsidRef, agentProps: wifiSsidAgentProps } = useAgentElement({
251
+ id: "wifi-ssid",
252
+ role: "text-input",
253
+ label: "Wi-Fi SSID",
254
+ group: "wifi",
255
+ description: "SSID of the Wi-Fi network to configure on the glasses",
256
+ getValue: () => wifiSsid,
257
+ onFill: (value) => setWifiSsid(value)
258
+ });
259
+ const { ref: wifiPasswordRef, agentProps: wifiPasswordAgentProps } = useAgentElement({
260
+ id: "wifi-password",
261
+ role: "text-input",
262
+ label: "Wi-Fi password",
263
+ group: "wifi",
264
+ description: "Password for the Wi-Fi network to configure on the glasses",
265
+ getValue: () => wifiPassword,
266
+ onFill: (value) => setWifiPassword(value)
267
+ });
268
+ function appendEvent(type, detail) {
269
+ setEvents(
270
+ (current) => [...current, { at: now(), type, detail }].slice(-80)
271
+ );
272
+ }
273
+ function markTest(id, value = true) {
274
+ setTests((current) => {
275
+ const next = { ...current, [id]: value };
276
+ testsRef.current = next;
277
+ return next;
278
+ });
279
+ }
280
+ function recordWrite(side, data) {
281
+ setWrites(
282
+ (current) => [
283
+ ...current,
284
+ {
285
+ at: now(),
286
+ side,
287
+ command: viewCommandName(data),
288
+ bytes: data.length,
289
+ hex: bytesToHex(data.slice(0, 24))
290
+ }
291
+ ].slice(-120)
292
+ );
293
+ }
294
+ async function writeSide(nextTransport, side, data) {
295
+ await nextTransport.write(side, data);
296
+ recordWrite(side, data);
297
+ }
298
+ async function writeBoth(nextTransport, data) {
299
+ await nextTransport.writeBoth(data);
300
+ recordWrite("both", data);
301
+ }
302
+ async function setTransportMic(nextTransport, enabled) {
303
+ await nextTransport.openMicrophone(enabled);
304
+ recordWrite("right", Uint8Array.from([14, enabled ? 1 : 0]));
305
+ }
306
+ async function connectHeadset() {
307
+ setBusy("connect");
308
+ setError(null);
309
+ try {
310
+ const nextTransport = transport ?? (bridge ? new EvenBridgeTransport(bridge) : new WebBluetoothG1Transport());
311
+ setTransport(nextTransport);
312
+ const eventDispose = nextTransport.onEvent((event) => {
313
+ markTest("eventStream");
314
+ if (event.stateCategory === "physical") {
315
+ setPhysicalState(event.stateName ?? event.label ?? null);
316
+ } else if (event.stateCategory === "battery") {
317
+ setBatteryState(event.stateName ?? event.label ?? null);
318
+ } else if (event.stateCategory === "device") {
319
+ setDeviceState(event.stateName ?? event.label ?? null);
320
+ }
321
+ if (event.type === "battery-status" && typeof event.batteryPercent === "number") {
322
+ setBatteryLevels((current) => ({
323
+ ...current,
324
+ [event.side]: event.batteryPercent
325
+ }));
326
+ }
327
+ if (event.type === "serial" && event.serialNumber) {
328
+ setSerialNumber(event.serialNumber);
329
+ markTest("serialObserved");
330
+ }
331
+ if (isMicEnableTap(event.label)) {
332
+ markTest("tapMicEnable");
333
+ setMicEnabled(true);
334
+ const eventAt = now();
335
+ setEvents(
336
+ (current) => [
337
+ ...current,
338
+ { at: eventAt, type: "tap", detail: event.label ?? "" }
339
+ ].slice(-80)
340
+ );
341
+ void nextTransport.openMicrophone(true).then(() => {
342
+ recordWrite("right", Uint8Array.from([14, 1]));
343
+ markTest("microphone");
344
+ markTest("micEnableWrite");
345
+ appendEvent("microphone", "Enabled by tap");
346
+ }).catch((err) => appendEvent("error", normalizeError(err)));
347
+ }
348
+ if (isMicDisableTap(event.label)) {
349
+ markTest("tapMicDisable");
350
+ setMicEnabled(false);
351
+ const eventAt = now();
352
+ setEvents(
353
+ (current) => [
354
+ ...current,
355
+ { at: eventAt, type: "tap", detail: event.label ?? "" }
356
+ ].slice(-80)
357
+ );
358
+ void nextTransport.openMicrophone(false).then(() => {
359
+ recordWrite("right", Uint8Array.from([14, 0]));
360
+ markTest("microphone");
361
+ markTest("micDisableWrite");
362
+ appendEvent("microphone", "Disabled by tap");
363
+ }).catch((err) => appendEvent("error", normalizeError(err)));
364
+ }
365
+ appendEvent(
366
+ "event",
367
+ `${event.side} ${event.type}${event.label ? ` ${event.label}` : ""}`
368
+ );
369
+ });
370
+ const audioDispose = nextTransport.onAudio(
371
+ (audio, sampleRate, side, encoding, sequence) => {
372
+ if (audio.byteLength > 0) markTest("audio");
373
+ setAudioChunks(
374
+ (current) => [
375
+ ...current,
376
+ {
377
+ at: now(),
378
+ side,
379
+ sampleRate,
380
+ encoding: encoding ?? null,
381
+ sequence,
382
+ bytes: audio.byteLength
383
+ }
384
+ ].slice(-80)
385
+ );
386
+ appendEvent(
387
+ "audio",
388
+ `${side} ${audio.byteLength} bytes @ ${sampleRate}Hz${encoding ? ` ${encoding}` : ""}`
389
+ );
390
+ }
391
+ );
392
+ const transcriptDispose = "onTranscript" in nextTransport && nextTransport.onTranscript ? nextTransport.onTranscript((text, isFinal) => {
393
+ markTest("transcript");
394
+ appendEvent(
395
+ "transcript",
396
+ `${isFinal ? "final" : "partial"} ${text}`
397
+ );
398
+ }) : void 0;
399
+ try {
400
+ if (nextTransport instanceof WebBluetoothG1Transport) {
401
+ await connectLens(nextTransport, "left");
402
+ await connectLens(nextTransport, "right");
403
+ } else {
404
+ await nextTransport.connect();
405
+ setLenses({ left: "connected", right: "connected" });
406
+ }
407
+ } catch (err) {
408
+ eventDispose();
409
+ audioDispose();
410
+ transcriptDispose?.();
411
+ throw err;
412
+ }
413
+ await writeSide(nextTransport, "left", encodeConnectionReady("left"));
414
+ await writeSide(nextTransport, "right", encodeConnectionReady("right"));
415
+ markTest("headsetConnected");
416
+ markTest("init");
417
+ appendEvent("connect", "Whole headset connected");
418
+ } catch (err) {
419
+ setError(normalizeError(err));
420
+ appendEvent("error", normalizeError(err));
421
+ } finally {
422
+ setBusy(null);
423
+ }
424
+ }
425
+ async function connectLens(nextTransport, side) {
426
+ setLenses((current) => ({ ...current, [side]: "prompting" }));
427
+ appendEvent("pairing", `Select the ${side} lens in the Bluetooth picker`);
428
+ try {
429
+ await timeout(
430
+ nextTransport.connectLens(side),
431
+ 6e4,
432
+ `Timed out connecting the ${side} lens`
433
+ );
434
+ setLenses((current) => ({ ...current, [side]: "connected" }));
435
+ appendEvent("connect", `${side} lens connected`);
436
+ } catch (err) {
437
+ setLenses((current) => ({ ...current, [side]: "failed" }));
438
+ throw err;
439
+ }
440
+ }
441
+ async function requireTransport() {
442
+ if (!transport || !headsetConnected) {
443
+ throw new Error("Connect the whole headset before running this test");
444
+ }
445
+ return transport;
446
+ }
447
+ async function sendDisplay() {
448
+ setBusy("display");
449
+ setError(null);
450
+ try {
451
+ const nextTransport = await requireTransport();
452
+ const display = buildViewDisplayPackets(testText, {
453
+ startSeq: displaySeqRef.current
454
+ });
455
+ displaySeqRef.current = display.nextSeq;
456
+ for (const packet of display.packets) {
457
+ await writeBoth(nextTransport, packet);
458
+ }
459
+ markTest("display");
460
+ appendEvent("display", `Sent ${display.pages} display page(s)`);
461
+ } catch (err) {
462
+ setError(normalizeError(err));
463
+ appendEvent("error", normalizeError(err));
464
+ } finally {
465
+ setBusy(null);
466
+ }
467
+ }
468
+ async function clearDisplay() {
469
+ setBusy("clear");
470
+ setError(null);
471
+ try {
472
+ const nextTransport = await requireTransport();
473
+ await writeBoth(nextTransport, encodeClearScreen());
474
+ appendEvent("display", "Cleared display");
475
+ } catch (err) {
476
+ setError(normalizeError(err));
477
+ appendEvent("error", normalizeError(err));
478
+ } finally {
479
+ setBusy(null);
480
+ }
481
+ }
482
+ async function runHardwareCheck() {
483
+ setBusy("check");
484
+ setError(null);
485
+ try {
486
+ const nextTransport = await requireTransport();
487
+ await writeSide(nextTransport, "left", encodeGetSerial());
488
+ await writeBoth(nextTransport, encodeBatteryStatusRequest());
489
+ await writeBoth(nextTransport, encodeBrightness(32));
490
+ await writeBoth(nextTransport, encodeSilentMode(false));
491
+ markTest("serial");
492
+ markTest("settings");
493
+ appendEvent("test", "Requested serial/battery and sent settings packets");
494
+ await sendDisplay();
495
+ } catch (err) {
496
+ setError(normalizeError(err));
497
+ appendEvent("error", normalizeError(err));
498
+ setBusy(null);
499
+ }
500
+ }
501
+ async function runGuidedValidation() {
502
+ setBusy("guided");
503
+ setError(null);
504
+ let nextTransport = null;
505
+ try {
506
+ nextTransport = await requireTransport();
507
+ const blocker = headsetValidationBlocker(physicalState, batteryState);
508
+ if (blocker) {
509
+ throw new Error(blocker);
510
+ }
511
+ await setTransportMic(nextTransport, false);
512
+ setMicEnabled(false);
513
+ markTest("microphone");
514
+ markTest("micDisableWrite");
515
+ const display = buildViewDisplayPackets(
516
+ "Validation: single tap, speak clearly, then double tap.",
517
+ { startSeq: displaySeqRef.current }
518
+ );
519
+ displaySeqRef.current = display.nextSeq;
520
+ for (const packet of display.packets) {
521
+ await writeBoth(nextTransport, packet);
522
+ }
523
+ markTest("display");
524
+ appendEvent("validation", "Single tap, speak clearly, then double tap");
525
+ const deadline = Date.now() + 6e4;
526
+ while (Date.now() < deadline) {
527
+ const current2 = testsRef.current;
528
+ if (current2.tapMicEnable && current2.audio && current2.tapMicDisable) {
529
+ appendEvent("validation", "Side-tap microphone validation passed");
530
+ return;
531
+ }
532
+ await sleep(500);
533
+ }
534
+ const current = testsRef.current;
535
+ const missing = [
536
+ !current.tapMicEnable && "tap mic enable",
537
+ !current.audio && "right/bridge audio",
538
+ !current.tapMicDisable && "tap mic disable"
539
+ ].filter(Boolean);
540
+ throw new Error(`Guided validation missing: ${missing.join(", ")}`);
541
+ } catch (err) {
542
+ setError(normalizeError(err));
543
+ appendEvent("error", normalizeError(err));
544
+ } finally {
545
+ try {
546
+ if (nextTransport) {
547
+ await setTransportMic(nextTransport, false);
548
+ setMicEnabled(false);
549
+ }
550
+ } catch {
551
+ }
552
+ setBusy(null);
553
+ }
554
+ }
555
+ async function toggleMic(enabled) {
556
+ setBusy(enabled ? "mic-on" : "mic-off");
557
+ setError(null);
558
+ try {
559
+ const nextTransport = await requireTransport();
560
+ await setTransportMic(nextTransport, enabled);
561
+ setMicEnabled(enabled);
562
+ markTest("microphone");
563
+ markTest(enabled ? "micEnableWrite" : "micDisableWrite");
564
+ appendEvent("microphone", enabled ? "Enabled" : "Disabled");
565
+ } catch (err) {
566
+ setError(normalizeError(err));
567
+ appendEvent("error", normalizeError(err));
568
+ } finally {
569
+ setBusy(null);
570
+ }
571
+ }
572
+ async function scanWifi() {
573
+ setBusy("wifi-scan");
574
+ setError(null);
575
+ try {
576
+ if (!bridge) throw new Error("Unavailable");
577
+ const result = await callWifiBridge(bridge, "request_wifi_scan");
578
+ const networks = parseWifiNetworks(result);
579
+ setWifiNetworks(networks);
580
+ setWifiStatus(
581
+ networks.length > 0 ? `Found ${networks.length} network(s)` : "Scan requested; waiting for bridge results"
582
+ );
583
+ appendEvent("wifi", "Requested Wi-Fi scan through bridge");
584
+ } catch (err) {
585
+ setError(normalizeError(err));
586
+ setWifiStatus(normalizeError(err));
587
+ appendEvent("error", normalizeError(err));
588
+ } finally {
589
+ setBusy(null);
590
+ }
591
+ }
592
+ async function refreshWifiStatus() {
593
+ setBusy("wifi-status");
594
+ setError(null);
595
+ try {
596
+ if (!bridge) throw new Error("Unavailable");
597
+ const result = await callWifiBridge(bridge, "request_wifi_status");
598
+ const networks = parseWifiNetworks(result);
599
+ if (networks.length > 0) setWifiNetworks(networks);
600
+ setWifiStatus(formatWifiStatus(result));
601
+ appendEvent("wifi", "Requested Wi-Fi status through bridge");
602
+ } catch (err) {
603
+ setError(normalizeError(err));
604
+ setWifiStatus(normalizeError(err));
605
+ appendEvent("error", normalizeError(err));
606
+ } finally {
607
+ setBusy(null);
608
+ }
609
+ }
610
+ async function configureWifi() {
611
+ setBusy("wifi-configure");
612
+ setError(null);
613
+ try {
614
+ if (!bridge) throw new Error("Unavailable");
615
+ if (!wifiSsid.trim()) throw new Error("Enter a Wi-Fi SSID");
616
+ await callWifiBridge(bridge, "set_wifi_credentials", {
617
+ ssid: wifiSsid.trim(),
618
+ password: wifiPassword
619
+ });
620
+ setWifiStatus(`Credentials sent for ${wifiSsid.trim()}`);
621
+ appendEvent("wifi", `Sent credentials for ${wifiSsid.trim()}`);
622
+ } catch (err) {
623
+ setError(normalizeError(err));
624
+ setWifiStatus(normalizeError(err));
625
+ appendEvent("error", normalizeError(err));
626
+ } finally {
627
+ setBusy(null);
628
+ }
629
+ }
630
+ async function requestWifiSetup() {
631
+ setBusy("wifi-setup");
632
+ setError(null);
633
+ try {
634
+ if (!bridge) throw new Error("Unavailable");
635
+ await callWifiBridge(bridge, "request_wifi_setup", {
636
+ reason: "Smartglasses setup"
637
+ });
638
+ setWifiStatus("Native Wi-Fi setup requested");
639
+ appendEvent("wifi", "Requested native Wi-Fi setup flow");
640
+ } catch (err) {
641
+ setError(normalizeError(err));
642
+ setWifiStatus(normalizeError(err));
643
+ appendEvent("error", normalizeError(err));
644
+ } finally {
645
+ setBusy(null);
646
+ }
647
+ }
648
+ async function copyReport() {
649
+ window.facewearSmartglassesReport = report;
650
+ await navigator.clipboard?.writeText(JSON.stringify(report, null, 2));
651
+ appendEvent("report", "Copied diagnostics report");
652
+ }
653
+ function downloadReport() {
654
+ window.facewearSmartglassesReport = report;
655
+ const blob = new Blob([JSON.stringify(report, null, 2)], {
656
+ type: "application/json"
657
+ });
658
+ const url = URL.createObjectURL(blob);
659
+ const anchor = document.createElement("a");
660
+ anchor.href = url;
661
+ anchor.download = `smartglasses-report-${Date.now()}.json`;
662
+ anchor.click();
663
+ URL.revokeObjectURL(url);
664
+ appendEvent("report", "Downloaded diagnostics report");
665
+ }
666
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto bg-bg text-txt", children: [
667
+ /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
668
+ /* @__PURE__ */ jsx("div", { className: "min-w-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
669
+ /* @__PURE__ */ jsx(Glasses, { className: "h-4 w-4 text-accent" }),
670
+ /* @__PURE__ */ jsx("h1", { className: "text-sm font-semibold", children: "Smartglasses" })
671
+ ] }) }),
672
+ /* @__PURE__ */ jsx(
673
+ StatusPill,
674
+ {
675
+ ok: headsetConnected,
676
+ label: headsetConnected ? "Connected" : "Offline"
677
+ }
678
+ )
679
+ ] }) }),
680
+ /* @__PURE__ */ jsxs("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-3 p-4", children: [
681
+ /* @__PURE__ */ jsxs(Panel, { children: [
682
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
683
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Setup" }) }),
684
+ /* @__PURE__ */ jsxs(
685
+ "button",
686
+ {
687
+ ref: connectRef,
688
+ type: "button",
689
+ onClick: () => void connectHeadset(),
690
+ disabled: !bridge && !webBluetoothAvailable || busy !== null,
691
+ "aria-label": "Connect",
692
+ className: "inline-flex h-9 items-center gap-2 rounded-md bg-accent px-3 text-sm font-medium text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50",
693
+ ...connectAgentProps,
694
+ children: [
695
+ /* @__PURE__ */ jsx(Bluetooth, { className: "h-4 w-4" }),
696
+ "Connect"
697
+ ]
698
+ }
699
+ )
700
+ ] }),
701
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-3 sm:grid-cols-2", children: [
702
+ /* @__PURE__ */ jsx(LensStatus, { side: "left", state: lenses.left }),
703
+ /* @__PURE__ */ jsx(LensStatus, { side: "right", state: lenses.right })
704
+ ] }),
705
+ !webBluetoothAvailable && /* @__PURE__ */ jsx("p", { className: "mt-3 px-1 text-xs text-muted", children: "Web Bluetooth unavailable" }),
706
+ /* @__PURE__ */ jsx(
707
+ HeadsetStateHint,
708
+ {
709
+ physicalState,
710
+ batteryState,
711
+ deviceState
712
+ }
713
+ )
714
+ ] }),
715
+ /* @__PURE__ */ jsxs(Panel, { children: [
716
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Platform" }),
717
+ /* @__PURE__ */ jsx("div", { className: "mt-3 grid grid-cols-3 gap-1", children: Object.keys(PLATFORM_COPY).map((key) => /* @__PURE__ */ jsx(
718
+ PlatformTabButton,
719
+ {
720
+ platformKey: key,
721
+ isActive: activePlatform === key,
722
+ onSelect: setActivePlatform
723
+ },
724
+ key
725
+ )) }),
726
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-xs text-txt", children: PLATFORM_COPY[activePlatform].primary }),
727
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-muted", children: PLATFORM_COPY[activePlatform].secondary })
728
+ ] }),
729
+ /* @__PURE__ */ jsxs(Panel, { children: [
730
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
731
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Test" }) }),
732
+ /* @__PURE__ */ jsxs(
733
+ "button",
734
+ {
735
+ ref: runCheckRef,
736
+ type: "button",
737
+ onClick: () => void runHardwareCheck(),
738
+ disabled: !headsetConnected || busy !== null,
739
+ "aria-label": "Run Check",
740
+ className: "inline-flex h-9 items-center gap-2 rounded-md border border-border px-3 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50",
741
+ ...runCheckAgentProps,
742
+ children: [
743
+ /* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4" }),
744
+ "Check"
745
+ ]
746
+ }
747
+ )
748
+ ] }),
749
+ /* @__PURE__ */ jsx("div", { className: "mt-4 grid grid-cols-3 gap-2", children: DISPLAY_PRESETS.map((preset) => /* @__PURE__ */ jsx(
750
+ "button",
751
+ {
752
+ type: "button",
753
+ onClick: () => setTestText(preset.text),
754
+ "aria-pressed": testText === preset.text,
755
+ className: `h-14 px-3 text-left text-xs font-semibold transition ${testText === preset.text ? preset.className : "text-muted hover:text-txt"}`,
756
+ children: preset.label
757
+ },
758
+ preset.id
759
+ )) }),
760
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
761
+ /* @__PURE__ */ jsx(
762
+ ActionButton,
763
+ {
764
+ onClick: sendDisplay,
765
+ disabled: !headsetConnected || busy !== null,
766
+ agentId: "test-send-display",
767
+ agentLabel: "Send Display",
768
+ agentGroup: "test",
769
+ agentDescription: "Send the display test text to the smartglasses",
770
+ children: "Display"
771
+ }
772
+ ),
773
+ /* @__PURE__ */ jsx(
774
+ ActionButton,
775
+ {
776
+ onClick: clearDisplay,
777
+ disabled: !headsetConnected || busy !== null,
778
+ agentId: "test-clear-display",
779
+ agentLabel: "Clear Display",
780
+ agentGroup: "test",
781
+ agentDescription: "Clear the smartglasses display",
782
+ children: "Clear"
783
+ }
784
+ ),
785
+ /* @__PURE__ */ jsxs(
786
+ ActionButton,
787
+ {
788
+ onClick: () => toggleMic(!micEnabled),
789
+ disabled: !headsetConnected || busy !== null,
790
+ agentId: "test-toggle-mic",
791
+ agentLabel: micEnabled ? "Turn Mic Off" : "Turn Mic On",
792
+ agentGroup: "test",
793
+ agentDescription: "Toggle the smartglasses microphone on or off",
794
+ children: [
795
+ /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" }),
796
+ micEnabled ? "Mic Off" : "Mic On"
797
+ ]
798
+ }
799
+ ),
800
+ /* @__PURE__ */ jsx(
801
+ ActionButton,
802
+ {
803
+ onClick: runGuidedValidation,
804
+ disabled: !headsetConnected || busy !== null,
805
+ agentId: "test-guided-validation",
806
+ agentLabel: "Guided Validation",
807
+ agentGroup: "test",
808
+ agentDescription: "Run the guided side-tap and microphone validation flow",
809
+ children: "Validate"
810
+ }
811
+ )
812
+ ] }),
813
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-2 sm:grid-cols-2", children: [
814
+ Object.entries(tests).slice(0, VISIBLE_TEST_LIMIT).map(([id, ok]) => /* @__PURE__ */ jsx(CheckRow, { ok, label: labelForTest(id) }, id)),
815
+ Object.keys(tests).length > VISIBLE_TEST_LIMIT ? /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 text-xs text-muted", children: [
816
+ "+",
817
+ Object.keys(tests).length - VISIBLE_TEST_LIMIT,
818
+ " checks"
819
+ ] }) : null
820
+ ] })
821
+ ] }),
822
+ /* @__PURE__ */ jsxs(Panel, { children: [
823
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
824
+ /* @__PURE__ */ jsx(Wifi, { className: "h-4 w-4 text-accent" }),
825
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Wi-Fi" })
826
+ ] }),
827
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-2 sm:grid-cols-2", children: [
828
+ /* @__PURE__ */ jsx(
829
+ "input",
830
+ {
831
+ ref: wifiSsidRef,
832
+ value: wifiSsid,
833
+ onChange: (event) => setWifiSsid(event.target.value),
834
+ placeholder: "SSID",
835
+ "aria-label": "Wi-Fi SSID",
836
+ className: "h-9 rounded-md border border-border bg-bg px-3 text-sm outline-none focus:ring-2 focus:ring-ring",
837
+ ...wifiSsidAgentProps
838
+ }
839
+ ),
840
+ /* @__PURE__ */ jsx(
841
+ "input",
842
+ {
843
+ ref: wifiPasswordRef,
844
+ value: wifiPassword,
845
+ onChange: (event) => setWifiPassword(event.target.value),
846
+ placeholder: "Password",
847
+ type: "password",
848
+ "aria-label": "Wi-Fi password",
849
+ className: "h-9 rounded-md border border-border bg-bg px-3 text-sm outline-none focus:ring-2 focus:ring-ring",
850
+ ...wifiPasswordAgentProps
851
+ }
852
+ )
853
+ ] }),
854
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
855
+ /* @__PURE__ */ jsx(
856
+ ActionButton,
857
+ {
858
+ onClick: scanWifi,
859
+ disabled: !bridge || busy !== null,
860
+ agentId: "wifi-scan",
861
+ agentLabel: "Scan Wi-Fi",
862
+ agentGroup: "wifi",
863
+ agentDescription: "Scan for nearby Wi-Fi networks through the native bridge",
864
+ children: "Scan"
865
+ }
866
+ ),
867
+ /* @__PURE__ */ jsx(
868
+ ActionButton,
869
+ {
870
+ onClick: refreshWifiStatus,
871
+ disabled: !bridge || busy !== null,
872
+ agentId: "wifi-status",
873
+ agentLabel: "Refresh Wi-Fi Status",
874
+ agentGroup: "wifi",
875
+ agentDescription: "Refresh the current Wi-Fi connection status",
876
+ children: "Status"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsx(
880
+ ActionButton,
881
+ {
882
+ onClick: configureWifi,
883
+ disabled: !bridge || busy !== null,
884
+ agentId: "wifi-configure",
885
+ agentLabel: "Configure Wi-Fi",
886
+ agentGroup: "wifi",
887
+ agentDescription: "Send the entered SSID and password to the glasses",
888
+ children: "Configure"
889
+ }
890
+ ),
891
+ /* @__PURE__ */ jsx(
892
+ ActionButton,
893
+ {
894
+ onClick: requestWifiSetup,
895
+ disabled: !bridge || busy !== null,
896
+ agentId: "wifi-native-setup",
897
+ agentLabel: "Native Wi-Fi Setup",
898
+ agentGroup: "wifi",
899
+ agentDescription: "Launch the native bridge Wi-Fi setup flow",
900
+ children: "Setup"
901
+ }
902
+ )
903
+ ] }),
904
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-xs text-muted", children: wifiStatus }),
905
+ wifiNetworks.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-1", children: [
906
+ wifiNetworks.slice(0, VISIBLE_WIFI_LIMIT).map((network) => /* @__PURE__ */ jsx("span", { className: "px-1.5 py-1 text-xs text-muted", children: network }, network)),
907
+ wifiNetworks.length > VISIBLE_WIFI_LIMIT ? /* @__PURE__ */ jsxs("span", { className: "px-1.5 py-1 text-xs text-muted", children: [
908
+ "+",
909
+ wifiNetworks.length - VISIBLE_WIFI_LIMIT
910
+ ] }) : null
911
+ ] })
912
+ ] }),
913
+ /* @__PURE__ */ jsxs(Panel, { children: [
914
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
915
+ /* @__PURE__ */ jsx(BatteryCharging, { className: "h-4 w-4 text-accent" }),
916
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Report" })
917
+ ] }),
918
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 grid gap-2 text-xs", children: [
919
+ /* @__PURE__ */ jsx(ReportRow, { label: "Transport", value: report.transport ?? "none" }),
920
+ /* @__PURE__ */ jsx(ReportRow, { label: "Complete", value: report.ok ? "yes" : "no" }),
921
+ /* @__PURE__ */ jsx(
922
+ ReportRow,
923
+ {
924
+ label: "Serial",
925
+ value: report.serialNumber ?? "unknown"
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx(ReportRow, { label: "Next", value: report.nextAction ?? "none" }),
929
+ /* @__PURE__ */ jsx(
930
+ ReportRow,
931
+ {
932
+ label: "Missing",
933
+ value: report.missingEvidence.length === 0 ? "none" : String(report.missingEvidence.length)
934
+ }
935
+ ),
936
+ /* @__PURE__ */ jsx(ReportRow, { label: "Bridge", value: bridge ? "available" : "none" }),
937
+ /* @__PURE__ */ jsx(
938
+ ReportRow,
939
+ {
940
+ label: "State",
941
+ value: [physicalState, batteryState, deviceState].filter(Boolean).join(" / ") || "none"
942
+ }
943
+ ),
944
+ /* @__PURE__ */ jsx(
945
+ ReportRow,
946
+ {
947
+ label: "Battery",
948
+ value: formatBatteryLevels(batteryLevels)
949
+ }
950
+ ),
951
+ /* @__PURE__ */ jsx(ReportRow, { label: "Events", value: String(events.length) })
952
+ ] }),
953
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap gap-2", children: [
954
+ /* @__PURE__ */ jsxs(
955
+ ActionButton,
956
+ {
957
+ onClick: copyReport,
958
+ agentId: "report-copy",
959
+ agentLabel: "Copy Report",
960
+ agentGroup: "report",
961
+ agentDescription: "Copy the smartglasses diagnostics report to the clipboard",
962
+ children: [
963
+ /* @__PURE__ */ jsx(Clipboard, { className: "h-4 w-4" }),
964
+ "Copy"
965
+ ]
966
+ }
967
+ ),
968
+ /* @__PURE__ */ jsxs(
969
+ ActionButton,
970
+ {
971
+ onClick: downloadReport,
972
+ agentId: "report-download",
973
+ agentLabel: "Download Report",
974
+ agentGroup: "report",
975
+ agentDescription: "Download the smartglasses diagnostics report as JSON",
976
+ children: [
977
+ /* @__PURE__ */ jsx(Download, { className: "h-4 w-4" }),
978
+ "Download"
979
+ ]
980
+ }
981
+ )
982
+ ] })
983
+ ] }),
984
+ /* @__PURE__ */ jsxs(Panel, { children: [
985
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
986
+ /* @__PURE__ */ jsx(Settings2, { className: "h-4 w-4 text-accent" }),
987
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Events" })
988
+ ] }),
989
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 max-h-72 overflow-y-auto", children: [
990
+ events.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-xs text-muted", children: "None" }) : events.slice().reverse().slice(0, VISIBLE_EVENT_LIMIT).map((event) => /* @__PURE__ */ jsxs(
991
+ "div",
992
+ {
993
+ className: "px-1 py-2",
994
+ children: [
995
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-txt", children: event.type }),
996
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted", children: event.detail })
997
+ ]
998
+ },
999
+ `${event.at}:${event.type}:${event.detail}`
1000
+ )),
1001
+ events.length > VISIBLE_EVENT_LIMIT ? /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 text-xs text-muted", children: [
1002
+ "+",
1003
+ events.length - VISIBLE_EVENT_LIMIT,
1004
+ " older events"
1005
+ ] }) : null
1006
+ ] })
1007
+ ] })
1008
+ ] }),
1009
+ error && /* @__PURE__ */ jsx("div", { className: "mx-4 mb-4 px-1 py-2 text-xs text-destructive", children: error })
1010
+ ] });
1011
+ }
1012
+ function Panel({ children }) {
1013
+ return /* @__PURE__ */ jsx("div", { className: "py-2", children });
1014
+ }
1015
+ function ActionButton({
1016
+ children,
1017
+ disabled,
1018
+ onClick,
1019
+ agentId,
1020
+ agentLabel,
1021
+ agentGroup,
1022
+ agentDescription
1023
+ }) {
1024
+ const { ref, agentProps } = useAgentElement({
1025
+ id: agentId,
1026
+ role: "button",
1027
+ label: agentLabel,
1028
+ group: agentGroup,
1029
+ description: agentDescription,
1030
+ onActivate: () => void onClick()
1031
+ });
1032
+ return /* @__PURE__ */ jsx(
1033
+ "button",
1034
+ {
1035
+ ref,
1036
+ type: "button",
1037
+ onClick: () => void onClick(),
1038
+ disabled,
1039
+ "aria-label": agentLabel,
1040
+ className: "inline-flex h-9 items-center gap-2 px-3 text-sm font-medium hover:bg-muted/20 disabled:cursor-not-allowed disabled:opacity-50",
1041
+ ...agentProps,
1042
+ children
1043
+ }
1044
+ );
1045
+ }
1046
+ function PlatformTabButton({
1047
+ platformKey,
1048
+ isActive,
1049
+ onSelect
1050
+ }) {
1051
+ const label = PLATFORM_COPY[platformKey].label;
1052
+ const { ref, agentProps } = useAgentElement({
1053
+ id: `platform-tab-${platformKey}`,
1054
+ role: "tab",
1055
+ label: `${label} platform`,
1056
+ group: "platform-setup",
1057
+ status: isActive ? "active" : "inactive",
1058
+ description: `Show ${label} smartglasses pairing instructions`,
1059
+ onActivate: () => onSelect(platformKey)
1060
+ });
1061
+ return /* @__PURE__ */ jsx(
1062
+ "button",
1063
+ {
1064
+ ref,
1065
+ type: "button",
1066
+ onClick: () => onSelect(platformKey),
1067
+ "aria-current": isActive ? "page" : void 0,
1068
+ "aria-label": `${label} platform`,
1069
+ className: `h-8 px-2 text-xs font-medium transition-colors ${isActive ? "text-accent" : "text-muted hover:bg-muted/20 hover:text-txt"}`,
1070
+ ...agentProps,
1071
+ children: label
1072
+ }
1073
+ );
1074
+ }
1075
+ function StatusPill({ ok, label }) {
1076
+ return /* @__PURE__ */ jsxs(
1077
+ "span",
1078
+ {
1079
+ className: `inline-flex h-7 items-center gap-1.5 px-1.5 text-xs font-medium ${ok ? "text-green-700 dark:text-green-300" : "text-muted"}`,
1080
+ children: [
1081
+ ok ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(XCircle, { className: "h-3.5 w-3.5" }),
1082
+ label
1083
+ ]
1084
+ }
1085
+ );
1086
+ }
1087
+ function LensStatus({ side, state }) {
1088
+ const ok = state === "connected";
1089
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-1 py-2", children: [
1090
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1091
+ /* @__PURE__ */ jsx(Glasses, { className: "h-4 w-4 text-muted" }),
1092
+ /* @__PURE__ */ jsx("span", { className: "text-sm capitalize", children: side })
1093
+ ] }),
1094
+ /* @__PURE__ */ jsx(StatusPill, { ok, label: state })
1095
+ ] });
1096
+ }
1097
+ function HeadsetStateHint({
1098
+ physicalState,
1099
+ batteryState,
1100
+ deviceState
1101
+ }) {
1102
+ const chips = [
1103
+ { key: "physical", value: physicalState ?? "" },
1104
+ { key: "battery", value: batteryState ?? "" },
1105
+ { key: "device", value: deviceState ?? "" }
1106
+ ].filter((c) => c.value.length > 0);
1107
+ const blocked = isCradleOrChargingState(physicalState, batteryState);
1108
+ const ready = physicalState === "wearing";
1109
+ const tone = blocked ? "text-amber-800 dark:text-amber-200" : ready ? "text-green-700 dark:text-green-300" : "text-muted";
1110
+ const hint = blocked ? "Remove from charger and wear before validation." : ready ? "Ready for tap/audio validation." : "Wear state required for tap/audio validation.";
1111
+ return /* @__PURE__ */ jsxs("div", { className: `mt-3 px-1 py-2 ${tone}`, children: [
1112
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
1113
+ /* @__PURE__ */ jsx("span", { className: "text-2xs font-semibold uppercase tracking-wider opacity-70", children: "Headset" }),
1114
+ chips.length > 0 ? chips.map((chip) => /* @__PURE__ */ jsxs(
1115
+ "span",
1116
+ {
1117
+ className: "inline-flex items-center gap-1 px-1.5 py-0.5 text-2xs font-medium",
1118
+ children: [
1119
+ /* @__PURE__ */ jsx(
1120
+ "span",
1121
+ {
1122
+ className: "h-1 w-1 rounded-full bg-current opacity-70",
1123
+ "aria-hidden": true
1124
+ }
1125
+ ),
1126
+ chip.value
1127
+ ]
1128
+ },
1129
+ chip.key
1130
+ )) : /* @__PURE__ */ jsx("span", { className: "text-2xs italic opacity-70", children: "no state yet" })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-2xs leading-4 opacity-80", children: hint })
1133
+ ] });
1134
+ }
1135
+ function CheckRow({ ok, label }) {
1136
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-1 py-2 transition-colors", children: [
1137
+ /* @__PURE__ */ jsx("span", { className: `text-xs ${ok ? "font-medium text-txt" : "text-muted"}`, children: label }),
1138
+ /* @__PURE__ */ jsxs(
1139
+ "span",
1140
+ {
1141
+ className: `inline-flex items-center gap-1.5 rounded-full px-1.5 py-0.5 ${ok ? "text-green-600 dark:text-green-400" : "text-muted"}`,
1142
+ children: [
1143
+ /* @__PURE__ */ jsx(
1144
+ "span",
1145
+ {
1146
+ className: `h-1.5 w-1.5 rounded-full ${ok ? "bg-green-500" : "bg-muted/40"}`,
1147
+ "aria-hidden": true
1148
+ }
1149
+ ),
1150
+ ok ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Circle, { className: "h-3.5 w-3.5" })
1151
+ ]
1152
+ }
1153
+ )
1154
+ ] });
1155
+ }
1156
+ function ReportRow({ label, value }) {
1157
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
1158
+ /* @__PURE__ */ jsx("span", { className: "text-muted", children: label }),
1159
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium text-txt", children: value })
1160
+ ] });
1161
+ }
1162
+ function formatBatteryLevels(levels) {
1163
+ const left = levels.left === void 0 ? "unknown" : `${levels.left}%`;
1164
+ const right = levels.right === void 0 ? "unknown" : `${levels.right}%`;
1165
+ return `L ${left} / R ${right}`;
1166
+ }
1167
+ function labelForTest(id) {
1168
+ const labels = {
1169
+ headsetConnected: "Whole headset",
1170
+ init: "Init packets",
1171
+ display: "Display",
1172
+ serial: "Serial request",
1173
+ serialObserved: "Serial observed",
1174
+ settings: "Settings",
1175
+ microphone: "Microphone",
1176
+ micEnableWrite: "Mic enable write",
1177
+ micDisableWrite: "Mic disable write",
1178
+ tapMicEnable: "Tap mic enable",
1179
+ tapMicDisable: "Tap mic disable",
1180
+ audio: "Audio",
1181
+ transcript: "Transcript",
1182
+ eventStream: "Events"
1183
+ };
1184
+ return labels[id] ?? id;
1185
+ }
1186
+ export {
1187
+ SmartglassesView
1188
+ };
1189
+ //# sourceMappingURL=SmartglassesView.js.map