@blueharford/scrypted-spatial-awareness 0.3.0 → 0.4.0
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 +94 -3
- package/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +1869 -19
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +588 -1
- package/src/main.ts +318 -19
- package/src/models/training.ts +300 -0
- package/src/ui/training-html.ts +1007 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Training Mode UI - Mobile-optimized walkthrough interface
|
|
3
|
+
* Designed for phone use while walking around property
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const TRAINING_HTML = `<!DOCTYPE html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="UTF-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
11
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
12
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
13
|
+
<title>Spatial Awareness - Training Mode</title>
|
|
14
|
+
<style>
|
|
15
|
+
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
16
|
+
html, body { height: 100%; overflow: hidden; }
|
|
17
|
+
body {
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
19
|
+
background: #121212;
|
|
20
|
+
color: rgba(255,255,255,0.87);
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Header */
|
|
27
|
+
.header {
|
|
28
|
+
background: rgba(255,255,255,0.03);
|
|
29
|
+
padding: 12px 16px;
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: center;
|
|
33
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
34
|
+
}
|
|
35
|
+
.header h1 { font-size: 16px; font-weight: 500; }
|
|
36
|
+
.header-status {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 8px;
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
}
|
|
42
|
+
.status-badge {
|
|
43
|
+
padding: 4px 10px;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
font-size: 11px;
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
}
|
|
49
|
+
.status-badge.idle { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); }
|
|
50
|
+
.status-badge.active { background: #4fc3f7; color: #000; animation: pulse 2s infinite; }
|
|
51
|
+
.status-badge.paused { background: #ffb74d; color: #000; }
|
|
52
|
+
.status-badge.completed { background: #81c784; color: #000; }
|
|
53
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
|
54
|
+
|
|
55
|
+
/* Main content area */
|
|
56
|
+
.main-content {
|
|
57
|
+
flex: 1;
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
padding: 12px;
|
|
61
|
+
overflow-y: auto;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Camera detection card */
|
|
66
|
+
.detection-card {
|
|
67
|
+
background: rgba(255,255,255,0.03);
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
padding: 16px;
|
|
70
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
71
|
+
transition: all 0.3s;
|
|
72
|
+
}
|
|
73
|
+
.detection-card.detecting {
|
|
74
|
+
border-color: #4fc3f7;
|
|
75
|
+
background: rgba(79, 195, 247, 0.1);
|
|
76
|
+
}
|
|
77
|
+
.detection-card.in-transit {
|
|
78
|
+
border-color: #ffb74d;
|
|
79
|
+
background: rgba(255, 183, 77, 0.1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.detection-icon {
|
|
83
|
+
width: 64px;
|
|
84
|
+
height: 64px;
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
background: rgba(255,255,255,0.05);
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
margin: 0 auto 12px;
|
|
91
|
+
font-size: 28px;
|
|
92
|
+
}
|
|
93
|
+
.detection-card.detecting .detection-icon {
|
|
94
|
+
background: #4fc3f7;
|
|
95
|
+
animation: detectPulse 1.5s infinite;
|
|
96
|
+
}
|
|
97
|
+
@keyframes detectPulse {
|
|
98
|
+
0%, 100% { transform: scale(1); }
|
|
99
|
+
50% { transform: scale(1.03); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.detection-title {
|
|
103
|
+
font-size: 18px;
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
text-align: center;
|
|
106
|
+
margin-bottom: 4px;
|
|
107
|
+
}
|
|
108
|
+
.detection-subtitle {
|
|
109
|
+
font-size: 13px;
|
|
110
|
+
color: rgba(255,255,255,0.5);
|
|
111
|
+
text-align: center;
|
|
112
|
+
}
|
|
113
|
+
.detection-confidence {
|
|
114
|
+
margin-top: 8px;
|
|
115
|
+
text-align: center;
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
color: rgba(255,255,255,0.4);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Transit timer */
|
|
121
|
+
.transit-timer {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
margin-top: 12px;
|
|
127
|
+
padding: 10px;
|
|
128
|
+
background: rgba(0,0,0,0.2);
|
|
129
|
+
border-radius: 6px;
|
|
130
|
+
}
|
|
131
|
+
.transit-timer-icon { font-size: 18px; }
|
|
132
|
+
.transit-timer-text { font-size: 16px; font-weight: 500; }
|
|
133
|
+
.transit-timer-from { font-size: 12px; color: rgba(255,255,255,0.5); }
|
|
134
|
+
|
|
135
|
+
/* Stats grid */
|
|
136
|
+
.stats-grid {
|
|
137
|
+
display: grid;
|
|
138
|
+
grid-template-columns: repeat(3, 1fr);
|
|
139
|
+
gap: 8px;
|
|
140
|
+
}
|
|
141
|
+
.stat-item {
|
|
142
|
+
background: rgba(255,255,255,0.03);
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
padding: 12px 8px;
|
|
145
|
+
text-align: center;
|
|
146
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
147
|
+
}
|
|
148
|
+
.stat-value {
|
|
149
|
+
font-size: 24px;
|
|
150
|
+
font-weight: 500;
|
|
151
|
+
color: #4fc3f7;
|
|
152
|
+
}
|
|
153
|
+
.stat-label {
|
|
154
|
+
font-size: 10px;
|
|
155
|
+
color: rgba(255,255,255,0.4);
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
margin-top: 2px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Progress bar */
|
|
161
|
+
.progress-section {
|
|
162
|
+
background: rgba(255,255,255,0.03);
|
|
163
|
+
border-radius: 6px;
|
|
164
|
+
padding: 12px;
|
|
165
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
166
|
+
}
|
|
167
|
+
.progress-header {
|
|
168
|
+
display: flex;
|
|
169
|
+
justify-content: space-between;
|
|
170
|
+
margin-bottom: 8px;
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
}
|
|
173
|
+
.progress-bar {
|
|
174
|
+
height: 6px;
|
|
175
|
+
background: rgba(255,255,255,0.1);
|
|
176
|
+
border-radius: 3px;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
}
|
|
179
|
+
.progress-fill {
|
|
180
|
+
height: 100%;
|
|
181
|
+
background: #4fc3f7;
|
|
182
|
+
border-radius: 3px;
|
|
183
|
+
transition: width 0.5s;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Suggestions */
|
|
187
|
+
.suggestions-section {
|
|
188
|
+
background: rgba(79, 195, 247, 0.08);
|
|
189
|
+
border: 1px solid rgba(79, 195, 247, 0.2);
|
|
190
|
+
border-radius: 6px;
|
|
191
|
+
padding: 12px;
|
|
192
|
+
}
|
|
193
|
+
.suggestions-title {
|
|
194
|
+
font-size: 11px;
|
|
195
|
+
text-transform: uppercase;
|
|
196
|
+
color: #4fc3f7;
|
|
197
|
+
margin-bottom: 6px;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
}
|
|
200
|
+
.suggestion-item {
|
|
201
|
+
font-size: 13px;
|
|
202
|
+
padding: 6px 0;
|
|
203
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
204
|
+
color: rgba(255,255,255,0.7);
|
|
205
|
+
}
|
|
206
|
+
.suggestion-item:last-child { border-bottom: none; }
|
|
207
|
+
.suggestion-item::before {
|
|
208
|
+
content: "→ ";
|
|
209
|
+
color: #4fc3f7;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Action buttons */
|
|
213
|
+
.action-buttons {
|
|
214
|
+
display: flex;
|
|
215
|
+
gap: 8px;
|
|
216
|
+
padding: 8px 0;
|
|
217
|
+
}
|
|
218
|
+
.btn {
|
|
219
|
+
flex: 1;
|
|
220
|
+
padding: 14px 16px;
|
|
221
|
+
border: none;
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
font-size: 14px;
|
|
224
|
+
font-weight: 500;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
transition: all 0.2s;
|
|
227
|
+
display: flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: center;
|
|
230
|
+
gap: 6px;
|
|
231
|
+
}
|
|
232
|
+
.btn:active { transform: scale(0.98); }
|
|
233
|
+
.btn-primary { background: #4fc3f7; color: #000; }
|
|
234
|
+
.btn-secondary { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.87); }
|
|
235
|
+
.btn-danger { background: #ef5350; color: #fff; }
|
|
236
|
+
.btn-warning { background: #ffb74d; color: #000; }
|
|
237
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
238
|
+
|
|
239
|
+
/* Full-width button */
|
|
240
|
+
.btn-full { flex: none; width: 100%; }
|
|
241
|
+
|
|
242
|
+
/* Mark landmark/structure panel */
|
|
243
|
+
.mark-panel {
|
|
244
|
+
background: rgba(255,255,255,0.03);
|
|
245
|
+
border-radius: 6px;
|
|
246
|
+
padding: 16px;
|
|
247
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
248
|
+
}
|
|
249
|
+
.mark-panel-title {
|
|
250
|
+
font-size: 14px;
|
|
251
|
+
font-weight: 500;
|
|
252
|
+
margin-bottom: 12px;
|
|
253
|
+
}
|
|
254
|
+
.mark-type-grid {
|
|
255
|
+
display: grid;
|
|
256
|
+
grid-template-columns: repeat(4, 1fr);
|
|
257
|
+
gap: 6px;
|
|
258
|
+
margin-bottom: 12px;
|
|
259
|
+
}
|
|
260
|
+
.mark-type-btn {
|
|
261
|
+
padding: 10px 6px;
|
|
262
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
263
|
+
border-radius: 6px;
|
|
264
|
+
background: transparent;
|
|
265
|
+
color: rgba(255,255,255,0.7);
|
|
266
|
+
font-size: 10px;
|
|
267
|
+
text-align: center;
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
transition: all 0.2s;
|
|
270
|
+
}
|
|
271
|
+
.mark-type-btn.selected {
|
|
272
|
+
border-color: #4fc3f7;
|
|
273
|
+
background: rgba(79, 195, 247, 0.15);
|
|
274
|
+
color: #fff;
|
|
275
|
+
}
|
|
276
|
+
.mark-type-btn .icon { font-size: 18px; margin-bottom: 2px; display: block; }
|
|
277
|
+
|
|
278
|
+
/* Input field */
|
|
279
|
+
.input-group { margin-bottom: 12px; }
|
|
280
|
+
.input-group label {
|
|
281
|
+
display: block;
|
|
282
|
+
font-size: 12px;
|
|
283
|
+
color: rgba(255,255,255,0.5);
|
|
284
|
+
margin-bottom: 4px;
|
|
285
|
+
}
|
|
286
|
+
.input-group input {
|
|
287
|
+
width: 100%;
|
|
288
|
+
padding: 12px;
|
|
289
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
290
|
+
border-radius: 6px;
|
|
291
|
+
background: rgba(0,0,0,0.2);
|
|
292
|
+
color: #fff;
|
|
293
|
+
font-size: 14px;
|
|
294
|
+
}
|
|
295
|
+
.input-group input:focus {
|
|
296
|
+
outline: none;
|
|
297
|
+
border-color: #4fc3f7;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* History list */
|
|
301
|
+
.history-section {
|
|
302
|
+
background: rgba(255,255,255,0.03);
|
|
303
|
+
border-radius: 6px;
|
|
304
|
+
padding: 12px;
|
|
305
|
+
max-height: 180px;
|
|
306
|
+
overflow-y: auto;
|
|
307
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
308
|
+
}
|
|
309
|
+
.history-title {
|
|
310
|
+
font-size: 13px;
|
|
311
|
+
font-weight: 500;
|
|
312
|
+
margin-bottom: 8px;
|
|
313
|
+
display: flex;
|
|
314
|
+
justify-content: space-between;
|
|
315
|
+
}
|
|
316
|
+
.history-item {
|
|
317
|
+
padding: 8px;
|
|
318
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
319
|
+
font-size: 13px;
|
|
320
|
+
}
|
|
321
|
+
.history-item:last-child { border-bottom: none; }
|
|
322
|
+
.history-item-time {
|
|
323
|
+
font-size: 10px;
|
|
324
|
+
color: rgba(255,255,255,0.4);
|
|
325
|
+
}
|
|
326
|
+
.history-item-camera { color: #4fc3f7; font-weight: 500; }
|
|
327
|
+
.history-item-transit { color: #ffb74d; }
|
|
328
|
+
|
|
329
|
+
/* Bottom action bar */
|
|
330
|
+
.bottom-bar {
|
|
331
|
+
background: rgba(255,255,255,0.03);
|
|
332
|
+
padding: 12px 16px;
|
|
333
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
|
334
|
+
border-top: 1px solid rgba(255,255,255,0.08);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Tabs */
|
|
338
|
+
.tabs {
|
|
339
|
+
display: flex;
|
|
340
|
+
background: rgba(0,0,0,0.2);
|
|
341
|
+
border-radius: 6px;
|
|
342
|
+
padding: 3px;
|
|
343
|
+
margin-bottom: 12px;
|
|
344
|
+
}
|
|
345
|
+
.tab {
|
|
346
|
+
flex: 1;
|
|
347
|
+
padding: 10px;
|
|
348
|
+
border: none;
|
|
349
|
+
background: transparent;
|
|
350
|
+
color: rgba(255,255,255,0.5);
|
|
351
|
+
font-size: 13px;
|
|
352
|
+
font-weight: 500;
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
border-radius: 4px;
|
|
355
|
+
transition: all 0.2s;
|
|
356
|
+
}
|
|
357
|
+
.tab.active {
|
|
358
|
+
background: rgba(255,255,255,0.08);
|
|
359
|
+
color: rgba(255,255,255,0.87);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Tab content */
|
|
363
|
+
.tab-content { display: none; }
|
|
364
|
+
.tab-content.active { display: block; }
|
|
365
|
+
|
|
366
|
+
/* Apply results modal */
|
|
367
|
+
.modal-overlay {
|
|
368
|
+
display: none;
|
|
369
|
+
position: fixed;
|
|
370
|
+
top: 0;
|
|
371
|
+
left: 0;
|
|
372
|
+
right: 0;
|
|
373
|
+
bottom: 0;
|
|
374
|
+
background: rgba(0,0,0,0.85);
|
|
375
|
+
z-index: 100;
|
|
376
|
+
align-items: center;
|
|
377
|
+
justify-content: center;
|
|
378
|
+
padding: 16px;
|
|
379
|
+
}
|
|
380
|
+
.modal-overlay.active { display: flex; }
|
|
381
|
+
.modal {
|
|
382
|
+
background: #1e1e1e;
|
|
383
|
+
border-radius: 8px;
|
|
384
|
+
padding: 20px;
|
|
385
|
+
max-width: 360px;
|
|
386
|
+
width: 100%;
|
|
387
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
388
|
+
}
|
|
389
|
+
.modal h2 { font-size: 18px; margin-bottom: 12px; font-weight: 500; }
|
|
390
|
+
.modal-result-item {
|
|
391
|
+
display: flex;
|
|
392
|
+
justify-content: space-between;
|
|
393
|
+
padding: 8px 0;
|
|
394
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
395
|
+
font-size: 13px;
|
|
396
|
+
}
|
|
397
|
+
.modal-result-value { color: #4fc3f7; font-weight: 500; }
|
|
398
|
+
.modal-buttons { display: flex; gap: 8px; margin-top: 16px; }
|
|
399
|
+
|
|
400
|
+
/* Idle state */
|
|
401
|
+
.idle-content {
|
|
402
|
+
flex: 1;
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
align-items: center;
|
|
406
|
+
justify-content: center;
|
|
407
|
+
text-align: center;
|
|
408
|
+
padding: 32px 16px;
|
|
409
|
+
}
|
|
410
|
+
.idle-icon {
|
|
411
|
+
font-size: 56px;
|
|
412
|
+
margin-bottom: 16px;
|
|
413
|
+
opacity: 0.7;
|
|
414
|
+
}
|
|
415
|
+
.idle-title {
|
|
416
|
+
font-size: 20px;
|
|
417
|
+
font-weight: 500;
|
|
418
|
+
margin-bottom: 8px;
|
|
419
|
+
}
|
|
420
|
+
.idle-desc {
|
|
421
|
+
font-size: 14px;
|
|
422
|
+
color: rgba(255,255,255,0.5);
|
|
423
|
+
max-width: 280px;
|
|
424
|
+
line-height: 1.5;
|
|
425
|
+
}
|
|
426
|
+
.idle-instructions {
|
|
427
|
+
margin-top: 24px;
|
|
428
|
+
text-align: left;
|
|
429
|
+
background: rgba(255,255,255,0.03);
|
|
430
|
+
border-radius: 6px;
|
|
431
|
+
padding: 16px;
|
|
432
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
433
|
+
}
|
|
434
|
+
.idle-instructions h3 {
|
|
435
|
+
font-size: 12px;
|
|
436
|
+
margin-bottom: 10px;
|
|
437
|
+
color: #4fc3f7;
|
|
438
|
+
font-weight: 500;
|
|
439
|
+
}
|
|
440
|
+
.idle-instructions ol {
|
|
441
|
+
padding-left: 18px;
|
|
442
|
+
font-size: 13px;
|
|
443
|
+
line-height: 1.8;
|
|
444
|
+
color: rgba(255,255,255,0.6);
|
|
445
|
+
}
|
|
446
|
+
</style>
|
|
447
|
+
</head>
|
|
448
|
+
<body>
|
|
449
|
+
<div class="header">
|
|
450
|
+
<h1>Training Mode</h1>
|
|
451
|
+
<div class="header-status">
|
|
452
|
+
<span class="status-badge" id="status-badge">Idle</span>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<!-- Idle State -->
|
|
457
|
+
<div class="main-content" id="idle-content">
|
|
458
|
+
<div class="idle-content">
|
|
459
|
+
<div class="idle-icon">🚶</div>
|
|
460
|
+
<div class="idle-title">Train Your System</div>
|
|
461
|
+
<div class="idle-desc">Walk around your property to teach the system about camera positions, transit times, and landmarks.</div>
|
|
462
|
+
|
|
463
|
+
<div class="idle-instructions">
|
|
464
|
+
<h3>How it works:</h3>
|
|
465
|
+
<ol>
|
|
466
|
+
<li>Tap <strong>Start Training</strong> below</li>
|
|
467
|
+
<li>Walk to each camera on your property</li>
|
|
468
|
+
<li>The system detects you automatically</li>
|
|
469
|
+
<li>Mark landmarks as you encounter them</li>
|
|
470
|
+
<li>End training when you're done</li>
|
|
471
|
+
</ol>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<div class="bottom-bar">
|
|
476
|
+
<button class="btn btn-primary btn-full" onclick="startTraining()">
|
|
477
|
+
▶ Start Training
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<!-- Active Training State -->
|
|
483
|
+
<div class="main-content" id="active-content" style="display: none;">
|
|
484
|
+
<!-- Detection Card -->
|
|
485
|
+
<div class="detection-card" id="detection-card">
|
|
486
|
+
<div class="detection-icon" id="detection-icon">👤</div>
|
|
487
|
+
<div class="detection-title" id="detection-title">Waiting for detection...</div>
|
|
488
|
+
<div class="detection-subtitle" id="detection-subtitle">Walk to any camera to begin</div>
|
|
489
|
+
<div class="detection-confidence" id="detection-confidence"></div>
|
|
490
|
+
|
|
491
|
+
<div class="transit-timer" id="transit-timer" style="display: none;">
|
|
492
|
+
<span class="transit-timer-icon">⏱</span>
|
|
493
|
+
<span class="transit-timer-text" id="transit-time">0s</span>
|
|
494
|
+
<span class="transit-timer-from" id="transit-from"></span>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<!-- Tabs -->
|
|
499
|
+
<div class="tabs">
|
|
500
|
+
<button class="tab active" onclick="switchTab('status')">Status</button>
|
|
501
|
+
<button class="tab" onclick="switchTab('mark')">Mark</button>
|
|
502
|
+
<button class="tab" onclick="switchTab('history')">History</button>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<!-- Status Tab -->
|
|
506
|
+
<div class="tab-content active" id="tab-status">
|
|
507
|
+
<!-- Stats -->
|
|
508
|
+
<div class="stats-grid">
|
|
509
|
+
<div class="stat-item">
|
|
510
|
+
<div class="stat-value" id="stat-cameras">0</div>
|
|
511
|
+
<div class="stat-label">Cameras</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="stat-item">
|
|
514
|
+
<div class="stat-value" id="stat-transits">0</div>
|
|
515
|
+
<div class="stat-label">Transits</div>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="stat-item">
|
|
518
|
+
<div class="stat-value" id="stat-landmarks">0</div>
|
|
519
|
+
<div class="stat-label">Landmarks</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<!-- Progress -->
|
|
524
|
+
<div class="progress-section" style="margin-top: 15px;">
|
|
525
|
+
<div class="progress-header">
|
|
526
|
+
<span>Coverage</span>
|
|
527
|
+
<span id="progress-percent">0%</span>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="progress-bar">
|
|
530
|
+
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<!-- Suggestions -->
|
|
535
|
+
<div class="suggestions-section" style="margin-top: 15px;" id="suggestions-section">
|
|
536
|
+
<div class="suggestions-title">Suggestions</div>
|
|
537
|
+
<div id="suggestions-list">
|
|
538
|
+
<div class="suggestion-item">Start walking to a camera</div>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<!-- Mark Tab -->
|
|
544
|
+
<div class="tab-content" id="tab-mark">
|
|
545
|
+
<div class="mark-panel">
|
|
546
|
+
<div class="mark-panel-title">Mark a Landmark</div>
|
|
547
|
+
<div class="mark-type-grid" id="landmark-type-grid">
|
|
548
|
+
<button class="mark-type-btn selected" data-type="mailbox" onclick="selectLandmarkType('mailbox')">
|
|
549
|
+
<span class="icon">📬</span>
|
|
550
|
+
Mailbox
|
|
551
|
+
</button>
|
|
552
|
+
<button class="mark-type-btn" data-type="garage" onclick="selectLandmarkType('garage')">
|
|
553
|
+
<span class="icon">🏠</span>
|
|
554
|
+
Garage
|
|
555
|
+
</button>
|
|
556
|
+
<button class="mark-type-btn" data-type="shed" onclick="selectLandmarkType('shed')">
|
|
557
|
+
<span class="icon">🏚</span>
|
|
558
|
+
Shed
|
|
559
|
+
</button>
|
|
560
|
+
<button class="mark-type-btn" data-type="tree" onclick="selectLandmarkType('tree')">
|
|
561
|
+
<span class="icon">🌳</span>
|
|
562
|
+
Tree
|
|
563
|
+
</button>
|
|
564
|
+
<button class="mark-type-btn" data-type="gate" onclick="selectLandmarkType('gate')">
|
|
565
|
+
<span class="icon">🚪</span>
|
|
566
|
+
Gate
|
|
567
|
+
</button>
|
|
568
|
+
<button class="mark-type-btn" data-type="driveway" onclick="selectLandmarkType('driveway')">
|
|
569
|
+
<span class="icon">🛣</span>
|
|
570
|
+
Driveway
|
|
571
|
+
</button>
|
|
572
|
+
<button class="mark-type-btn" data-type="pool" onclick="selectLandmarkType('pool')">
|
|
573
|
+
<span class="icon">🏊</span>
|
|
574
|
+
Pool
|
|
575
|
+
</button>
|
|
576
|
+
<button class="mark-type-btn" data-type="other" onclick="selectLandmarkType('other')">
|
|
577
|
+
<span class="icon">📍</span>
|
|
578
|
+
Other
|
|
579
|
+
</button>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div class="input-group">
|
|
583
|
+
<label>Landmark Name</label>
|
|
584
|
+
<input type="text" id="landmark-name" placeholder="e.g., Front Mailbox">
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
<button class="btn btn-primary btn-full" onclick="markLandmark()">
|
|
588
|
+
📍 Mark Landmark Here
|
|
589
|
+
</button>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<!-- History Tab -->
|
|
594
|
+
<div class="tab-content" id="tab-history">
|
|
595
|
+
<div class="history-section">
|
|
596
|
+
<div class="history-title">
|
|
597
|
+
<span>Recent Activity</span>
|
|
598
|
+
<span id="history-count">0 events</span>
|
|
599
|
+
</div>
|
|
600
|
+
<div id="history-list">
|
|
601
|
+
<div class="history-item" style="color: rgba(255,255,255,0.4); text-align: center;">
|
|
602
|
+
No activity yet
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<!-- Bottom Actions -->
|
|
609
|
+
<div class="bottom-bar">
|
|
610
|
+
<div class="action-buttons">
|
|
611
|
+
<button class="btn btn-warning" id="pause-btn" onclick="togglePause()">
|
|
612
|
+
⏸ Pause
|
|
613
|
+
</button>
|
|
614
|
+
<button class="btn btn-danger" onclick="endTraining()">
|
|
615
|
+
⏹ End
|
|
616
|
+
</button>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<!-- Completed State -->
|
|
622
|
+
<div class="main-content" id="completed-content" style="display: none;">
|
|
623
|
+
<div class="idle-content">
|
|
624
|
+
<div class="idle-icon">✅</div>
|
|
625
|
+
<div class="idle-title">Training Complete!</div>
|
|
626
|
+
<div class="idle-desc">Review the results and apply them to your topology.</div>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<!-- Final Stats -->
|
|
630
|
+
<div class="stats-grid">
|
|
631
|
+
<div class="stat-item">
|
|
632
|
+
<div class="stat-value" id="final-cameras">0</div>
|
|
633
|
+
<div class="stat-label">Cameras</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="stat-item">
|
|
636
|
+
<div class="stat-value" id="final-transits">0</div>
|
|
637
|
+
<div class="stat-label">Transits</div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="stat-item">
|
|
640
|
+
<div class="stat-value" id="final-landmarks">0</div>
|
|
641
|
+
<div class="stat-label">Landmarks</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<div class="stats-grid" style="margin-top: 10px;">
|
|
646
|
+
<div class="stat-item">
|
|
647
|
+
<div class="stat-value" id="final-overlaps">0</div>
|
|
648
|
+
<div class="stat-label">Overlaps</div>
|
|
649
|
+
</div>
|
|
650
|
+
<div class="stat-item">
|
|
651
|
+
<div class="stat-value" id="final-avg-transit">0s</div>
|
|
652
|
+
<div class="stat-label">Avg Transit</div>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="stat-item">
|
|
655
|
+
<div class="stat-value" id="final-coverage">0%</div>
|
|
656
|
+
<div class="stat-label">Coverage</div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
<div class="bottom-bar" style="margin-top: auto;">
|
|
661
|
+
<div class="action-buttons">
|
|
662
|
+
<button class="btn btn-secondary" onclick="resetTraining()">
|
|
663
|
+
↻ Start Over
|
|
664
|
+
</button>
|
|
665
|
+
<button class="btn btn-primary" onclick="applyTraining()">
|
|
666
|
+
✓ Apply Results
|
|
667
|
+
</button>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<!-- Apply Results Modal -->
|
|
673
|
+
<div class="modal-overlay" id="results-modal">
|
|
674
|
+
<div class="modal">
|
|
675
|
+
<h2>Training Applied!</h2>
|
|
676
|
+
<div id="results-content">
|
|
677
|
+
<div class="modal-result-item">
|
|
678
|
+
<span>Connections Created</span>
|
|
679
|
+
<span class="modal-result-value" id="result-connections">0</span>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="modal-result-item">
|
|
682
|
+
<span>Connections Updated</span>
|
|
683
|
+
<span class="modal-result-value" id="result-updated">0</span>
|
|
684
|
+
</div>
|
|
685
|
+
<div class="modal-result-item">
|
|
686
|
+
<span>Landmarks Added</span>
|
|
687
|
+
<span class="modal-result-value" id="result-landmarks">0</span>
|
|
688
|
+
</div>
|
|
689
|
+
<div class="modal-result-item">
|
|
690
|
+
<span>Zones Created</span>
|
|
691
|
+
<span class="modal-result-value" id="result-zones">0</span>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="modal-buttons">
|
|
695
|
+
<button class="btn btn-secondary" style="flex: 1;" onclick="closeResultsModal()">Close</button>
|
|
696
|
+
<button class="btn btn-primary" style="flex: 1;" onclick="openEditor()">Open Editor</button>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
<script>
|
|
702
|
+
let trainingState = 'idle'; // idle, active, paused, completed
|
|
703
|
+
let session = null;
|
|
704
|
+
let pollInterval = null;
|
|
705
|
+
let transitInterval = null;
|
|
706
|
+
let selectedLandmarkType = 'mailbox';
|
|
707
|
+
let historyItems = [];
|
|
708
|
+
|
|
709
|
+
// Initialize
|
|
710
|
+
async function init() {
|
|
711
|
+
// Check if there's an existing session
|
|
712
|
+
const status = await fetchTrainingStatus();
|
|
713
|
+
if (status && (status.state === 'active' || status.state === 'paused')) {
|
|
714
|
+
session = status;
|
|
715
|
+
trainingState = status.state;
|
|
716
|
+
updateUI();
|
|
717
|
+
startPolling();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// API calls
|
|
722
|
+
async function fetchTrainingStatus() {
|
|
723
|
+
try {
|
|
724
|
+
const response = await fetch('../api/training/status');
|
|
725
|
+
if (response.ok) {
|
|
726
|
+
return await response.json();
|
|
727
|
+
}
|
|
728
|
+
} catch (e) { console.error('Failed to fetch status:', e); }
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function startTraining() {
|
|
733
|
+
try {
|
|
734
|
+
const response = await fetch('../api/training/start', { method: 'POST' });
|
|
735
|
+
if (response.ok) {
|
|
736
|
+
session = await response.json();
|
|
737
|
+
trainingState = 'active';
|
|
738
|
+
updateUI();
|
|
739
|
+
startPolling();
|
|
740
|
+
addHistoryItem('Training started', 'start');
|
|
741
|
+
}
|
|
742
|
+
} catch (e) {
|
|
743
|
+
console.error('Failed to start training:', e);
|
|
744
|
+
alert('Failed to start training. Please try again.');
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function togglePause() {
|
|
749
|
+
const endpoint = trainingState === 'active' ? 'pause' : 'resume';
|
|
750
|
+
try {
|
|
751
|
+
const response = await fetch('../api/training/' + endpoint, { method: 'POST' });
|
|
752
|
+
if (response.ok) {
|
|
753
|
+
trainingState = trainingState === 'active' ? 'paused' : 'active';
|
|
754
|
+
updateUI();
|
|
755
|
+
addHistoryItem('Training ' + (trainingState === 'paused' ? 'paused' : 'resumed'), 'control');
|
|
756
|
+
}
|
|
757
|
+
} catch (e) { console.error('Failed to toggle pause:', e); }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function endTraining() {
|
|
761
|
+
if (!confirm('End training session?')) return;
|
|
762
|
+
try {
|
|
763
|
+
const response = await fetch('../api/training/end', { method: 'POST' });
|
|
764
|
+
if (response.ok) {
|
|
765
|
+
session = await response.json();
|
|
766
|
+
trainingState = 'completed';
|
|
767
|
+
stopPolling();
|
|
768
|
+
updateUI();
|
|
769
|
+
}
|
|
770
|
+
} catch (e) { console.error('Failed to end training:', e); }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function applyTraining() {
|
|
774
|
+
try {
|
|
775
|
+
const response = await fetch('../api/training/apply', { method: 'POST' });
|
|
776
|
+
if (response.ok) {
|
|
777
|
+
const result = await response.json();
|
|
778
|
+
document.getElementById('result-connections').textContent = result.connectionsCreated;
|
|
779
|
+
document.getElementById('result-updated').textContent = result.connectionsUpdated;
|
|
780
|
+
document.getElementById('result-landmarks').textContent = result.landmarksAdded;
|
|
781
|
+
document.getElementById('result-zones').textContent = result.zonesCreated;
|
|
782
|
+
document.getElementById('results-modal').classList.add('active');
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
console.error('Failed to apply training:', e);
|
|
786
|
+
alert('Failed to apply training results.');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function closeResultsModal() {
|
|
791
|
+
document.getElementById('results-modal').classList.remove('active');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function openEditor() {
|
|
795
|
+
window.location.href = '../ui/editor';
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function resetTraining() {
|
|
799
|
+
trainingState = 'idle';
|
|
800
|
+
session = null;
|
|
801
|
+
historyItems = [];
|
|
802
|
+
updateUI();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function markLandmark() {
|
|
806
|
+
const name = document.getElementById('landmark-name').value.trim();
|
|
807
|
+
if (!name) {
|
|
808
|
+
alert('Please enter a landmark name');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const currentCameraId = session?.currentCamera?.id;
|
|
813
|
+
const visibleFromCameras = currentCameraId ? [currentCameraId] : [];
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
const response = await fetch('../api/training/landmark', {
|
|
817
|
+
method: 'POST',
|
|
818
|
+
headers: { 'Content-Type': 'application/json' },
|
|
819
|
+
body: JSON.stringify({
|
|
820
|
+
name,
|
|
821
|
+
type: selectedLandmarkType,
|
|
822
|
+
visibleFromCameras,
|
|
823
|
+
position: { x: 50, y: 50 }, // Will be refined when applied
|
|
824
|
+
})
|
|
825
|
+
});
|
|
826
|
+
if (response.ok) {
|
|
827
|
+
document.getElementById('landmark-name').value = '';
|
|
828
|
+
addHistoryItem('Marked: ' + name + ' (' + selectedLandmarkType + ')', 'landmark');
|
|
829
|
+
// Refresh status
|
|
830
|
+
const status = await fetchTrainingStatus();
|
|
831
|
+
if (status) {
|
|
832
|
+
session = status;
|
|
833
|
+
updateStatsUI();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch (e) { console.error('Failed to mark landmark:', e); }
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function selectLandmarkType(type) {
|
|
840
|
+
selectedLandmarkType = type;
|
|
841
|
+
document.querySelectorAll('.mark-type-btn').forEach(btn => {
|
|
842
|
+
btn.classList.toggle('selected', btn.dataset.type === type);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Polling
|
|
847
|
+
function startPolling() {
|
|
848
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
849
|
+
pollInterval = setInterval(async () => {
|
|
850
|
+
const status = await fetchTrainingStatus();
|
|
851
|
+
if (status) {
|
|
852
|
+
session = status;
|
|
853
|
+
updateDetectionUI();
|
|
854
|
+
updateStatsUI();
|
|
855
|
+
updateSuggestionsUI();
|
|
856
|
+
}
|
|
857
|
+
}, 1000);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function stopPolling() {
|
|
861
|
+
if (pollInterval) {
|
|
862
|
+
clearInterval(pollInterval);
|
|
863
|
+
pollInterval = null;
|
|
864
|
+
}
|
|
865
|
+
if (transitInterval) {
|
|
866
|
+
clearInterval(transitInterval);
|
|
867
|
+
transitInterval = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// UI Updates
|
|
872
|
+
function updateUI() {
|
|
873
|
+
// Show/hide content sections
|
|
874
|
+
document.getElementById('idle-content').style.display = trainingState === 'idle' ? 'flex' : 'none';
|
|
875
|
+
document.getElementById('active-content').style.display = (trainingState === 'active' || trainingState === 'paused') ? 'flex' : 'none';
|
|
876
|
+
document.getElementById('completed-content').style.display = trainingState === 'completed' ? 'flex' : 'none';
|
|
877
|
+
|
|
878
|
+
// Update status badge
|
|
879
|
+
const badge = document.getElementById('status-badge');
|
|
880
|
+
badge.textContent = trainingState.charAt(0).toUpperCase() + trainingState.slice(1);
|
|
881
|
+
badge.className = 'status-badge ' + trainingState;
|
|
882
|
+
|
|
883
|
+
// Update pause button
|
|
884
|
+
const pauseBtn = document.getElementById('pause-btn');
|
|
885
|
+
if (pauseBtn) {
|
|
886
|
+
pauseBtn.innerHTML = trainingState === 'paused' ? '▶ Resume' : '⏸ Pause';
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Update completed stats
|
|
890
|
+
if (trainingState === 'completed' && session) {
|
|
891
|
+
document.getElementById('final-cameras').textContent = session.stats?.camerasVisited || 0;
|
|
892
|
+
document.getElementById('final-transits').textContent = session.stats?.transitsRecorded || 0;
|
|
893
|
+
document.getElementById('final-landmarks').textContent = session.stats?.landmarksMarked || 0;
|
|
894
|
+
document.getElementById('final-overlaps').textContent = session.stats?.overlapsDetected || 0;
|
|
895
|
+
document.getElementById('final-avg-transit').textContent = (session.stats?.averageTransitTime || 0) + 's';
|
|
896
|
+
document.getElementById('final-coverage').textContent = (session.stats?.coveragePercentage || 0) + '%';
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function updateDetectionUI() {
|
|
901
|
+
if (!session) return;
|
|
902
|
+
|
|
903
|
+
const card = document.getElementById('detection-card');
|
|
904
|
+
const icon = document.getElementById('detection-icon');
|
|
905
|
+
const title = document.getElementById('detection-title');
|
|
906
|
+
const subtitle = document.getElementById('detection-subtitle');
|
|
907
|
+
const confidence = document.getElementById('detection-confidence');
|
|
908
|
+
const transitTimer = document.getElementById('transit-timer');
|
|
909
|
+
|
|
910
|
+
if (session.currentCamera) {
|
|
911
|
+
// Detected on a camera
|
|
912
|
+
card.className = 'detection-card detecting';
|
|
913
|
+
icon.textContent = '📷';
|
|
914
|
+
title.textContent = session.currentCamera.name;
|
|
915
|
+
subtitle.textContent = 'You are visible on this camera';
|
|
916
|
+
confidence.textContent = 'Confidence: ' + Math.round(session.currentCamera.confidence * 100) + '%';
|
|
917
|
+
transitTimer.style.display = 'none';
|
|
918
|
+
|
|
919
|
+
// Check for new camera detection to add to history
|
|
920
|
+
const lastHistoryCamera = historyItems.find(h => h.type === 'camera');
|
|
921
|
+
if (!lastHistoryCamera || lastHistoryCamera.cameraId !== session.currentCamera.id) {
|
|
922
|
+
addHistoryItem('Detected on: ' + session.currentCamera.name, 'camera', session.currentCamera.id);
|
|
923
|
+
}
|
|
924
|
+
} else if (session.activeTransit) {
|
|
925
|
+
// In transit
|
|
926
|
+
card.className = 'detection-card in-transit';
|
|
927
|
+
icon.textContent = '🚶';
|
|
928
|
+
title.textContent = 'In Transit';
|
|
929
|
+
subtitle.textContent = 'Walking to next camera...';
|
|
930
|
+
confidence.textContent = '';
|
|
931
|
+
transitTimer.style.display = 'flex';
|
|
932
|
+
document.getElementById('transit-from').textContent = 'from ' + session.activeTransit.fromCameraName;
|
|
933
|
+
|
|
934
|
+
// Start transit timer if not already running
|
|
935
|
+
if (!transitInterval) {
|
|
936
|
+
transitInterval = setInterval(() => {
|
|
937
|
+
if (session?.activeTransit) {
|
|
938
|
+
document.getElementById('transit-time').textContent = session.activeTransit.elapsedSeconds + 's';
|
|
939
|
+
}
|
|
940
|
+
}, 1000);
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
// Waiting
|
|
944
|
+
card.className = 'detection-card';
|
|
945
|
+
icon.textContent = '👤';
|
|
946
|
+
title.textContent = 'Waiting for detection...';
|
|
947
|
+
subtitle.textContent = 'Walk to any camera to begin';
|
|
948
|
+
confidence.textContent = '';
|
|
949
|
+
transitTimer.style.display = 'none';
|
|
950
|
+
|
|
951
|
+
if (transitInterval) {
|
|
952
|
+
clearInterval(transitInterval);
|
|
953
|
+
transitInterval = null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function updateStatsUI() {
|
|
959
|
+
if (!session?.stats) return;
|
|
960
|
+
|
|
961
|
+
document.getElementById('stat-cameras').textContent = session.stats.camerasVisited;
|
|
962
|
+
document.getElementById('stat-transits').textContent = session.stats.transitsRecorded;
|
|
963
|
+
document.getElementById('stat-landmarks').textContent = session.stats.landmarksMarked;
|
|
964
|
+
document.getElementById('progress-percent').textContent = session.stats.coveragePercentage + '%';
|
|
965
|
+
document.getElementById('progress-fill').style.width = session.stats.coveragePercentage + '%';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function updateSuggestionsUI() {
|
|
969
|
+
if (!session?.suggestions || session.suggestions.length === 0) return;
|
|
970
|
+
|
|
971
|
+
const list = document.getElementById('suggestions-list');
|
|
972
|
+
list.innerHTML = session.suggestions.map(s =>
|
|
973
|
+
'<div class="suggestion-item">' + s + '</div>'
|
|
974
|
+
).join('');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function addHistoryItem(text, type, cameraId) {
|
|
978
|
+
const time = new Date().toLocaleTimeString();
|
|
979
|
+
historyItems.unshift({ text, type, time, cameraId });
|
|
980
|
+
if (historyItems.length > 50) historyItems.pop();
|
|
981
|
+
|
|
982
|
+
const list = document.getElementById('history-list');
|
|
983
|
+
document.getElementById('history-count').textContent = historyItems.length + ' events';
|
|
984
|
+
|
|
985
|
+
list.innerHTML = historyItems.map(item => {
|
|
986
|
+
let className = '';
|
|
987
|
+
if (item.type === 'camera') className = 'history-item-camera';
|
|
988
|
+
if (item.type === 'transit') className = 'history-item-transit';
|
|
989
|
+
return '<div class="history-item"><span class="' + className + '">' + item.text + '</span>' +
|
|
990
|
+
'<div class="history-item-time">' + item.time + '</div></div>';
|
|
991
|
+
}).join('');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function switchTab(tabName) {
|
|
995
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
996
|
+
tab.classList.toggle('active', tab.textContent.toLowerCase() === tabName);
|
|
997
|
+
});
|
|
998
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
999
|
+
content.classList.toggle('active', content.id === 'tab-' + tabName);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Initialize on load
|
|
1004
|
+
init();
|
|
1005
|
+
</script>
|
|
1006
|
+
</body>
|
|
1007
|
+
</html>`;
|