borreooo commited on
Commit
39a6f54
Β·
1 Parent(s): f491f79

Add guide box for chassis camera

Browse files
Files changed (3) hide show
  1. web/app.css +266 -50
  2. web/app.js +45 -24
  3. web/index.html +26 -8
web/app.css CHANGED
@@ -19,14 +19,18 @@
19
  --border: #e5e7eb;
20
  --bg: #f9fafb;
21
  --surface: #ffffff;
22
- --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.05);
23
- --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.08), 0 2px 4px -2px rgba(0,0,0,0.05);
24
 
25
  --radius: 8px;
26
  --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
27
  }
28
 
29
- * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
30
 
31
  body {
32
  font-family: var(--font);
@@ -45,6 +49,7 @@ body {
45
  top: 0;
46
  z-index: 100;
47
  }
 
48
  .header-inner {
49
  max-width: 1100px;
50
  margin: 0 auto;
@@ -54,6 +59,7 @@ body {
54
  align-items: center;
55
  justify-content: space-between;
56
  }
 
57
  .header-brand {
58
  display: flex;
59
  align-items: center;
@@ -62,8 +68,16 @@ body {
62
  font-size: 15px;
63
  color: var(--text);
64
  }
65
- .header-brand svg { color: var(--blue); }
66
- .header-right { display: flex; align-items: center; gap: 12px; }
 
 
 
 
 
 
 
 
67
 
68
  .status-dot {
69
  width: 8px;
@@ -71,8 +85,14 @@ body {
71
  border-radius: 50%;
72
  display: inline-block;
73
  }
74
- .status-dot.online { background: var(--green); }
75
- .status-dot.offline { background: var(--red); }
 
 
 
 
 
 
76
 
77
  .btn-install {
78
  font-family: var(--font);
@@ -96,6 +116,7 @@ body {
96
  max-width: 100%;
97
  overflow-x: auto;
98
  }
 
99
  .tab-btn {
100
  font-family: var(--font);
101
  font-size: 14px;
@@ -109,7 +130,11 @@ body {
109
  white-space: nowrap;
110
  transition: color 0.15s, border-color 0.15s;
111
  }
112
- .tab-btn:hover { color: var(--text); }
 
 
 
 
113
  .tab-btn.active {
114
  color: var(--blue);
115
  border-bottom-color: var(--blue);
@@ -122,8 +147,13 @@ body {
122
  padding: 24px 20px;
123
  }
124
 
125
- .tab-panel { display: none; }
126
- .tab-panel.active { display: block; }
 
 
 
 
 
127
 
128
  .two-col {
129
  display: grid;
@@ -131,8 +161,11 @@ body {
131
  gap: 20px;
132
  align-items: start;
133
  }
 
134
  @media (max-width: 768px) {
135
- .two-col { grid-template-columns: 1fr; }
 
 
136
  }
137
 
138
  /* ── Panel ─────────────────────────────────────────────── */
@@ -142,19 +175,23 @@ body {
142
  border-radius: var(--radius);
143
  box-shadow: var(--shadow);
144
  }
 
145
  .panel-header {
146
  padding: 16px 20px;
147
  border-bottom: 1px solid var(--border);
148
  }
 
149
  .panel-header h2 {
150
  font-size: 15px;
151
  font-weight: 600;
152
  }
 
153
  .panel-desc {
154
  color: var(--text-2);
155
  font-size: 13px;
156
  margin-top: 2px;
157
  }
 
158
  .panel-body {
159
  padding: 20px;
160
  display: flex;
@@ -168,12 +205,15 @@ body {
168
  flex-direction: column;
169
  gap: 6px;
170
  }
 
171
  label {
172
  font-size: 13px;
173
  font-weight: 500;
174
  color: var(--text-2);
175
  }
176
- .input, .select {
 
 
177
  font-family: var(--font);
178
  font-size: 14px;
179
  color: var(--text);
@@ -185,7 +225,9 @@ label {
185
  transition: border-color 0.15s;
186
  width: 100%;
187
  }
188
- .input:focus, .select:focus {
 
 
189
  border-color: var(--blue);
190
  }
191
 
@@ -198,6 +240,7 @@ label {
198
  padding: 3px;
199
  gap: 2px;
200
  }
 
201
  .seg-btn {
202
  flex: 1;
203
  font-family: var(--font);
@@ -211,6 +254,7 @@ label {
211
  cursor: pointer;
212
  transition: all 0.15s;
213
  }
 
214
  .seg-btn.active {
215
  background: var(--surface);
216
  color: var(--text);
@@ -231,12 +275,20 @@ label {
231
  background: var(--bg);
232
  text-align: center;
233
  }
 
234
  .dropzone:hover {
235
  border-color: var(--blue);
236
  background: var(--blue-light);
237
  }
238
- .dropzone svg { color: var(--text-3); }
239
- .dropzone span { font-size: 13px; color: var(--text-2); }
 
 
 
 
 
 
 
240
 
241
  /* ── Camera ────────────────────────────────────────────── */
242
  .camera-step {
@@ -247,7 +299,12 @@ label {
247
  flex-direction: column;
248
  gap: 10px;
249
  }
250
- .camera-step.disabled { opacity: 0.4; pointer-events: none; }
 
 
 
 
 
251
  .step-label {
252
  font-size: 12px;
253
  font-weight: 600;
@@ -255,6 +312,7 @@ label {
255
  text-transform: uppercase;
256
  letter-spacing: 0.5px;
257
  }
 
258
  .cam-view {
259
  width: 100%;
260
  min-height: 160px;
@@ -262,11 +320,13 @@ label {
262
  border-radius: 6px;
263
  overflow: hidden;
264
  }
 
265
  .snap-preview {
266
  width: 100%;
267
  border-radius: 6px;
268
  display: block;
269
  }
 
270
  .cam-actions {
271
  display: flex;
272
  gap: 8px;
@@ -287,38 +347,66 @@ label {
287
  border: none;
288
  transition: all 0.15s;
289
  }
 
290
  .btn-primary {
291
  background: var(--blue);
292
  color: #fff;
293
  }
294
- .btn-primary:hover:not(:disabled) { background: #1d4ed8; }
295
- .btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
 
 
 
 
 
 
 
 
296
  .btn-secondary {
297
  background: var(--surface);
298
  color: var(--text);
299
  border: 1px solid var(--border);
300
  }
301
- .btn-secondary:hover:not(:disabled) { background: var(--bg); }
302
- .btn-secondary:disabled { opacity: 0.4; cursor: not-allowed; }
 
 
 
 
 
 
 
 
303
  .btn-ghost {
304
  background: transparent;
305
  color: var(--text-2);
306
  border: 1px solid var(--border);
307
  }
308
- .btn-ghost:hover { background: var(--bg); }
309
- .btn-full { width: 100%; }
 
 
 
 
 
 
310
 
311
  /* ── Spinner ───────────────────────────────────────────── */
312
  .spinner {
313
  width: 14px;
314
  height: 14px;
315
- border: 2px solid rgba(255,255,255,0.3);
316
  border-top-color: currentColor;
317
  border-radius: 50%;
318
  animation: spin 0.7s linear infinite;
319
  display: inline-block;
320
  }
321
- @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
 
 
322
 
323
  /* ── Empty State ───────────────────────────────────────── */
324
  .empty-state {
@@ -330,7 +418,11 @@ label {
330
  text-align: center;
331
  color: var(--text-3);
332
  }
333
- .empty-state p { font-size: 13px; max-width: 260px; }
 
 
 
 
334
 
335
  /* ── Result Banner ─────────────────────────────────────── */
336
  .result-banner {
@@ -342,25 +434,39 @@ label {
342
  margin-bottom: 16px;
343
  border: 1px solid;
344
  }
 
345
  .result-banner.EXACT {
346
  background: var(--green-light);
347
  border-color: #bbf7d0;
348
  }
 
349
  .result-banner.CORRECTED {
350
  background: var(--amber-light);
351
  border-color: #fde68a;
352
  }
 
353
  .result-banner.FAILED {
354
  background: var(--red-light);
355
  border-color: #fecaca;
356
  }
 
357
  .banner-title {
358
  font-size: 14px;
359
  font-weight: 600;
360
  }
361
- .EXACT .banner-title { color: var(--green); }
362
- .CORRECTED .banner-title { color: var(--amber); }
363
- .FAILED .banner-title { color: var(--red); }
 
 
 
 
 
 
 
 
 
 
364
  .banner-sub {
365
  font-size: 12px;
366
  color: var(--text-2);
@@ -373,12 +479,17 @@ label {
373
  border-collapse: collapse;
374
  margin-bottom: 16px;
375
  }
 
376
  .result-table td {
377
  padding: 8px 0;
378
  border-bottom: 1px solid var(--border);
379
  vertical-align: top;
380
  }
381
- .result-table tr:last-child td { border-bottom: none; }
 
 
 
 
382
  .rt-label {
383
  font-size: 12px;
384
  font-weight: 500;
@@ -386,8 +497,15 @@ label {
386
  width: 40%;
387
  padding-right: 12px;
388
  }
389
- .rt-value { font-size: 14px; }
390
- .mono { font-family: 'Courier New', monospace; letter-spacing: 0.5px; }
 
 
 
 
 
 
 
391
 
392
  /* ── Preprocessing Tabs ─────────────────────────────────── */
393
  .section-title {
@@ -398,12 +516,14 @@ label {
398
  letter-spacing: 0.5px;
399
  margin-bottom: 8px;
400
  }
 
401
  .pre-tabs {
402
  display: flex;
403
  gap: 4px;
404
  overflow-x: auto;
405
  margin-bottom: 10px;
406
  }
 
407
  .pre-tab {
408
  font-family: var(--font);
409
  font-size: 12px;
@@ -417,12 +537,17 @@ label {
417
  white-space: nowrap;
418
  transition: all 0.15s;
419
  }
420
- .pre-tab:hover { color: var(--text); }
 
 
 
 
421
  .pre-tab.active {
422
  background: var(--blue);
423
  color: #fff;
424
  border-color: var(--blue);
425
  }
 
426
  .pre-img-wrap {
427
  border: 1px solid var(--border);
428
  border-radius: var(--radius);
@@ -430,6 +555,7 @@ label {
430
  background: #f3f4f6;
431
  position: relative;
432
  }
 
433
  .pre-img-wrap img {
434
  width: 100%;
435
  display: block;
@@ -437,6 +563,7 @@ label {
437
  object-fit: contain;
438
  background: #000;
439
  }
 
440
  .pre-ocr-label {
441
  padding: 8px 12px;
442
  font-size: 12px;
@@ -453,6 +580,7 @@ label {
453
  color: var(--text-2);
454
  margin-bottom: 6px;
455
  }
 
456
  .progress-track {
457
  height: 6px;
458
  background: var(--bg);
@@ -460,6 +588,7 @@ label {
460
  border: 1px solid var(--border);
461
  overflow: hidden;
462
  }
 
463
  .progress-fill {
464
  height: 100%;
465
  background: var(--blue);
@@ -472,9 +601,13 @@ label {
472
  grid-template-columns: repeat(4, 1fr);
473
  gap: 12px;
474
  }
 
475
  @media (max-width: 600px) {
476
- .stats-grid { grid-template-columns: 1fr 1fr; }
 
 
477
  }
 
478
  .stat-card {
479
  border: 1px solid var(--border);
480
  border-radius: var(--radius);
@@ -482,27 +615,38 @@ label {
482
  text-align: center;
483
  background: var(--bg);
484
  }
 
485
  .stat-value {
486
  font-size: 22px;
487
  font-weight: 700;
488
  color: var(--text);
489
  line-height: 1.2;
490
  }
 
491
  .stat-label {
492
  font-size: 12px;
493
  color: var(--text-2);
494
  margin-top: 4px;
495
  }
496
 
497
- .table-toolbar { margin-bottom: 12px; }
498
- .search-input { max-width: 300px; }
 
 
 
 
 
 
 
 
 
499
 
500
- .table-scroll { overflow-x: auto; }
501
  .data-table {
502
  width: 100%;
503
  border-collapse: collapse;
504
  font-size: 13px;
505
  }
 
506
  .data-table th {
507
  text-align: left;
508
  padding: 8px 12px;
@@ -513,13 +657,20 @@ label {
513
  border-bottom: 1px solid var(--border);
514
  white-space: nowrap;
515
  }
 
516
  .data-table td {
517
  padding: 10px 12px;
518
  border-bottom: 1px solid var(--border);
519
  vertical-align: middle;
520
  }
521
- .data-table tbody tr:hover { background: var(--bg); }
522
- .data-table tbody tr:last-child td { border-bottom: none; }
 
 
 
 
 
 
523
 
524
  .badge {
525
  display: inline-block;
@@ -529,21 +680,34 @@ label {
529
  border-radius: 20px;
530
  letter-spacing: 0.2px;
531
  }
532
- .badge-exact { background: var(--green-light); color: var(--green); }
533
- .badge-corrected { background: var(--amber-light); color: var(--amber); }
534
- .badge-failed { background: var(--red-light); color: var(--red); }
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
  /* ── Modal ─────────────────────────────────────────────── */
537
  .modal {
538
  position: fixed;
539
  inset: 0;
540
- background: rgba(0,0,0,0.5);
541
  z-index: 500;
542
  display: flex;
543
  align-items: center;
544
  justify-content: center;
545
  padding: 20px;
546
  }
 
547
  .modal-box {
548
  background: var(--surface);
549
  border-radius: var(--radius);
@@ -552,6 +716,7 @@ label {
552
  max-width: 860px;
553
  overflow: hidden;
554
  }
 
555
  .modal-head {
556
  display: flex;
557
  justify-content: space-between;
@@ -561,6 +726,7 @@ label {
561
  font-weight: 600;
562
  font-size: 14px;
563
  }
 
564
  .modal-close-btn {
565
  background: none;
566
  border: none;
@@ -570,7 +736,12 @@ label {
570
  line-height: 1;
571
  padding: 0 4px;
572
  }
573
- .modal-body { padding: 20px; max-height: 75vh; overflow-y: auto; }
 
 
 
 
 
574
 
575
  /* ── Toasts ────────────────────────────────────────────── */
576
  .toast-stack {
@@ -582,6 +753,7 @@ label {
582
  gap: 8px;
583
  z-index: 1000;
584
  }
 
585
  .toast {
586
  background: var(--surface);
587
  border: 1px solid var(--border);
@@ -596,14 +768,58 @@ label {
596
  align-items: center;
597
  gap: 8px;
598
  }
599
- .toast.success { border-left: 3px solid var(--green); }
600
- .toast.error { border-left: 3px solid var(--red); }
601
- .toast.info { border-left: 3px solid var(--blue); }
 
 
 
 
 
 
 
 
 
 
602
  @keyframes slide-up {
603
- from { opacity: 0; transform: translateY(8px); }
604
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  }
606
 
607
  /* ── Utilities ─────────────────────────────────────────── */
608
- .hidden { display: none !important; }
609
- .hidden-input { display: none; }
 
 
 
 
 
 
19
  --border: #e5e7eb;
20
  --bg: #f9fafb;
21
  --surface: #ffffff;
22
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.05);
23
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
24
 
25
  --radius: 8px;
26
  --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
27
  }
28
 
29
+ * {
30
+ box-sizing: border-box;
31
+ margin: 0;
32
+ padding: 0;
33
+ }
34
 
35
  body {
36
  font-family: var(--font);
 
49
  top: 0;
50
  z-index: 100;
51
  }
52
+
53
  .header-inner {
54
  max-width: 1100px;
55
  margin: 0 auto;
 
59
  align-items: center;
60
  justify-content: space-between;
61
  }
62
+
63
  .header-brand {
64
  display: flex;
65
  align-items: center;
 
68
  font-size: 15px;
69
  color: var(--text);
70
  }
71
+
72
+ .header-brand svg {
73
+ color: var(--blue);
74
+ }
75
+
76
+ .header-right {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 12px;
80
+ }
81
 
82
  .status-dot {
83
  width: 8px;
 
85
  border-radius: 50%;
86
  display: inline-block;
87
  }
88
+
89
+ .status-dot.online {
90
+ background: var(--green);
91
+ }
92
+
93
+ .status-dot.offline {
94
+ background: var(--red);
95
+ }
96
 
97
  .btn-install {
98
  font-family: var(--font);
 
116
  max-width: 100%;
117
  overflow-x: auto;
118
  }
119
+
120
  .tab-btn {
121
  font-family: var(--font);
122
  font-size: 14px;
 
130
  white-space: nowrap;
131
  transition: color 0.15s, border-color 0.15s;
132
  }
133
+
134
+ .tab-btn:hover {
135
+ color: var(--text);
136
+ }
137
+
138
  .tab-btn.active {
139
  color: var(--blue);
140
  border-bottom-color: var(--blue);
 
147
  padding: 24px 20px;
148
  }
149
 
150
+ .tab-panel {
151
+ display: none;
152
+ }
153
+
154
+ .tab-panel.active {
155
+ display: block;
156
+ }
157
 
158
  .two-col {
159
  display: grid;
 
161
  gap: 20px;
162
  align-items: start;
163
  }
164
+
165
  @media (max-width: 768px) {
166
+ .two-col {
167
+ grid-template-columns: 1fr;
168
+ }
169
  }
170
 
171
  /* ── Panel ─────────────────────────────────────────────── */
 
175
  border-radius: var(--radius);
176
  box-shadow: var(--shadow);
177
  }
178
+
179
  .panel-header {
180
  padding: 16px 20px;
181
  border-bottom: 1px solid var(--border);
182
  }
183
+
184
  .panel-header h2 {
185
  font-size: 15px;
186
  font-weight: 600;
187
  }
188
+
189
  .panel-desc {
190
  color: var(--text-2);
191
  font-size: 13px;
192
  margin-top: 2px;
193
  }
194
+
195
  .panel-body {
196
  padding: 20px;
197
  display: flex;
 
205
  flex-direction: column;
206
  gap: 6px;
207
  }
208
+
209
  label {
210
  font-size: 13px;
211
  font-weight: 500;
212
  color: var(--text-2);
213
  }
214
+
215
+ .input,
216
+ .select {
217
  font-family: var(--font);
218
  font-size: 14px;
219
  color: var(--text);
 
225
  transition: border-color 0.15s;
226
  width: 100%;
227
  }
228
+
229
+ .input:focus,
230
+ .select:focus {
231
  border-color: var(--blue);
232
  }
233
 
 
240
  padding: 3px;
241
  gap: 2px;
242
  }
243
+
244
  .seg-btn {
245
  flex: 1;
246
  font-family: var(--font);
 
254
  cursor: pointer;
255
  transition: all 0.15s;
256
  }
257
+
258
  .seg-btn.active {
259
  background: var(--surface);
260
  color: var(--text);
 
275
  background: var(--bg);
276
  text-align: center;
277
  }
278
+
279
  .dropzone:hover {
280
  border-color: var(--blue);
281
  background: var(--blue-light);
282
  }
283
+
284
+ .dropzone svg {
285
+ color: var(--text-3);
286
+ }
287
+
288
+ .dropzone span {
289
+ font-size: 13px;
290
+ color: var(--text-2);
291
+ }
292
 
293
  /* ── Camera ────────────────────────────────────────────── */
294
  .camera-step {
 
299
  flex-direction: column;
300
  gap: 10px;
301
  }
302
+
303
+ .camera-step.disabled {
304
+ opacity: 0.4;
305
+ pointer-events: none;
306
+ }
307
+
308
  .step-label {
309
  font-size: 12px;
310
  font-weight: 600;
 
312
  text-transform: uppercase;
313
  letter-spacing: 0.5px;
314
  }
315
+
316
  .cam-view {
317
  width: 100%;
318
  min-height: 160px;
 
320
  border-radius: 6px;
321
  overflow: hidden;
322
  }
323
+
324
  .snap-preview {
325
  width: 100%;
326
  border-radius: 6px;
327
  display: block;
328
  }
329
+
330
  .cam-actions {
331
  display: flex;
332
  gap: 8px;
 
347
  border: none;
348
  transition: all 0.15s;
349
  }
350
+
351
  .btn-primary {
352
  background: var(--blue);
353
  color: #fff;
354
  }
355
+
356
+ .btn-primary:hover:not(:disabled) {
357
+ background: #1d4ed8;
358
+ }
359
+
360
+ .btn-primary:disabled {
361
+ opacity: 0.4;
362
+ cursor: not-allowed;
363
+ }
364
+
365
  .btn-secondary {
366
  background: var(--surface);
367
  color: var(--text);
368
  border: 1px solid var(--border);
369
  }
370
+
371
+ .btn-secondary:hover:not(:disabled) {
372
+ background: var(--bg);
373
+ }
374
+
375
+ .btn-secondary:disabled {
376
+ opacity: 0.4;
377
+ cursor: not-allowed;
378
+ }
379
+
380
  .btn-ghost {
381
  background: transparent;
382
  color: var(--text-2);
383
  border: 1px solid var(--border);
384
  }
385
+
386
+ .btn-ghost:hover {
387
+ background: var(--bg);
388
+ }
389
+
390
+ .btn-full {
391
+ width: 100%;
392
+ }
393
 
394
  /* ── Spinner ───────────────────────────────────────────── */
395
  .spinner {
396
  width: 14px;
397
  height: 14px;
398
+ border: 2px solid rgba(255, 255, 255, 0.3);
399
  border-top-color: currentColor;
400
  border-radius: 50%;
401
  animation: spin 0.7s linear infinite;
402
  display: inline-block;
403
  }
404
+
405
+ @keyframes spin {
406
+ to {
407
+ transform: rotate(360deg);
408
+ }
409
+ }
410
 
411
  /* ── Empty State ───────────────────────────────────────── */
412
  .empty-state {
 
418
  text-align: center;
419
  color: var(--text-3);
420
  }
421
+
422
+ .empty-state p {
423
+ font-size: 13px;
424
+ max-width: 260px;
425
+ }
426
 
427
  /* ── Result Banner ─────────────────────────────────────── */
428
  .result-banner {
 
434
  margin-bottom: 16px;
435
  border: 1px solid;
436
  }
437
+
438
  .result-banner.EXACT {
439
  background: var(--green-light);
440
  border-color: #bbf7d0;
441
  }
442
+
443
  .result-banner.CORRECTED {
444
  background: var(--amber-light);
445
  border-color: #fde68a;
446
  }
447
+
448
  .result-banner.FAILED {
449
  background: var(--red-light);
450
  border-color: #fecaca;
451
  }
452
+
453
  .banner-title {
454
  font-size: 14px;
455
  font-weight: 600;
456
  }
457
+
458
+ .EXACT .banner-title {
459
+ color: var(--green);
460
+ }
461
+
462
+ .CORRECTED .banner-title {
463
+ color: var(--amber);
464
+ }
465
+
466
+ .FAILED .banner-title {
467
+ color: var(--red);
468
+ }
469
+
470
  .banner-sub {
471
  font-size: 12px;
472
  color: var(--text-2);
 
479
  border-collapse: collapse;
480
  margin-bottom: 16px;
481
  }
482
+
483
  .result-table td {
484
  padding: 8px 0;
485
  border-bottom: 1px solid var(--border);
486
  vertical-align: top;
487
  }
488
+
489
+ .result-table tr:last-child td {
490
+ border-bottom: none;
491
+ }
492
+
493
  .rt-label {
494
  font-size: 12px;
495
  font-weight: 500;
 
497
  width: 40%;
498
  padding-right: 12px;
499
  }
500
+
501
+ .rt-value {
502
+ font-size: 14px;
503
+ }
504
+
505
+ .mono {
506
+ font-family: 'Courier New', monospace;
507
+ letter-spacing: 0.5px;
508
+ }
509
 
510
  /* ── Preprocessing Tabs ─────────────────────────────────── */
511
  .section-title {
 
516
  letter-spacing: 0.5px;
517
  margin-bottom: 8px;
518
  }
519
+
520
  .pre-tabs {
521
  display: flex;
522
  gap: 4px;
523
  overflow-x: auto;
524
  margin-bottom: 10px;
525
  }
526
+
527
  .pre-tab {
528
  font-family: var(--font);
529
  font-size: 12px;
 
537
  white-space: nowrap;
538
  transition: all 0.15s;
539
  }
540
+
541
+ .pre-tab:hover {
542
+ color: var(--text);
543
+ }
544
+
545
  .pre-tab.active {
546
  background: var(--blue);
547
  color: #fff;
548
  border-color: var(--blue);
549
  }
550
+
551
  .pre-img-wrap {
552
  border: 1px solid var(--border);
553
  border-radius: var(--radius);
 
555
  background: #f3f4f6;
556
  position: relative;
557
  }
558
+
559
  .pre-img-wrap img {
560
  width: 100%;
561
  display: block;
 
563
  object-fit: contain;
564
  background: #000;
565
  }
566
+
567
  .pre-ocr-label {
568
  padding: 8px 12px;
569
  font-size: 12px;
 
580
  color: var(--text-2);
581
  margin-bottom: 6px;
582
  }
583
+
584
  .progress-track {
585
  height: 6px;
586
  background: var(--bg);
 
588
  border: 1px solid var(--border);
589
  overflow: hidden;
590
  }
591
+
592
  .progress-fill {
593
  height: 100%;
594
  background: var(--blue);
 
601
  grid-template-columns: repeat(4, 1fr);
602
  gap: 12px;
603
  }
604
+
605
  @media (max-width: 600px) {
606
+ .stats-grid {
607
+ grid-template-columns: 1fr 1fr;
608
+ }
609
  }
610
+
611
  .stat-card {
612
  border: 1px solid var(--border);
613
  border-radius: var(--radius);
 
615
  text-align: center;
616
  background: var(--bg);
617
  }
618
+
619
  .stat-value {
620
  font-size: 22px;
621
  font-weight: 700;
622
  color: var(--text);
623
  line-height: 1.2;
624
  }
625
+
626
  .stat-label {
627
  font-size: 12px;
628
  color: var(--text-2);
629
  margin-top: 4px;
630
  }
631
 
632
+ .table-toolbar {
633
+ margin-bottom: 12px;
634
+ }
635
+
636
+ .search-input {
637
+ max-width: 300px;
638
+ }
639
+
640
+ .table-scroll {
641
+ overflow-x: auto;
642
+ }
643
 
 
644
  .data-table {
645
  width: 100%;
646
  border-collapse: collapse;
647
  font-size: 13px;
648
  }
649
+
650
  .data-table th {
651
  text-align: left;
652
  padding: 8px 12px;
 
657
  border-bottom: 1px solid var(--border);
658
  white-space: nowrap;
659
  }
660
+
661
  .data-table td {
662
  padding: 10px 12px;
663
  border-bottom: 1px solid var(--border);
664
  vertical-align: middle;
665
  }
666
+
667
+ .data-table tbody tr:hover {
668
+ background: var(--bg);
669
+ }
670
+
671
+ .data-table tbody tr:last-child td {
672
+ border-bottom: none;
673
+ }
674
 
675
  .badge {
676
  display: inline-block;
 
680
  border-radius: 20px;
681
  letter-spacing: 0.2px;
682
  }
683
+
684
+ .badge-exact {
685
+ background: var(--green-light);
686
+ color: var(--green);
687
+ }
688
+
689
+ .badge-corrected {
690
+ background: var(--amber-light);
691
+ color: var(--amber);
692
+ }
693
+
694
+ .badge-failed {
695
+ background: var(--red-light);
696
+ color: var(--red);
697
+ }
698
 
699
  /* ── Modal ─────────────────────────────────────────────── */
700
  .modal {
701
  position: fixed;
702
  inset: 0;
703
+ background: rgba(0, 0, 0, 0.5);
704
  z-index: 500;
705
  display: flex;
706
  align-items: center;
707
  justify-content: center;
708
  padding: 20px;
709
  }
710
+
711
  .modal-box {
712
  background: var(--surface);
713
  border-radius: var(--radius);
 
716
  max-width: 860px;
717
  overflow: hidden;
718
  }
719
+
720
  .modal-head {
721
  display: flex;
722
  justify-content: space-between;
 
726
  font-weight: 600;
727
  font-size: 14px;
728
  }
729
+
730
  .modal-close-btn {
731
  background: none;
732
  border: none;
 
736
  line-height: 1;
737
  padding: 0 4px;
738
  }
739
+
740
+ .modal-body {
741
+ padding: 20px;
742
+ max-height: 75vh;
743
+ overflow-y: auto;
744
+ }
745
 
746
  /* ── Toasts ────────────────────────────────────────────── */
747
  .toast-stack {
 
753
  gap: 8px;
754
  z-index: 1000;
755
  }
756
+
757
  .toast {
758
  background: var(--surface);
759
  border: 1px solid var(--border);
 
768
  align-items: center;
769
  gap: 8px;
770
  }
771
+
772
+ .toast.success {
773
+ border-left: 3px solid var(--green);
774
+ }
775
+
776
+ .toast.error {
777
+ border-left: 3px solid var(--red);
778
+ }
779
+
780
+ .toast.info {
781
+ border-left: 3px solid var(--blue);
782
+ }
783
+
784
  @keyframes slide-up {
785
+ from {
786
+ opacity: 0;
787
+ transform: translateY(8px);
788
+ }
789
+
790
+ to {
791
+ opacity: 1;
792
+ transform: translateY(0);
793
+ }
794
+ }
795
+
796
+ /* ADD THIS TO THE END OF YOUR EXISTING app.css β€” guide box for chassis camera capture */
797
+
798
+ .chassis-video-wrap {
799
+ position: relative;
800
+ display: inline-block;
801
+ width: 100%;
802
+ }
803
+
804
+ .guide-box {
805
+ position: absolute;
806
+ top: 50%;
807
+ left: 50%;
808
+ transform: translate(-50%, -50%);
809
+ width: 70%;
810
+ height: 22%;
811
+ border: 3px solid #2563eb;
812
+ border-radius: 8px;
813
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
814
+ pointer-events: none;
815
+ z-index: 2;
816
  }
817
 
818
  /* ── Utilities ─────────────────────────────────────────── */
819
+ .hidden {
820
+ display: none !important;
821
+ }
822
+
823
+ .hidden-input {
824
+ display: none;
825
+ }
web/app.js CHANGED
@@ -91,7 +91,7 @@ document.addEventListener('DOMContentLoaded', () => {
91
  function setupDropzone(dzId, inputId, nameId, isBarcode) {
92
  const dz = document.getElementById(dzId);
93
  const inp = document.getElementById(inputId);
94
- const nm = document.getElementById(nameId);
95
 
96
  dz.addEventListener('click', () => inp.click());
97
  dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('active'); });
@@ -158,19 +158,23 @@ document.addEventListener('DOMContentLoaded', () => {
158
  validate();
159
  });
160
  },
161
- () => {}
162
  ).catch(e => { toast(`Camera error: ${e}`, 'error'); startBarcodeCam.textContent = 'Start Camera'; qr = null; });
163
  });
164
 
165
  // ── Camera β€” chassis ─────────────────────────────────────
166
  let chassisStream = null;
167
- const chassisVideo = document.getElementById('chassis-video');
168
- const chassisCanvas = document.getElementById('chassis-canvas');
169
- const snapContainer = document.getElementById('chassis-snap-container');
170
- const snapImg = document.getElementById('chassis-snap-img');
171
  const startChassisCam = document.getElementById('start-chassis-cam');
172
- const captureBtn = document.getElementById('capture-chassis');
173
- const retakeBtn = document.getElementById('reset-chassis-snap');
 
 
 
 
174
 
175
  startChassisCam.addEventListener('click', async () => {
176
  if (chassisStream) { stopChassisCamera(); return; }
@@ -180,21 +184,37 @@ document.addEventListener('DOMContentLoaded', () => {
180
  });
181
  chassisVideo.srcObject = chassisStream;
182
  chassisVideo.classList.remove('hidden');
 
183
  captureBtn.classList.remove('hidden');
184
  snapContainer.classList.add('hidden');
185
  retakeBtn.classList.add('hidden');
186
  startChassisCam.textContent = 'Stop';
187
- } catch(e) { toast(`Camera error: ${e.message}`, 'error'); }
188
  });
189
 
190
  captureBtn.addEventListener('click', () => {
191
- chassisCanvas.width = chassisVideo.videoWidth;
192
- chassisCanvas.height = chassisVideo.videoHeight;
193
- chassisCanvas.getContext('2d').drawImage(chassisVideo, 0, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  chassisCanvas.toBlob(blob => {
195
  chassisBlob = blob;
196
  snapImg.src = URL.createObjectURL(blob);
197
  chassisVideo.classList.add('hidden');
 
198
  captureBtn.classList.add('hidden');
199
  snapContainer.classList.remove('hidden');
200
  retakeBtn.classList.remove('hidden');
@@ -215,12 +235,13 @@ document.addEventListener('DOMContentLoaded', () => {
215
  if (chassisStream) { chassisStream.getTracks().forEach(t => t.stop()); chassisStream = null; }
216
  chassisVideo.srcObject = null;
217
  chassisVideo.classList.add('hidden');
 
218
  captureBtn.classList.add('hidden');
219
  startChassisCam.textContent = 'Start Camera';
220
  }
221
 
222
  function stopAllCameras() {
223
- if (qr) { qr.stop().catch(() => {}); qr = null; startBarcodeCam.textContent = 'Start Camera'; }
224
  stopChassisCamera();
225
  }
226
 
@@ -273,14 +294,14 @@ document.addEventListener('DOMContentLoaded', () => {
273
  const banner = document.getElementById('result-banner');
274
  banner.className = `result-banner ${d.status}`;
275
  const icons = {
276
- EXACT: 'βœ“',
277
  CORRECTED: '~',
278
- FAILED: 'βœ—'
279
  };
280
  const subs = {
281
- EXACT: 'Raw OCR matched the barcode exactly.',
282
  CORRECTED: 'Match achieved after error-correction.',
283
- FAILED: 'OCR output could not be matched.'
284
  };
285
  document.getElementById('banner-icon').textContent = icons[d.status];
286
  document.getElementById('banner-title').textContent = d.status === 'EXACT' ? 'Exact Match' : d.status === 'CORRECTED' ? 'Corrected Match' : 'No Match';
@@ -288,10 +309,10 @@ document.addEventListener('DOMContentLoaded', () => {
288
 
289
  // Table
290
  document.getElementById('res-expected').textContent = d.barcode_val;
291
- document.getElementById('res-raw').textContent = d.raw_ocr || 'β€”';
292
  document.getElementById('res-corrected').textContent = d.corrected_ocr || 'β€”';
293
- document.getElementById('res-conf').textContent = `${Math.round(d.confidence * 100)}%`;
294
- document.getElementById('res-filter').textContent = d.winning_label || 'β€”';
295
 
296
  // Preprocessing carousel
297
  const tabs = document.querySelectorAll('.pre-tab');
@@ -326,8 +347,8 @@ document.addEventListener('DOMContentLoaded', () => {
326
  }
327
 
328
  // ── Modal ────────────────────────────────────────────────
329
- const modal = document.getElementById('modal');
330
- const modalImg = document.getElementById('modal-img');
331
  const modalTitle = document.getElementById('modal-title');
332
 
333
  function openModal(url, title) {
@@ -341,6 +362,6 @@ document.addEventListener('DOMContentLoaded', () => {
341
 
342
  // ── Service Worker ───────────────────────────────────────
343
  if ('serviceWorker' in navigator) {
344
- navigator.serviceWorker.register('/sw.js').catch(() => {});
345
  }
346
- });
 
91
  function setupDropzone(dzId, inputId, nameId, isBarcode) {
92
  const dz = document.getElementById(dzId);
93
  const inp = document.getElementById(inputId);
94
+ const nm = document.getElementById(nameId);
95
 
96
  dz.addEventListener('click', () => inp.click());
97
  dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('active'); });
 
158
  validate();
159
  });
160
  },
161
+ () => { }
162
  ).catch(e => { toast(`Camera error: ${e}`, 'error'); startBarcodeCam.textContent = 'Start Camera'; qr = null; });
163
  });
164
 
165
  // ── Camera β€” chassis ─────────────────────────────────────
166
  let chassisStream = null;
167
+ const chassisVideo = document.getElementById('chassis-video');
168
+ const chassisCanvas = document.getElementById('chassis-canvas');
169
+ const snapContainer = document.getElementById('chassis-snap-container');
170
+ const snapImg = document.getElementById('chassis-snap-img');
171
  const startChassisCam = document.getElementById('start-chassis-cam');
172
+ const captureBtn = document.getElementById('capture-chassis');
173
+ const retakeBtn = document.getElementById('reset-chassis-snap');
174
+ const guideBox = document.getElementById('chassis-guide-box');
175
+
176
+ // Guide box region as fractions of the video frame (matches CSS: 70% wide, 22% tall, centered)
177
+ const GUIDE_BOX_FRACTION = { widthFrac: 0.70, heightFrac: 0.22, xFrac: 0.15, yFrac: 0.39 };
178
 
179
  startChassisCam.addEventListener('click', async () => {
180
  if (chassisStream) { stopChassisCamera(); return; }
 
184
  });
185
  chassisVideo.srcObject = chassisStream;
186
  chassisVideo.classList.remove('hidden');
187
+ guideBox.classList.remove('hidden');
188
  captureBtn.classList.remove('hidden');
189
  snapContainer.classList.add('hidden');
190
  retakeBtn.classList.add('hidden');
191
  startChassisCam.textContent = 'Stop';
192
+ } catch (e) { toast(`Camera error: ${e.message}`, 'error'); }
193
  });
194
 
195
  captureBtn.addEventListener('click', () => {
196
+ const vw = chassisVideo.videoWidth;
197
+ const vh = chassisVideo.videoHeight;
198
+
199
+ // Crop to just the guide box region so OCR only sees the framed chassis strip
200
+ const cropX = vw * GUIDE_BOX_FRACTION.xFrac;
201
+ const cropY = vh * GUIDE_BOX_FRACTION.yFrac;
202
+ const cropW = vw * GUIDE_BOX_FRACTION.widthFrac;
203
+ const cropH = vh * GUIDE_BOX_FRACTION.heightFrac;
204
+
205
+ chassisCanvas.width = cropW;
206
+ chassisCanvas.height = cropH;
207
+ chassisCanvas.getContext('2d').drawImage(
208
+ chassisVideo,
209
+ cropX, cropY, cropW, cropH, // source rect (the guide box area)
210
+ 0, 0, cropW, cropH // destination rect (full canvas)
211
+ );
212
+
213
  chassisCanvas.toBlob(blob => {
214
  chassisBlob = blob;
215
  snapImg.src = URL.createObjectURL(blob);
216
  chassisVideo.classList.add('hidden');
217
+ guideBox.classList.add('hidden');
218
  captureBtn.classList.add('hidden');
219
  snapContainer.classList.remove('hidden');
220
  retakeBtn.classList.remove('hidden');
 
235
  if (chassisStream) { chassisStream.getTracks().forEach(t => t.stop()); chassisStream = null; }
236
  chassisVideo.srcObject = null;
237
  chassisVideo.classList.add('hidden');
238
+ guideBox.classList.add('hidden');
239
  captureBtn.classList.add('hidden');
240
  startChassisCam.textContent = 'Start Camera';
241
  }
242
 
243
  function stopAllCameras() {
244
+ if (qr) { qr.stop().catch(() => { }); qr = null; startBarcodeCam.textContent = 'Start Camera'; }
245
  stopChassisCamera();
246
  }
247
 
 
294
  const banner = document.getElementById('result-banner');
295
  banner.className = `result-banner ${d.status}`;
296
  const icons = {
297
+ EXACT: 'βœ“',
298
  CORRECTED: '~',
299
+ FAILED: 'βœ—'
300
  };
301
  const subs = {
302
+ EXACT: 'Raw OCR matched the barcode exactly.',
303
  CORRECTED: 'Match achieved after error-correction.',
304
+ FAILED: 'OCR output could not be matched.'
305
  };
306
  document.getElementById('banner-icon').textContent = icons[d.status];
307
  document.getElementById('banner-title').textContent = d.status === 'EXACT' ? 'Exact Match' : d.status === 'CORRECTED' ? 'Corrected Match' : 'No Match';
 
309
 
310
  // Table
311
  document.getElementById('res-expected').textContent = d.barcode_val;
312
+ document.getElementById('res-raw').textContent = d.raw_ocr || 'β€”';
313
  document.getElementById('res-corrected').textContent = d.corrected_ocr || 'β€”';
314
+ document.getElementById('res-conf').textContent = `${Math.round(d.confidence * 100)}%`;
315
+ document.getElementById('res-filter').textContent = d.winning_label || 'β€”';
316
 
317
  // Preprocessing carousel
318
  const tabs = document.querySelectorAll('.pre-tab');
 
347
  }
348
 
349
  // ── Modal ────────────────────────────────────────────────
350
+ const modal = document.getElementById('modal');
351
+ const modalImg = document.getElementById('modal-img');
352
  const modalTitle = document.getElementById('modal-title');
353
 
354
  function openModal(url, title) {
 
362
 
363
  // ── Service Worker ───────────────────────────────────────
364
  if ('serviceWorker' in navigator) {
365
+ navigator.serviceWorker.register('/sw.js').catch(() => { });
366
  }
367
+ });
web/index.html CHANGED
@@ -1,5 +1,6 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,13 +14,15 @@
13
  <link rel="stylesheet" href="app.css">
14
  <script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
15
  </head>
 
16
  <body>
17
  <header class="header">
18
  <div class="header-inner">
19
  <div class="header-brand">
20
- <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21
- <rect x="2" y="5" width="20" height="14" rx="2"/>
22
- <line x1="2" y1="10" x2="22" y2="10"/>
 
23
  </svg>
24
  <span>Chassis OCR</span>
25
  </div>
@@ -64,7 +67,10 @@
64
  <label>Barcode Image</label>
65
  <div class="dropzone" id="barcode-dropzone">
66
  <input type="file" id="barcode-file" accept="image/*" class="hidden-input">
67
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
 
 
 
68
  <span id="barcode-file-name">Drop or click to upload</span>
69
  </div>
70
  </div>
@@ -72,7 +78,10 @@
72
  <label>Chassis Image</label>
73
  <div class="dropzone" id="chassis-dropzone">
74
  <input type="file" id="chassis-file" accept="image/*" class="hidden-input">
75
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
 
 
 
76
  <span id="chassis-file-name">Drop or click to upload</span>
77
  </div>
78
  </div>
@@ -87,7 +96,10 @@
87
  </div>
88
  <div class="camera-step disabled" id="cam-step-2">
89
  <div class="step-label">Step 2 β€” Capture Chassis</div>
90
- <video id="chassis-video" autoplay playsinline class="cam-view hidden"></video>
 
 
 
91
  <canvas id="chassis-canvas" class="hidden"></canvas>
92
  <div id="chassis-snap-container" class="hidden">
93
  <img id="chassis-snap-img" class="snap-preview" src="" alt="Chassis snapshot">
@@ -120,7 +132,12 @@
120
  </div>
121
  <div class="panel-body" id="result-body">
122
  <div id="result-empty" class="empty-state">
123
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
 
 
 
 
 
124
  <p>Select a pair and run OCR to see results.</p>
125
  </div>
126
 
@@ -179,4 +196,5 @@
179
 
180
  <script src="app.js"></script>
181
  </body>
182
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
14
  <link rel="stylesheet" href="app.css">
15
  <script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
16
  </head>
17
+
18
  <body>
19
  <header class="header">
20
  <div class="header-inner">
21
  <div class="header-brand">
22
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
23
+ stroke-linecap="round" stroke-linejoin="round">
24
+ <rect x="2" y="5" width="20" height="14" rx="2" />
25
+ <line x1="2" y1="10" x2="22" y2="10" />
26
  </svg>
27
  <span>Chassis OCR</span>
28
  </div>
 
67
  <label>Barcode Image</label>
68
  <div class="dropzone" id="barcode-dropzone">
69
  <input type="file" id="barcode-file" accept="image/*" class="hidden-input">
70
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
71
+ stroke-width="1.5">
72
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
73
+ </svg>
74
  <span id="barcode-file-name">Drop or click to upload</span>
75
  </div>
76
  </div>
 
78
  <label>Chassis Image</label>
79
  <div class="dropzone" id="chassis-dropzone">
80
  <input type="file" id="chassis-file" accept="image/*" class="hidden-input">
81
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
82
+ stroke-width="1.5">
83
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
84
+ </svg>
85
  <span id="chassis-file-name">Drop or click to upload</span>
86
  </div>
87
  </div>
 
96
  </div>
97
  <div class="camera-step disabled" id="cam-step-2">
98
  <div class="step-label">Step 2 β€” Capture Chassis</div>
99
+ <div class="chassis-video-wrap">
100
+ <video id="chassis-video" autoplay playsinline class="cam-view hidden"></video>
101
+ <div id="chassis-guide-box" class="guide-box hidden"></div>
102
+ </div>
103
  <canvas id="chassis-canvas" class="hidden"></canvas>
104
  <div id="chassis-snap-container" class="hidden">
105
  <img id="chassis-snap-img" class="snap-preview" src="" alt="Chassis snapshot">
 
132
  </div>
133
  <div class="panel-body" id="result-body">
134
  <div id="result-empty" class="empty-state">
135
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor"
136
+ stroke-width="1.5">
137
+ <circle cx="12" cy="12" r="10" />
138
+ <line x1="12" y1="8" x2="12" y2="12" />
139
+ <line x1="12" y1="16" x2="12.01" y2="16" />
140
+ </svg>
141
  <p>Select a pair and run OCR to see results.</p>
142
  </div>
143
 
 
196
 
197
  <script src="app.js"></script>
198
  </body>
199
+
200
+ </html>