SiddharthVenba commited on
Commit
46981e7
Β·
1 Parent(s): bef05ef

Initial UI Demo deployment with simulated AI

Browse files
Files changed (5) hide show
  1. README.md +1 -10
  2. css/styles.css +1725 -0
  3. index.html +560 -19
  4. js/alt_app.js +1010 -0
  5. js/app.js +852 -0
README.md CHANGED
@@ -1,10 +1 @@
1
- ---
2
- title: TraceScene UI
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: red
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---\ntitle: TraceScene UI Demo\nemoji: πŸš”\ncolorFrom: blue\ncolorTo: red\nsdk: static\npinned: false\n---\n
 
 
 
 
 
 
 
 
 
css/styles.css ADDED
@@ -0,0 +1,1725 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ AI Accident Analysis β€” Stylesheet
3
+ Dark theme with police/forensic aesthetic, glassmorphism
4
+ ========================================================================== */
5
+
6
+ /* ── CSS Variables ──────────────────────────────────────────────────── */
7
+ :root {
8
+ /* Primary palette β€” clean white with dark blue accents */
9
+ --bg-primary: #f8fafc;
10
+ --bg-secondary: #ffffff;
11
+ --bg-tertiary: #f1f5f9;
12
+
13
+ /* Dark Blue Panels */
14
+ --surface: rgba(255, 255, 255, 0.85);
15
+ /* deeply saturated dark blue */
16
+ --surface-hover: rgba(240, 244, 248, 0.9);
17
+ --glass: rgba(255, 255, 255, 0.75);
18
+ /* translucent dark blue */
19
+ --glass-border: rgba(30, 58, 138, 0.15);
20
+
21
+ /* Text on light backgrounds (Global) */
22
+ --text-primary: #0f172a;
23
+ --text-secondary: #334155;
24
+ --text-muted: #64748b;
25
+
26
+ /* Accent */
27
+ --accent: #1e3a8a;
28
+ /* deep dark blue */
29
+ --accent-hover: #172554;
30
+ --accent-glow: rgba(30, 58, 138, 0.15);
31
+
32
+ /* Severity colors */
33
+ --critical: #ef4444;
34
+ --critical-bg: rgba(239, 68, 68, 0.12);
35
+ --high: #f97316;
36
+ --high-bg: rgba(249, 115, 22, 0.12);
37
+ --medium: #eab308;
38
+ --medium-bg: rgba(234, 179, 8, 0.12);
39
+ --low: #22c55e;
40
+ --low-bg: rgba(34, 197, 94, 0.12);
41
+
42
+ /* Status */
43
+ --pending: #8b97b0;
44
+ --analyzing: #3b82f6;
45
+ --complete: #22c55e;
46
+ --error: #ef4444;
47
+
48
+ /* Layout */
49
+ --sidebar-width: 240px;
50
+ --radius: 12px;
51
+ --radius-sm: 8px;
52
+ --radius-xs: 6px;
53
+
54
+ /* Fonts */
55
+ --font-heading: 'Outfit', sans-serif;
56
+ --font-body: 'Inter', sans-serif;
57
+
58
+ /* Transitions */
59
+ --transition: 0.2s ease;
60
+ --transition-slow: 0.4s ease;
61
+ }
62
+
63
+ /* ── Reset & Base ──────────────────────────────────────────────────── */
64
+ *,
65
+ *::before,
66
+ *::after {
67
+ box-sizing: border-box;
68
+ margin: 0;
69
+ padding: 0;
70
+ }
71
+
72
+ html,
73
+ body {
74
+ height: 100%;
75
+ font-family: var(--font-body);
76
+ font-size: 14px;
77
+ line-height: 1.6;
78
+ color: var(--text-primary);
79
+ background: var(--bg-primary);
80
+ -webkit-font-smoothing: antialiased;
81
+ overflow: hidden;
82
+ }
83
+
84
+ h1,
85
+ h2,
86
+ h3,
87
+ h4,
88
+ h5 {
89
+ font-family: var(--font-heading);
90
+ font-weight: 600;
91
+ }
92
+
93
+ input,
94
+ select,
95
+ textarea,
96
+ button {
97
+ font-family: var(--font-body);
98
+ font-size: 0.9rem;
99
+ }
100
+
101
+ /* ── App Layout ────────────────────────────────────────────────────── */
102
+ .app-container {
103
+ display: flex;
104
+ height: 100vh;
105
+ background:
106
+ radial-gradient(ellipse at 20% 50%, rgba(59, 130, 246, 0.06) 0%, transparent 60%),
107
+ radial-gradient(ellipse at 80% 20%, rgba(139, 92, 246, 0.04) 0%, transparent 50%),
108
+ var(--bg-primary);
109
+ }
110
+
111
+ /* ── Sidebar ───────────────────────────────────────────────────────── */
112
+ .sidebar {
113
+ width: var(--sidebar-width);
114
+ min-width: var(--sidebar-width);
115
+ display: flex;
116
+ flex-direction: column;
117
+ padding: 1.5rem 1rem;
118
+ border-right: 1px solid var(--glass-border);
119
+ background: var(--glass);
120
+ backdrop-filter: blur(20px);
121
+ }
122
+
123
+ .brand {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 0.7rem;
127
+ padding: 0 0.5rem;
128
+ margin-bottom: 2rem;
129
+ }
130
+
131
+ .brand i {
132
+ font-size: 1.5rem;
133
+ color: var(--accent);
134
+ text-shadow: 0 0 20px var(--accent-glow);
135
+ }
136
+
137
+ .brand h1 {
138
+ font-size: 1.2rem;
139
+ letter-spacing: -0.5px;
140
+ background: linear-gradient(135deg, var(--text-primary), var(--accent));
141
+ -webkit-background-clip: text;
142
+ background-clip: text;
143
+ -webkit-text-fill-color: transparent;
144
+ }
145
+
146
+ .nav-menu {
147
+ flex: 1;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 4px;
151
+ }
152
+
153
+ .nav-item {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 0.7rem;
157
+ padding: 0.65rem 0.8rem;
158
+ border: none;
159
+ background: transparent;
160
+ color: var(--text-secondary);
161
+ border-radius: var(--radius-sm);
162
+ cursor: pointer;
163
+ transition: var(--transition);
164
+ text-align: left;
165
+ font-size: 0.88rem;
166
+ }
167
+
168
+ .nav-item:hover {
169
+ background: var(--surface-hover);
170
+ color: var(--text-primary);
171
+ }
172
+
173
+ .nav-item.active {
174
+ background: var(--accent-glow);
175
+ color: var(--accent);
176
+ font-weight: 500;
177
+ }
178
+
179
+ .nav-section-title {
180
+ font-size: 0.65rem;
181
+ text-transform: uppercase;
182
+ letter-spacing: 0.8px;
183
+ color: var(--text-muted);
184
+ font-weight: 600;
185
+ margin: 1.2rem 0 0.4rem 0.8rem;
186
+ }
187
+
188
+ .nav-divider {
189
+ height: 1px;
190
+ background: var(--glass-border);
191
+ margin: 0.5rem 0;
192
+ }
193
+
194
+ .status-panel {
195
+ padding: 0.8rem;
196
+ background: var(--surface);
197
+ border-radius: var(--radius-sm);
198
+ margin-top: auto;
199
+ }
200
+
201
+ .status-item {
202
+ display: flex;
203
+ justify-content: space-between;
204
+ align-items: center;
205
+ padding: 0.3rem 0;
206
+ }
207
+
208
+ .status-label {
209
+ font-size: 0.75rem;
210
+ color: var(--text-muted);
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.5px;
213
+ }
214
+
215
+ .status-value {
216
+ font-size: 0.78rem;
217
+ color: var(--text-secondary);
218
+ font-weight: 500;
219
+ }
220
+
221
+ .disclaimer-badge {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 0.5rem;
225
+ padding: 0.6rem 0.8rem;
226
+ margin-top: 0.8rem;
227
+ background: var(--medium-bg);
228
+ border: 1px solid rgba(234, 179, 8, 0.2);
229
+ border-radius: var(--radius-xs);
230
+ color: var(--medium);
231
+ font-size: 0.72rem;
232
+ font-weight: 500;
233
+ letter-spacing: 0.3px;
234
+ }
235
+
236
+ /* ── Main Content ──────────────────────────────────────────────────── */
237
+ .main-content {
238
+ flex: 1;
239
+ overflow-y: auto;
240
+ padding: 1.5rem 2rem;
241
+ position: relative;
242
+ }
243
+
244
+ .view {
245
+ display: none;
246
+ animation: fadeIn 0.3s ease;
247
+ }
248
+
249
+ .view.active {
250
+ display: block;
251
+ }
252
+
253
+ .hidden {
254
+ display: none !important;
255
+ }
256
+
257
+ @keyframes fadeIn {
258
+ from {
259
+ opacity: 0;
260
+ transform: translateY(8px);
261
+ }
262
+
263
+ to {
264
+ opacity: 1;
265
+ transform: translateY(0);
266
+ }
267
+ }
268
+
269
+ .view-header {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 1rem;
273
+ margin-bottom: 1.5rem;
274
+ }
275
+
276
+ .view-header h2 {
277
+ font-size: 1.3rem;
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 0.6rem;
281
+ }
282
+
283
+ .view-header h2 i {
284
+ color: var(--accent);
285
+ font-size: 1.1rem;
286
+ }
287
+
288
+ .header-actions {
289
+ margin-left: auto;
290
+ display: flex;
291
+ gap: 0.6rem;
292
+ }
293
+
294
+ /* ── Buttons ───────────────────────────────────────────────────────── */
295
+ .btn-primary {
296
+ background: linear-gradient(135deg, var(--accent), var(--accent-hover));
297
+ color: white;
298
+ border: none;
299
+ padding: 0.6rem 1.2rem;
300
+ border-radius: var(--radius-sm);
301
+ cursor: pointer;
302
+ font-weight: 500;
303
+ display: flex;
304
+ align-items: center;
305
+ gap: 0.4rem;
306
+ transition: var(--transition);
307
+ box-shadow: 0 2px 10px var(--accent-glow);
308
+ }
309
+
310
+ .btn-primary:hover {
311
+ transform: translateY(-1px);
312
+ box-shadow: 0 4px 15px var(--accent-glow);
313
+ }
314
+
315
+ .btn-primary:disabled {
316
+ opacity: 0.5;
317
+ cursor: not-allowed;
318
+ transform: none;
319
+ }
320
+
321
+ .btn-secondary {
322
+ background: var(--surface);
323
+ color: var(--text-secondary);
324
+ border: 1px solid var(--glass-border);
325
+ padding: 0.6rem 1rem;
326
+ border-radius: var(--radius-sm);
327
+ cursor: pointer;
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.4rem;
331
+ transition: var(--transition);
332
+ }
333
+
334
+ .btn-secondary:hover {
335
+ background: var(--surface-hover);
336
+ color: var(--text-primary);
337
+ }
338
+
339
+ .btn-secondary:disabled {
340
+ opacity: 0.5;
341
+ cursor: not-allowed;
342
+ }
343
+
344
+ .btn-large {
345
+ padding: 0.8rem 2rem;
346
+ font-size: 1rem;
347
+ }
348
+
349
+ .btn-back {
350
+ background: transparent;
351
+ border: none;
352
+ color: var(--text-secondary);
353
+ cursor: pointer;
354
+ font-size: 1.1rem;
355
+ padding: 0.4rem;
356
+ transition: var(--transition);
357
+ }
358
+
359
+ .btn-back:hover {
360
+ color: var(--accent);
361
+ }
362
+
363
+ /* ── Glass Panel ───────────────────────────────────────────────────── */
364
+ .glass {
365
+ background: var(--glass);
366
+ backdrop-filter: blur(20px);
367
+ border: 2px solid var(--accent);
368
+ /* thick dark blue border */
369
+ border-radius: var(--radius);
370
+ }
371
+
372
+ /* ── Cases Grid (Dashboard) ────────────────────────────────────────── */
373
+ .cases-grid {
374
+ display: grid;
375
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
376
+ gap: 1rem;
377
+ }
378
+
379
+ .case-card {
380
+ background: var(--surface);
381
+ border: 2px solid var(--accent);
382
+ /* thick dark blue border */
383
+ border-radius: var(--radius);
384
+ padding: 1.2rem;
385
+ cursor: pointer;
386
+ transition: var(--transition);
387
+ position: relative;
388
+ overflow: hidden;
389
+ }
390
+
391
+ .case-card::before {
392
+ content: '';
393
+ position: absolute;
394
+ top: 0;
395
+ left: 0;
396
+ width: 100%;
397
+ height: 3px;
398
+ background: var(--accent);
399
+ opacity: 0;
400
+ transition: var(--transition);
401
+ }
402
+
403
+ .case-card:hover {
404
+ border-color: var(--accent);
405
+ transform: translateY(-2px);
406
+ }
407
+
408
+ .case-card:hover::before {
409
+ opacity: 1;
410
+ }
411
+
412
+ .case-card .card-header {
413
+ display: flex;
414
+ justify-content: space-between;
415
+ align-items: center;
416
+ margin-bottom: 0.6rem;
417
+ }
418
+
419
+ .case-card .case-number {
420
+ font-weight: 600;
421
+ font-size: 1rem;
422
+ color: var(--text-primary);
423
+ }
424
+
425
+ .case-card .case-meta {
426
+ display: flex;
427
+ flex-direction: column;
428
+ gap: 0.25rem;
429
+ font-size: 0.82rem;
430
+ color: var(--text-secondary);
431
+ }
432
+
433
+ .case-card .case-meta span {
434
+ display: flex;
435
+ align-items: center;
436
+ gap: 0.4rem;
437
+ }
438
+
439
+ .case-card .case-meta i {
440
+ width: 14px;
441
+ text-align: center;
442
+ color: var(--text-muted);
443
+ font-size: 0.78rem;
444
+ }
445
+
446
+ .case-card .card-footer {
447
+ display: flex;
448
+ justify-content: space-between;
449
+ align-items: center;
450
+ margin-top: 0.8rem;
451
+ padding-top: 0.8rem;
452
+ border-top: 1px solid var(--glass-border);
453
+ }
454
+
455
+ .case-card .photo-count {
456
+ font-size: 0.78rem;
457
+ color: var(--text-muted);
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 0.3rem;
461
+ }
462
+
463
+ /* Status badge */
464
+ .status-badge {
465
+ display: inline-flex;
466
+ align-items: center;
467
+ gap: 0.3rem;
468
+ padding: 0.2rem 0.6rem;
469
+ border-radius: 999px;
470
+ font-size: 0.7rem;
471
+ font-weight: 600;
472
+ text-transform: uppercase;
473
+ letter-spacing: 0.5px;
474
+ }
475
+
476
+ .status-badge.pending {
477
+ background: rgba(139, 151, 176, 0.15);
478
+ color: var(--pending);
479
+ }
480
+
481
+ .status-badge.analyzing {
482
+ background: rgba(59, 130, 246, 0.15);
483
+ color: var(--analyzing);
484
+ }
485
+
486
+ .status-badge.complete {
487
+ background: rgba(34, 197, 94, 0.15);
488
+ color: var(--complete);
489
+ }
490
+
491
+ .status-badge.error {
492
+ background: rgba(239, 68, 68, 0.15);
493
+ color: var(--error);
494
+ }
495
+
496
+ .status-badge i {
497
+ font-size: 0.5rem;
498
+ }
499
+
500
+ /* ── Empty State ───────────────────────────────────────────────────── */
501
+ .empty-state {
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ padding: 4rem 2rem;
507
+ text-align: center;
508
+ }
509
+
510
+ .empty-state i {
511
+ font-size: 3rem;
512
+ color: var(--text-muted);
513
+ margin-bottom: 1rem;
514
+ opacity: 0.4;
515
+ }
516
+
517
+ .empty-state h3 {
518
+ font-size: 1.2rem;
519
+ color: var(--text-secondary);
520
+ margin-bottom: 0.5rem;
521
+ }
522
+
523
+ .empty-state p {
524
+ color: var(--text-muted);
525
+ margin-bottom: 1.5rem;
526
+ }
527
+
528
+ /* ── New Case Form ─────────────────────────────────────────────────── */
529
+ .form-container {
530
+ padding: 1.5rem;
531
+ }
532
+
533
+ .form-grid {
534
+ display: grid;
535
+ grid-template-columns: 1fr 1fr;
536
+ gap: 1rem;
537
+ margin-bottom: 1.5rem;
538
+ }
539
+
540
+ .form-group {
541
+ display: flex;
542
+ flex-direction: column;
543
+ gap: 0.3rem;
544
+ }
545
+
546
+ .form-group.full-width {
547
+ grid-column: 1 / -1;
548
+ }
549
+
550
+ .form-group label {
551
+ font-size: 0.78rem;
552
+ font-weight: 500;
553
+ color: var(--text-secondary);
554
+ text-transform: uppercase;
555
+ letter-spacing: 0.5px;
556
+ }
557
+
558
+ .form-group input,
559
+ .form-group textarea,
560
+ .form-group select {
561
+ background: var(--bg-secondary);
562
+ border: 1px solid var(--glass-border);
563
+ border-radius: var(--radius-xs);
564
+ padding: 0.6rem 0.8rem;
565
+ color: var(--text-primary);
566
+ /* explicitly dark text for white inputs */
567
+ transition: var(--transition);
568
+ }
569
+
570
+ .form-group input:focus,
571
+ .form-group textarea:focus {
572
+ outline: none;
573
+ border-color: var(--accent);
574
+ box-shadow: 0 0 0 3px var(--accent-glow);
575
+ }
576
+
577
+ .form-group textarea {
578
+ resize: vertical;
579
+ }
580
+
581
+ .form-actions {
582
+ display: flex;
583
+ justify-content: center;
584
+ margin-top: 1rem;
585
+ }
586
+
587
+ /* ── Drop Zone ─────────────────────────────────────────────────────── */
588
+ .drop-zone {
589
+ display: flex;
590
+ flex-direction: column;
591
+ align-items: center;
592
+ justify-content: center;
593
+ padding: 2.5rem 1.5rem;
594
+ border: 2px dashed var(--glass-border);
595
+ border-radius: var(--radius);
596
+ cursor: pointer;
597
+ transition: var(--transition);
598
+ text-align: center;
599
+ }
600
+
601
+ .drop-zone:hover,
602
+ .drop-zone.drag-over {
603
+ border-color: var(--accent);
604
+ background: var(--accent-glow);
605
+ }
606
+
607
+ .drop-zone i {
608
+ font-size: 2.2rem;
609
+ color: var(--accent);
610
+ margin-bottom: 0.8rem;
611
+ opacity: 0.7;
612
+ }
613
+
614
+ .drop-zone p {
615
+ color: var(--text-secondary);
616
+ font-size: 0.95rem;
617
+ margin-bottom: 0.3rem;
618
+ }
619
+
620
+ .drop-zone .sub-text {
621
+ font-size: 0.78rem;
622
+ color: var(--text-muted);
623
+ }
624
+
625
+ /* ── Photo Preview ─────────────────────────────────────────────────── */
626
+ .photo-preview {
627
+ margin-top: 1rem;
628
+ }
629
+
630
+ .photo-thumbnails {
631
+ display: flex;
632
+ flex-wrap: wrap;
633
+ gap: 0.5rem;
634
+ margin-bottom: 0.5rem;
635
+ }
636
+
637
+ .photo-thumbnail {
638
+ width: 70px;
639
+ height: 70px;
640
+ border-radius: var(--radius-xs);
641
+ overflow: hidden;
642
+ border: 2px solid var(--glass-border);
643
+ }
644
+
645
+ .photo-thumbnail img {
646
+ width: 100%;
647
+ height: 100%;
648
+ object-fit: cover;
649
+ }
650
+
651
+ .photo-count {
652
+ font-size: 0.82rem;
653
+ color: var(--text-secondary);
654
+ }
655
+
656
+ /* ── Progress ────────────────────────────────────────────��─────────── */
657
+ .progress-section {
658
+ margin-top: 1rem;
659
+ }
660
+
661
+ .progress-bar {
662
+ width: 100%;
663
+ height: 6px;
664
+ background: var(--bg-secondary);
665
+ border-radius: 999px;
666
+ overflow: hidden;
667
+ margin-bottom: 0.5rem;
668
+ }
669
+
670
+ .progress-fill {
671
+ height: 100%;
672
+ background: linear-gradient(90deg, var(--accent), #818cf8);
673
+ border-radius: 999px;
674
+ width: 0%;
675
+ transition: width 0.5s ease;
676
+ }
677
+
678
+ #upload-status-text {
679
+ font-size: 0.82rem;
680
+ color: var(--text-secondary);
681
+ }
682
+
683
+ /* ── Case Detail ───────────────────────────────────────────────────── */
684
+ .case-info-bar {
685
+ display: flex;
686
+ flex-wrap: wrap;
687
+ gap: 0.8rem;
688
+ padding: 0.8rem 1rem;
689
+ margin-bottom: 1.5rem;
690
+ }
691
+
692
+ .info-chip {
693
+ display: flex;
694
+ align-items: center;
695
+ gap: 0.4rem;
696
+ font-size: 0.82rem;
697
+ color: var(--text-secondary);
698
+ padding: 0.3rem 0.7rem;
699
+ background: var(--surface);
700
+ border-radius: 999px;
701
+ }
702
+
703
+ .info-chip i {
704
+ color: var(--accent);
705
+ font-size: 0.75rem;
706
+ }
707
+
708
+ .status-chip.pending i {
709
+ color: var(--pending);
710
+ }
711
+
712
+ .status-chip.analyzing i {
713
+ color: var(--analyzing);
714
+ }
715
+
716
+ .status-chip.complete i {
717
+ color: var(--complete);
718
+ }
719
+
720
+ .status-chip.error i {
721
+ color: var(--error);
722
+ }
723
+
724
+ /* ── Analysis Grid ─────────────────────────────────────────────────── */
725
+ .analysis-grid {
726
+ display: grid;
727
+ grid-template-columns: 1fr 1fr;
728
+ gap: 1rem;
729
+ }
730
+
731
+ .panel {
732
+ padding: 1rem;
733
+ min-height: 200px;
734
+ max-height: 500px;
735
+ overflow-y: auto;
736
+ }
737
+
738
+ .panel-header {
739
+ display: flex;
740
+ justify-content: space-between;
741
+ align-items: center;
742
+ margin-bottom: 0.8rem;
743
+ padding-bottom: 0.6rem;
744
+ border-bottom: 1px solid var(--glass-border);
745
+ }
746
+
747
+ .panel-header h3 {
748
+ font-size: 0.9rem;
749
+ display: flex;
750
+ align-items: center;
751
+ gap: 0.5rem;
752
+ color: var(--text-primary);
753
+ }
754
+
755
+ .panel-header h3 i {
756
+ color: var(--accent);
757
+ font-size: 0.85rem;
758
+ }
759
+
760
+ .badge {
761
+ background: var(--surface);
762
+ color: var(--text-secondary);
763
+ padding: 0.15rem 0.5rem;
764
+ border-radius: 999px;
765
+ font-size: 0.72rem;
766
+ font-weight: 600;
767
+ }
768
+
769
+ .badge.danger {
770
+ background: var(--critical-bg);
771
+ color: var(--critical);
772
+ }
773
+
774
+ .placeholder-text {
775
+ color: var(--text-muted);
776
+ font-size: 0.85rem;
777
+ font-style: italic;
778
+ }
779
+
780
+ /* ── Detail Photos Grid ────────────────────────────────────────────── */
781
+ .detail-photos-grid {
782
+ display: grid;
783
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
784
+ gap: 0.5rem;
785
+ }
786
+
787
+ .detail-photo {
788
+ aspect-ratio: 4/3;
789
+ border-radius: var(--radius-xs);
790
+ overflow: hidden;
791
+ cursor: pointer;
792
+ border: 2px solid transparent;
793
+ transition: var(--transition);
794
+ }
795
+
796
+ .detail-photo:hover {
797
+ border-color: var(--accent);
798
+ transform: scale(1.02);
799
+ }
800
+
801
+ .detail-photo img {
802
+ width: 100%;
803
+ height: 100%;
804
+ object-fit: cover;
805
+ }
806
+
807
+ /* ── Analysis Content ──────────────────────────────────────────────── */
808
+ .analysis-content {
809
+ font-size: 0.85rem;
810
+ line-height: 1.7;
811
+ color: var(--text-secondary);
812
+ }
813
+
814
+ .analysis-content pre {
815
+ white-space: pre-wrap;
816
+ word-break: break-word;
817
+ font-family: var(--font-body);
818
+ }
819
+
820
+ .analysis-photo-section {
821
+ margin-bottom: 1rem;
822
+ }
823
+
824
+ .analysis-photo-label {
825
+ font-weight: 600;
826
+ color: var(--accent);
827
+ margin-bottom: 0.3rem;
828
+ font-size: 0.82rem;
829
+ }
830
+
831
+ /* ── Violations List ───────────────────────────────────────────────── */
832
+ .violation-card {
833
+ background: var(--surface);
834
+ border: 2px solid var(--accent);
835
+ /* thick dark blue border */
836
+ border-radius: var(--radius-sm);
837
+ padding: 0.8rem;
838
+ margin-bottom: 0.5rem;
839
+ border-left: 6px solid var(--text-muted);
840
+ transition: var(--transition);
841
+ }
842
+
843
+ .violation-card:hover {
844
+ background: var(--surface-hover);
845
+ }
846
+
847
+ .violation-card.CRITICAL {
848
+ border-left-color: var(--critical);
849
+ }
850
+
851
+ .violation-card.HIGH {
852
+ border-left-color: var(--high);
853
+ }
854
+
855
+ .violation-card.MEDIUM {
856
+ border-left-color: var(--medium);
857
+ }
858
+
859
+ .violation-card.LOW {
860
+ border-left-color: var(--low);
861
+ }
862
+
863
+ .violation-header {
864
+ display: flex;
865
+ justify-content: space-between;
866
+ align-items: center;
867
+ margin-bottom: 0.3rem;
868
+ }
869
+
870
+ .violation-title {
871
+ font-weight: 600;
872
+ font-size: 0.85rem;
873
+ color: var(--text-primary);
874
+ }
875
+
876
+ .severity-tag {
877
+ font-size: 0.65rem;
878
+ font-weight: 700;
879
+ padding: 0.15rem 0.5rem;
880
+ border-radius: 999px;
881
+ text-transform: uppercase;
882
+ letter-spacing: 0.5px;
883
+ }
884
+
885
+ .severity-tag.CRITICAL {
886
+ background: var(--critical-bg);
887
+ color: var(--critical);
888
+ }
889
+
890
+ .severity-tag.HIGH {
891
+ background: var(--high-bg);
892
+ color: var(--high);
893
+ }
894
+
895
+ .severity-tag.MEDIUM {
896
+ background: var(--medium-bg);
897
+ color: var(--medium);
898
+ }
899
+
900
+ .severity-tag.LOW {
901
+ background: var(--low-bg);
902
+ color: var(--low);
903
+ }
904
+
905
+ .violation-meta {
906
+ font-size: 0.78rem;
907
+ color: var(--text-muted);
908
+ display: flex;
909
+ gap: 1rem;
910
+ }
911
+
912
+ .violation-meta span {
913
+ display: flex;
914
+ align-items: center;
915
+ gap: 0.3rem;
916
+ }
917
+
918
+ .violation-evidence {
919
+ font-size: 0.78rem;
920
+ color: var(--text-secondary);
921
+ margin-top: 0.3rem;
922
+ font-style: italic;
923
+ }
924
+
925
+ /* ── Fault Content ─────────────────────────────────────────────────── */
926
+ .fault-content {
927
+ font-size: 0.85rem;
928
+ }
929
+
930
+ .fault-party-bars {
931
+ margin-bottom: 1rem;
932
+ }
933
+
934
+ .party-bar {
935
+ margin-bottom: 0.6rem;
936
+ }
937
+
938
+ .party-bar-label {
939
+ display: flex;
940
+ justify-content: space-between;
941
+ margin-bottom: 0.25rem;
942
+ font-size: 0.82rem;
943
+ }
944
+
945
+ .party-bar-label .party-name {
946
+ font-weight: 600;
947
+ color: var(--text-primary);
948
+ }
949
+
950
+ .party-bar-label .party-pct {
951
+ color: var(--accent);
952
+ font-weight: 600;
953
+ }
954
+
955
+ .party-bar-track {
956
+ height: 8px;
957
+ background: var(--bg-secondary);
958
+ border-radius: 999px;
959
+ overflow: hidden;
960
+ }
961
+
962
+ .party-bar-fill {
963
+ height: 100%;
964
+ border-radius: 999px;
965
+ transition: width 1s ease;
966
+ }
967
+
968
+ .party-bar-fill.primary {
969
+ background: linear-gradient(90deg, var(--critical), var(--high));
970
+ }
971
+
972
+ .party-bar-fill.secondary {
973
+ background: linear-gradient(90deg, var(--medium), var(--low));
974
+ }
975
+
976
+ .fault-cause {
977
+ background: var(--surface);
978
+ border-radius: var(--radius-sm);
979
+ padding: 0.8rem;
980
+ margin-top: 0.8rem;
981
+ }
982
+
983
+ .fault-cause h4 {
984
+ font-size: 0.82rem;
985
+ color: var(--accent);
986
+ margin-bottom: 0.4rem;
987
+ }
988
+
989
+ .fault-cause p {
990
+ font-size: 0.82rem;
991
+ color: var(--text-secondary);
992
+ line-height: 1.6;
993
+ }
994
+
995
+ .confidence-meter {
996
+ display: flex;
997
+ align-items: center;
998
+ gap: 0.5rem;
999
+ margin-top: 0.8rem;
1000
+ font-size: 0.82rem;
1001
+ color: var(--text-muted);
1002
+ }
1003
+
1004
+ .confidence-meter .meter-bar {
1005
+ flex: 1;
1006
+ height: 4px;
1007
+ background: var(--bg-secondary);
1008
+ border-radius: 999px;
1009
+ overflow: hidden;
1010
+ }
1011
+
1012
+ .confidence-meter .meter-fill {
1013
+ height: 100%;
1014
+ background: var(--accent);
1015
+ border-radius: 999px;
1016
+ }
1017
+
1018
+ /* ── Analysis Overlay ──────────────────────────────────────────────── */
1019
+ .analysis-overlay {
1020
+ position: absolute;
1021
+ top: 0;
1022
+ left: 0;
1023
+ right: 0;
1024
+ bottom: 0;
1025
+ background: rgba(10, 14, 26, 0.85);
1026
+ display: flex;
1027
+ align-items: center;
1028
+ justify-content: center;
1029
+ z-index: 100;
1030
+ backdrop-filter: blur(5px);
1031
+ }
1032
+
1033
+ .analysis-overlay.hidden {
1034
+ display: none;
1035
+ }
1036
+
1037
+ .overlay-content {
1038
+ text-align: center;
1039
+ padding: 2.5rem 3rem;
1040
+ border-radius: var(--radius);
1041
+ }
1042
+
1043
+ .overlay-content h3 {
1044
+ margin: 1rem 0 0.5rem;
1045
+ color: var(--text-primary);
1046
+ }
1047
+
1048
+ .overlay-content p {
1049
+ color: var(--text-secondary);
1050
+ font-size: 0.88rem;
1051
+ }
1052
+
1053
+ /* ── Spinner ───────────────────────────────────────────────────────── */
1054
+ .spinner-large {
1055
+ width: 48px;
1056
+ height: 48px;
1057
+ border: 4px solid var(--glass-border);
1058
+ border-top-color: var(--accent);
1059
+ border-radius: 50%;
1060
+ margin: 0 auto;
1061
+ animation: spin 1s linear infinite;
1062
+ }
1063
+
1064
+ @keyframes spin {
1065
+ to {
1066
+ transform: rotate(360deg);
1067
+ }
1068
+ }
1069
+
1070
+ /* ── Report Content ────────────────────────────────────────────────── */
1071
+ .report-content {
1072
+ padding: 2rem;
1073
+ max-width: 900px;
1074
+ margin: 0 auto;
1075
+ line-height: 1.8;
1076
+ }
1077
+
1078
+ .report-content h3 {
1079
+ font-size: 1.1rem;
1080
+ color: var(--accent);
1081
+ margin: 1.5rem 0 0.5rem;
1082
+ padding-bottom: 0.3rem;
1083
+ border-bottom: 1px solid var(--glass-border);
1084
+ }
1085
+
1086
+ .report-content h4 {
1087
+ font-size: 0.95rem;
1088
+ color: var(--text-primary);
1089
+ margin: 1rem 0 0.3rem;
1090
+ }
1091
+
1092
+ .report-content p {
1093
+ font-size: 0.88rem;
1094
+ color: var(--text-secondary);
1095
+ margin-bottom: 0.5rem;
1096
+ }
1097
+
1098
+ .report-header {
1099
+ text-align: center;
1100
+ margin-bottom: 2rem;
1101
+ padding-bottom: 1.5rem;
1102
+ border-bottom: 2px solid var(--accent);
1103
+ }
1104
+
1105
+ .report-header h2 {
1106
+ font-size: 1.4rem;
1107
+ margin-bottom: 0.3rem;
1108
+ }
1109
+
1110
+ .report-header .report-subtitle {
1111
+ color: var(--text-muted);
1112
+ font-size: 0.88rem;
1113
+ }
1114
+
1115
+ .report-disclaimer {
1116
+ background: var(--medium-bg);
1117
+ border: 1px solid rgba(234, 179, 8, 0.2);
1118
+ border-radius: var(--radius-sm);
1119
+ padding: 0.8rem 1rem;
1120
+ font-size: 0.78rem;
1121
+ color: var(--medium);
1122
+ line-height: 1.6;
1123
+ margin-bottom: 1.5rem;
1124
+ }
1125
+
1126
+ .report-stat-grid {
1127
+ display: grid;
1128
+ grid-template-columns: repeat(4, 1fr);
1129
+ gap: 0.8rem;
1130
+ margin-bottom: 1.5rem;
1131
+ }
1132
+
1133
+ .report-stat {
1134
+ text-align: center;
1135
+ background: var(--surface);
1136
+ border-radius: var(--radius-sm);
1137
+ padding: 0.8rem;
1138
+ }
1139
+
1140
+ .report-stat .stat-number {
1141
+ font-size: 1.8rem;
1142
+ font-weight: 700;
1143
+ color: var(--accent);
1144
+ }
1145
+
1146
+ .report-stat .stat-label {
1147
+ font-size: 0.72rem;
1148
+ color: var(--text-muted);
1149
+ text-transform: uppercase;
1150
+ letter-spacing: 0.5px;
1151
+ }
1152
+
1153
+ /* ── Rules View ────────────────────────────────────────────────────── */
1154
+ .rules-content {
1155
+ display: flex;
1156
+ flex-direction: column;
1157
+ gap: 1.5rem;
1158
+ }
1159
+
1160
+ .rule-category {
1161
+ margin-bottom: 1.5rem;
1162
+ }
1163
+
1164
+ .rule-category-header {
1165
+ font-size: 1rem;
1166
+ color: var(--text-primary);
1167
+ padding: 0.6rem 1rem;
1168
+ background: var(--surface);
1169
+ border-radius: var(--radius-sm);
1170
+ margin-bottom: 0.5rem;
1171
+ display: flex;
1172
+ align-items: center;
1173
+ gap: 0.5rem;
1174
+ cursor: pointer;
1175
+ transition: var(--transition);
1176
+ }
1177
+
1178
+ .rule-category-header:hover {
1179
+ background: var(--surface-hover);
1180
+ }
1181
+
1182
+ .rule-category-header .rule-count {
1183
+ margin-left: auto;
1184
+ color: var(--text-muted);
1185
+ font-size: 0.82rem;
1186
+ }
1187
+
1188
+ .rule-list {
1189
+ padding-left: 0.5rem;
1190
+ }
1191
+
1192
+ .rule-item {
1193
+ padding: 0.4rem 0.8rem;
1194
+ margin-bottom: 0.25rem;
1195
+ font-size: 0.82rem;
1196
+ color: var(--text-secondary);
1197
+ display: flex;
1198
+ align-items: center;
1199
+ gap: 0.5rem;
1200
+ }
1201
+
1202
+ .rule-item .rule-id {
1203
+ color: var(--text-muted);
1204
+ font-family: monospace;
1205
+ font-size: 0.75rem;
1206
+ min-width: 90px;
1207
+ }
1208
+
1209
+ /* ── Toast ─────────────────────────────────────────────────────────── */
1210
+ #toast-container {
1211
+ position: fixed;
1212
+ top: 1rem;
1213
+ right: 1rem;
1214
+ z-index: 1000;
1215
+ display: flex;
1216
+ flex-direction: column;
1217
+ gap: 0.5rem;
1218
+ }
1219
+
1220
+ .toast {
1221
+ padding: 0.7rem 1.2rem;
1222
+ border-radius: var(--radius-sm);
1223
+ font-size: 0.85rem;
1224
+ animation: slideIn 0.3s ease, fadeOut 0.5s ease 3.5s forwards;
1225
+ max-width: 350px;
1226
+ backdrop-filter: blur(10px);
1227
+ }
1228
+
1229
+ .toast.success {
1230
+ background: rgba(34, 197, 94, 0.9);
1231
+ color: white;
1232
+ }
1233
+
1234
+ .toast.error {
1235
+ background: rgba(239, 68, 68, 0.9);
1236
+ color: white;
1237
+ }
1238
+
1239
+ .toast.info {
1240
+ background: rgba(59, 130, 246, 0.9);
1241
+ color: white;
1242
+ }
1243
+
1244
+ @keyframes slideIn {
1245
+ from {
1246
+ transform: translateX(100%);
1247
+ opacity: 0;
1248
+ }
1249
+
1250
+ to {
1251
+ transform: translateX(0);
1252
+ opacity: 1;
1253
+ }
1254
+ }
1255
+
1256
+ @keyframes fadeOut {
1257
+ to {
1258
+ opacity: 0;
1259
+ }
1260
+ }
1261
+
1262
+ /* ── Responsive ────────────────────────────────────────────────────── */
1263
+ @media (max-width: 768px) {
1264
+ .sidebar {
1265
+ display: none;
1266
+ }
1267
+
1268
+ .main-content {
1269
+ padding: 1rem;
1270
+ }
1271
+
1272
+ .form-grid {
1273
+ grid-template-columns: 1fr;
1274
+ }
1275
+
1276
+ .analysis-grid {
1277
+ grid-template-columns: 1fr;
1278
+ }
1279
+
1280
+ .case-info-bar {
1281
+ flex-direction: column;
1282
+ }
1283
+
1284
+ .report-stat-grid {
1285
+ grid-template-columns: repeat(2, 1fr);
1286
+ }
1287
+ }
1288
+
1289
+ /* ── Scrollbar ─────────────────────────────────────────────────────── */
1290
+ ::-webkit-scrollbar {
1291
+ width: 6px;
1292
+ }
1293
+
1294
+ ::-webkit-scrollbar-track {
1295
+ background: transparent;
1296
+ }
1297
+
1298
+ ::-webkit-scrollbar-thumb {
1299
+ background: var(--glass-border);
1300
+ border-radius: 999px;
1301
+ }
1302
+
1303
+ ::-webkit-scrollbar-thumb:hover {
1304
+ background: var(--text-muted);
1305
+ }
1306
+
1307
+ /* ── Modals ────────────────────────────────────────────────────────── */
1308
+ .modal {
1309
+ position: fixed;
1310
+ top: 0;
1311
+ left: 0;
1312
+ width: 100%;
1313
+ height: 100%;
1314
+ background: rgba(0, 0, 0, 0.6);
1315
+ display: flex;
1316
+ align-items: center;
1317
+ justify-content: center;
1318
+ z-index: 1000;
1319
+ backdrop-filter: blur(4px);
1320
+ transition: opacity 0.3s ease;
1321
+ }
1322
+
1323
+ .modal.hidden {
1324
+ display: none;
1325
+ opacity: 0;
1326
+ pointer-events: none;
1327
+ }
1328
+
1329
+ .modal-content {
1330
+ width: 90%;
1331
+ max-width: 600px;
1332
+ max-height: 90vh;
1333
+ overflow-y: auto;
1334
+ padding: 2rem;
1335
+ position: relative;
1336
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
1337
+ }
1338
+
1339
+ .modal-header {
1340
+ display: flex;
1341
+ justify-content: space-between;
1342
+ align-items: center;
1343
+ margin-bottom: 1.5rem;
1344
+ }
1345
+
1346
+ .modal-header h2 {
1347
+ font-size: 1.3rem;
1348
+ margin: 0;
1349
+ }
1350
+
1351
+ .btn-close {
1352
+ background: transparent;
1353
+ border: none;
1354
+ color: var(--text-secondary);
1355
+ font-size: 1.8rem;
1356
+ cursor: pointer;
1357
+ line-height: 1;
1358
+ }
1359
+
1360
+ .modal-footer {
1361
+ display: flex;
1362
+ justify-content: flex-end;
1363
+ gap: 1rem;
1364
+ margin-top: 2rem;
1365
+ padding-top: 1rem;
1366
+ border-top: 1px solid var(--glass-border);
1367
+ }
1368
+
1369
+ /* ── UI Enhancements ──────────────────────────────────────────────── */
1370
+ .panel-actions {
1371
+ display: flex;
1372
+ align-items: center;
1373
+ gap: 0.8rem;
1374
+ }
1375
+
1376
+ .btn-icon-sm {
1377
+ background: var(--surface);
1378
+ border: 1px solid var(--glass-border);
1379
+ color: var(--text-secondary);
1380
+ width: 28px;
1381
+ height: 28px;
1382
+ border-radius: 999px;
1383
+ display: flex;
1384
+ align-items: center;
1385
+ justify-content: center;
1386
+ cursor: pointer;
1387
+ transition: var(--transition);
1388
+ }
1389
+
1390
+ .btn-icon-sm:hover {
1391
+ color: var(--accent);
1392
+ border-color: var(--accent);
1393
+ background: var(--accent-glow);
1394
+ }
1395
+
1396
+ /* Delete Icon in Card */
1397
+ .btn-delete-card {
1398
+ position: absolute;
1399
+ top: 0.8rem;
1400
+ right: 0.8rem;
1401
+ background: rgba(239, 68, 68, 0.1);
1402
+ color: var(--critical);
1403
+ border: 1px solid rgba(239, 68, 68, 0.2);
1404
+ width: 24px;
1405
+ height: 24px;
1406
+ border-radius: 4px;
1407
+ display: flex;
1408
+ align-items: center;
1409
+ justify-content: center;
1410
+ cursor: pointer;
1411
+ transition: var(--transition);
1412
+ z-index: 10;
1413
+ opacity: 0.4;
1414
+ }
1415
+
1416
+ .case-card:hover .btn-delete-card {
1417
+ opacity: 1;
1418
+ }
1419
+
1420
+ .btn-delete-card:hover {
1421
+ background: var(--critical);
1422
+ color: white;
1423
+ border-color: var(--critical);
1424
+ }
1425
+
1426
+ /* ── Landing Page ─────────────────────────────────────────────────── */
1427
+ #view-landing {
1428
+ display: flex;
1429
+ flex-direction: column;
1430
+ align-items: center;
1431
+ max-width: 1000px;
1432
+ margin: 0 auto;
1433
+ padding: 2rem 1rem;
1434
+ animation: fadeIn 0.8s ease-out;
1435
+ }
1436
+
1437
+ @keyframes fadeIn {
1438
+ from {
1439
+ opacity: 0;
1440
+ transform: translateY(10px);
1441
+ }
1442
+
1443
+ to {
1444
+ opacity: 1;
1445
+ transform: translateY(0);
1446
+ }
1447
+ }
1448
+
1449
+ .landing-hero {
1450
+ text-align: center;
1451
+ margin-bottom: 4rem;
1452
+ }
1453
+
1454
+ .landing-title {
1455
+ font-size: 4.5rem;
1456
+ font-weight: 800;
1457
+ color: var(--accent);
1458
+ margin-bottom: 1rem;
1459
+ letter-spacing: -1px;
1460
+ background: linear-gradient(135deg, var(--accent) 0%, #3b82f6 100%);
1461
+ -webkit-background-clip: text;
1462
+ background-clip: text;
1463
+ -webkit-text-fill-color: transparent;
1464
+ }
1465
+
1466
+ .landing-subtitle {
1467
+ font-size: 1.6rem;
1468
+ color: var(--text-primary);
1469
+ font-weight: 600;
1470
+ margin-bottom: 1.5rem;
1471
+ line-height: 1.3;
1472
+ }
1473
+
1474
+ .landing-description {
1475
+ font-size: 1.15rem;
1476
+ color: var(--text-secondary);
1477
+ max-width: 800px;
1478
+ margin: 0 auto;
1479
+ line-height: 1.6;
1480
+ }
1481
+
1482
+ .landing-use-cases {
1483
+ width: 100%;
1484
+ }
1485
+
1486
+ .use-cases-title {
1487
+ font-size: 1.8rem;
1488
+ color: var(--text-primary);
1489
+ margin-bottom: 2rem;
1490
+ text-align: center;
1491
+ font-weight: 700;
1492
+ }
1493
+
1494
+ /* ── Modern Accordion ─────────────────────────────────────────────── */
1495
+ .accordion-container {
1496
+ display: flex;
1497
+ flex-direction: column;
1498
+ gap: 1rem;
1499
+ }
1500
+
1501
+ .accordion-item {
1502
+ border-radius: var(--radius);
1503
+ overflow: hidden;
1504
+ transition: box-shadow 0.3s ease, border-color 0.3s ease;
1505
+ }
1506
+
1507
+ .accordion-item:hover {
1508
+ box-shadow: 0 4px 20px var(--accent-glow);
1509
+ border-color: var(--accent);
1510
+ }
1511
+
1512
+ .accordion-header {
1513
+ width: 100%;
1514
+ display: flex;
1515
+ justify-content: space-between;
1516
+ align-items: center;
1517
+ padding: 1.2rem 1.5rem;
1518
+ background: transparent;
1519
+ border: none;
1520
+ color: var(--text-primary);
1521
+ font-size: 1.2rem;
1522
+ font-weight: 600;
1523
+ cursor: pointer;
1524
+ text-align: left;
1525
+ transition: color 0.3s ease;
1526
+ }
1527
+
1528
+ .accordion-header i:first-child {
1529
+ margin-right: 0.8rem;
1530
+ color: var(--accent);
1531
+ width: 24px;
1532
+ text-align: center;
1533
+ }
1534
+
1535
+ .accordion-icon {
1536
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1537
+ color: var(--text-secondary);
1538
+ font-size: 1rem;
1539
+ }
1540
+
1541
+ .accordion-header:hover {
1542
+ color: var(--accent);
1543
+ }
1544
+
1545
+ /* Modern smooth height transition using Grid */
1546
+ .accordion-body {
1547
+ display: grid;
1548
+ grid-template-rows: 0fr;
1549
+ transition: grid-template-rows 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1550
+ background: rgba(255, 255, 255, 0.4);
1551
+ }
1552
+
1553
+ .accordion-content {
1554
+ overflow: hidden;
1555
+ }
1556
+
1557
+ .accordion-content p {
1558
+ padding: 0 1.5rem 1.2rem 3.5rem;
1559
+ margin: 0;
1560
+ color: var(--text-secondary);
1561
+ line-height: 1.6;
1562
+ }
1563
+
1564
+ .accordion-content p strong {
1565
+ color: var(--text-primary);
1566
+ }
1567
+
1568
+ .accordion-content p:first-child {
1569
+ padding-top: 0.5rem;
1570
+ }
1571
+
1572
+ /* Active State */
1573
+ .accordion-item.active .accordion-body {
1574
+ grid-template-rows: 1fr;
1575
+ }
1576
+
1577
+ .accordion-item.active .accordion-icon {
1578
+ transform: rotate(180deg);
1579
+ color: var(--accent);
1580
+ }
1581
+
1582
+ .accordion-item.active .accordion-header {
1583
+ color: var(--accent);
1584
+ }
1585
+
1586
+ /* ── Sidebar Collapsible Widgets ──────────────────────────────────── */
1587
+ .sidebar-collapsible {
1588
+ margin-bottom: 0.4rem;
1589
+ }
1590
+
1591
+ .sidebar-collapse-btn {
1592
+ width: 100%;
1593
+ display: flex;
1594
+ justify-content: space-between;
1595
+ align-items: center;
1596
+ padding: 0.55rem 0.8rem;
1597
+ background: transparent;
1598
+ border: none;
1599
+ color: var(--text-secondary);
1600
+ font-size: 0.82rem;
1601
+ font-weight: 500;
1602
+ cursor: pointer;
1603
+ border-radius: var(--radius-sm);
1604
+ transition: var(--transition);
1605
+ text-align: left;
1606
+ }
1607
+
1608
+ .sidebar-collapse-btn span {
1609
+ display: flex;
1610
+ align-items: center;
1611
+ gap: 0.55rem;
1612
+ }
1613
+
1614
+ .sidebar-collapse-btn span i {
1615
+ color: var(--accent);
1616
+ font-size: 0.85rem;
1617
+ width: 16px;
1618
+ text-align: center;
1619
+ }
1620
+
1621
+ .sidebar-collapse-btn:hover {
1622
+ background: var(--surface-hover);
1623
+ color: var(--text-primary);
1624
+ }
1625
+
1626
+ .sidebar-collapse-icon {
1627
+ font-size: 0.65rem;
1628
+ color: var(--text-muted);
1629
+ transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
1630
+ }
1631
+
1632
+ .sidebar-collapsible.open .sidebar-collapse-icon {
1633
+ transform: rotate(180deg);
1634
+ color: var(--accent);
1635
+ }
1636
+
1637
+ .sidebar-collapsible.open .sidebar-collapse-btn {
1638
+ color: var(--accent);
1639
+ background: var(--accent-glow);
1640
+ }
1641
+
1642
+ /* Smooth height animation via CSS Grid */
1643
+ .sidebar-collapse-body {
1644
+ display: grid;
1645
+ grid-template-rows: 0fr;
1646
+ transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1);
1647
+ }
1648
+
1649
+ .sidebar-collapsible.open .sidebar-collapse-body {
1650
+ grid-template-rows: 1fr;
1651
+ }
1652
+
1653
+ .sidebar-collapse-content {
1654
+ overflow: hidden;
1655
+ padding: 0 0.8rem;
1656
+ }
1657
+
1658
+ .sidebar-collapsible.open .sidebar-collapse-content {
1659
+ padding: 0.4rem 0.8rem 0.6rem;
1660
+ }
1661
+
1662
+ /* Founder details in sidebar */
1663
+ .sidebar-founder-role {
1664
+ font-size: 0.75rem;
1665
+ color: var(--text-muted);
1666
+ line-height: 1.5;
1667
+ margin-bottom: 0.5rem;
1668
+ }
1669
+
1670
+ .sidebar-linkedin {
1671
+ display: inline-flex;
1672
+ align-items: center;
1673
+ gap: 0.4rem;
1674
+ padding: 0.3rem 0.7rem;
1675
+ background: #0a66c2;
1676
+ color: #ffffff;
1677
+ border-radius: var(--radius-xs);
1678
+ text-decoration: none;
1679
+ font-size: 0.75rem;
1680
+ font-weight: 500;
1681
+ transition: var(--transition);
1682
+ }
1683
+
1684
+ .sidebar-linkedin:hover {
1685
+ background: #004182;
1686
+ transform: translateY(-1px);
1687
+ }
1688
+
1689
+ .sidebar-linkedin i {
1690
+ font-size: 0.85rem;
1691
+ }
1692
+
1693
+ /* Contact details in sidebar */
1694
+ .sidebar-contact-link {
1695
+ display: flex;
1696
+ align-items: center;
1697
+ gap: 0.4rem;
1698
+ color: var(--accent);
1699
+ text-decoration: none;
1700
+ font-size: 0.78rem;
1701
+ font-weight: 500;
1702
+ margin-bottom: 0.4rem;
1703
+ transition: var(--transition);
1704
+ }
1705
+
1706
+ .sidebar-contact-link:hover {
1707
+ color: var(--accent-hover);
1708
+ }
1709
+
1710
+ .sidebar-contact-link i,
1711
+ .sidebar-contact-phone i {
1712
+ color: var(--accent);
1713
+ font-size: 0.78rem;
1714
+ width: 14px;
1715
+ text-align: center;
1716
+ }
1717
+
1718
+ .sidebar-contact-phone {
1719
+ display: flex;
1720
+ align-items: center;
1721
+ gap: 0.4rem;
1722
+ color: var(--text-muted);
1723
+ font-size: 0.75rem;
1724
+ font-style: italic;
1725
+ }
index.html CHANGED
@@ -1,19 +1,560 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </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">
7
+ <title>TraceScene β€” Visual Intelligence for Law Enforcement</title>
8
+ <meta name="description"
9
+ content="AI-powered accident scene analysis: upload photos, detect traffic violations, determine fault.">
10
+
11
+ <!-- Fonts -->
12
+ <link rel="preconnect" href="https://fonts.googleapis.com">
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
+ <link
15
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;700&display=swap"
16
+ rel="stylesheet">
17
+
18
+ <!-- Icons -->
19
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
20
+
21
+ <!-- Styles -->
22
+ <link rel="stylesheet" href="css/styles.css">
23
+ </head>
24
+
25
+ <body>
26
+ <div class="app-container">
27
+ <!-- Sidebar -->
28
+ <aside class="sidebar glass">
29
+ <div class="brand">
30
+ <i class="fa-solid fa-shield-halved"></i>
31
+ <h1>TraceScene</h1>
32
+ </div>
33
+
34
+ <nav class="nav-menu">
35
+ <button class="nav-item active" data-view="landing">
36
+ <i class="fa-solid fa-home"></i> Home
37
+ </button>
38
+
39
+ <div class="nav-section-title">Law Enforcement</div>
40
+ <button class="nav-item" data-view="le-dashboard">
41
+ <i class="fa-solid fa-chart-line"></i> Dashboard
42
+ </button>
43
+ <button class="nav-item" data-view="le-new-case">
44
+ <i class="fa-solid fa-shield-halved"></i> New Case
45
+ </button>
46
+
47
+ <div class="nav-section-title">Insurance</div>
48
+ <button class="nav-item" data-view="ins-dashboard">
49
+ <i class="fa-solid fa-chart-line"></i> Dashboard
50
+ </button>
51
+ <button class="nav-item" data-view="ins-new-case">
52
+ <i class="fa-solid fa-file-invoice-dollar"></i> New Case
53
+ </button>
54
+
55
+ <div class="nav-divider"></div>
56
+
57
+ <button class="nav-item" data-view="rules">
58
+ <i class="fa-solid fa-book-open"></i> Traffic Rules
59
+ </button>
60
+ </nav>
61
+
62
+ <!-- Sidebar: Founder (collapsible) -->
63
+ <div class="sidebar-collapsible">
64
+ <button class="sidebar-collapse-btn" aria-expanded="false"
65
+ onclick="this.parentElement.classList.toggle('open'); this.setAttribute('aria-expanded', this.parentElement.classList.contains('open'))">
66
+ <span><i class="fa-solid fa-user-tie"></i> Built by</span>
67
+ <i class="fa-solid fa-chevron-down sidebar-collapse-icon"></i>
68
+ </button>
69
+ <div class="sidebar-collapse-body">
70
+ <div class="sidebar-collapse-content">
71
+ <a href="https://www.linkedin.com/in/siddharth-ravikumar-17262a50/" target="_blank"
72
+ rel="noopener noreferrer" class="sidebar-linkedin">
73
+ <i class="fa-brands fa-linkedin"></i> LinkedIn
74
+ </a>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Sidebar: Contact (collapsible) -->
80
+ <div class="sidebar-collapsible">
81
+ <button class="sidebar-collapse-btn" aria-expanded="false"
82
+ onclick="this.parentElement.classList.toggle('open'); this.setAttribute('aria-expanded', this.parentElement.classList.contains('open'))">
83
+ <span><i class="fa-solid fa-envelope"></i> Request Demo</span>
84
+ <i class="fa-solid fa-chevron-down sidebar-collapse-icon"></i>
85
+ </button>
86
+ <div class="sidebar-collapse-body">
87
+ <div class="sidebar-collapse-content">
88
+ <a href="mailto:tracescene@gmail.com" class="sidebar-contact-link">
89
+ <i class="fa-solid fa-at"></i> tracescene@gmail.com
90
+ </a>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="status-panel">
96
+ <div class="status-item">
97
+ <span class="status-label">Model</span>
98
+ <span class="status-value" id="status-model">Loading...</span>
99
+ </div>
100
+ <div class="status-item">
101
+ <span class="status-label">Device</span>
102
+ <span class="status-value" id="status-device">β€”</span>
103
+ </div>
104
+ <div class="status-item">
105
+ <span class="status-label">Rules</span>
106
+ <span class="status-value" id="status-rules">β€”</span>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="disclaimer-badge">
111
+ <i class="fa-solid fa-triangle-exclamation"></i>
112
+ <span>AI Advisory Only</span>
113
+ </div>
114
+ </aside>
115
+
116
+ <!-- Main Content -->
117
+ <main class="main-content">
118
+
119
+ <!-- Landing Page View -->
120
+ <section id="view-landing" class="view active">
121
+ <div class="landing-hero">
122
+ <h1 class="landing-title">TraceScene</h1>
123
+ <h2 class="landing-subtitle">Edge AI for Accident Intelligence β€” Fully Offline. Fully Private.</h2>
124
+ <p class="landing-description">
125
+ Edge-based AI enables powerful reasoning, and real-time physical world analysis directly on edge
126
+ devices custom-trained in specialized physical conditions for accurate accident analysis and
127
+ Insurance Verificationβ€”all fully offline and privacy-preserving, with no data leaving the device
128
+ </p>
129
+ </div>
130
+
131
+ <div class="landing-use-cases">
132
+ <h3 class="use-cases-title">Cross-Vertical Use Cases: From Intelligence to Implementation</h3>
133
+
134
+ <div class="accordion-container">
135
+ <!-- Law Enforcement -->
136
+ <div class="accordion-item glass">
137
+ <button class="accordion-header" aria-expanded="false">
138
+ <span><i class="fa-solid fa-shield-halved"></i> Law Enforcement and Public Safety</span>
139
+ <i class="fa-solid fa-chevron-down accordion-icon"></i>
140
+ </button>
141
+ <div class="accordion-body">
142
+ <div class="accordion-content">
143
+ <p><strong>Automated Incident Reporting:</strong> Field officers capture a 30-second
144
+ scene walkthrough. The engine determines causality (e.g., "Vehicle A rear-ended
145
+ Vehicle B due to wet road conditions") and
146
+ auto-generates a structured First Information Report (FIR) instantly.</p>
147
+ <p><strong>Rule-Book Verification:</strong> Insights are matched against the UAE
148
+ Federal Traffic rulebook to identify violations like Article 18 tailgating or
149
+ signal jumping.</p>
150
+ <p><strong>Forensic Reconstructions:</strong> Generates "frozen-in-time" 3D digital
151
+ twins and evidence maps locally on mobile NPUs.</p>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Insurance -->
157
+ <div class="accordion-item glass">
158
+ <button class="accordion-header" aria-expanded="false">
159
+ <span><i class="fa-solid fa-file-invoice-dollar"></i> Insurance and Claims
160
+ Management</span>
161
+ <i class="fa-solid fa-chevron-down accordion-icon"></i>
162
+ </button>
163
+ <div class="accordion-body">
164
+ <div class="accordion-content">
165
+ <p><strong>Verified Local Law Mapping:</strong> Automatically cross-references
166
+ visual evidence
167
+ against the UAE Federal Traffic Law (e.g., Article 18 for tailgating or Article
168
+ 12 for accident assistance) to determine fault based on objective legal
169
+ standards rather than human estimation.</p>
170
+ <p><strong>Real-Time Reasoning vs. Manual Verification:</strong> Shifts the
171
+ adjuster's
172
+ workflow from "Collect-then-Verify" to an Analyze-while-Collecting model.
173
+ Real-time physical-AI analyzes impact trajectories and debris patterns to
174
+ justify or flag claims instantly.</p>
175
+ <p><strong>Auto-Filled Insurance Pointers:</strong> Uses document intelligence to
176
+ instantly extract data from Emirates IDs, Mulkiya cards, and police reports,
177
+ converting them into structured JSON to auto-populate mandatory claim fields.
178
+ </p>
179
+ <p><strong>Rapid Triage and settlement:</strong> Reduces the average UAE claims
180
+ lifecycle from 15 days to minutes by automating the intake and verification of
181
+ accident photos.</p>
182
+ <p><strong>Fraud Shielding and CBUAE 2026 Compliance:</strong> Identifies suspicious
183
+ patterns that contradict the reported scene context. Every reasoning deduction
184
+ is
185
+ linked to an auditable Evidence Map (linking frame coordinates to specific law
186
+ articles) as mandated by 2026 Central Bank guidelines on AI transparency.</p>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <!-- Real Estate & Legal Removed for Alternative UI -->
192
+ </div>
193
+ </div>
194
+ </section>
195
+
196
+ <!-- LE Dashboard View -->
197
+ <section id="view-le-dashboard" class="view hidden">
198
+ <div class="view-header">
199
+ <h2><i class="fa-solid fa-chart-line"></i> Active Cases</h2>
200
+ <button class="btn-secondary" id="btn-refresh-cases">
201
+ <i class="fa-solid fa-rotate-right"></i>
202
+ </button>
203
+ </div>
204
+
205
+ <div id="le-cases-grid" class="cases-grid">
206
+ <!-- Cases injected here -->
207
+ </div>
208
+
209
+ <div id="le-empty-state-dashboard" class="empty-state">
210
+ <i class="fa-solid fa-folder-open"></i>
211
+ <h3>No Cases Yet</h3>
212
+ <p>Create a new case to begin accident analysis.</p>
213
+ <button class="btn-primary" id="btn-le-empty-new-case">
214
+ <i class="fa-solid fa-plus"></i> Create First Case
215
+ </button>
216
+ </div>
217
+ </section>
218
+
219
+ <!-- LE New Case View -->
220
+ <section id="view-le-new-case" class="view hidden">
221
+ <div class="view-header">
222
+ <h2><i class="fa-solid fa-plus-circle"></i> New Accident Case</h2>
223
+ </div>
224
+
225
+ <div class="form-container glass">
226
+ <div class="form-grid">
227
+ <div class="form-group">
228
+ <label for="case-number">Case / Incident Number *</label>
229
+ <input type="text" id="case-number" placeholder="e.g., ACC-2026-001">
230
+ </div>
231
+ <div class="form-group">
232
+ <label for="officer-name">Officer Name</label>
233
+ <input type="text" id="officer-name" placeholder="e.g., Officer Smith">
234
+ </div>
235
+ <div class="form-group">
236
+ <label for="incident-location">Location</label>
237
+ <input type="text" id="incident-location" placeholder="e.g., Main St & 5th Ave">
238
+ </div>
239
+ <div class="form-group">
240
+ <label for="incident-date">Incident Date</label>
241
+ <input type="date" id="incident-date">
242
+ </div>
243
+ <div class="form-group full-width">
244
+ <label for="officer-notes">Officer Notes</label>
245
+ <textarea id="officer-notes" rows="3"
246
+ placeholder="Initial observations, weather conditions, etc."></textarea>
247
+ </div>
248
+ </div>
249
+
250
+ <div class="drop-zone" id="drop-zone">
251
+ <i class="fa-solid fa-camera"></i>
252
+ <p>Drag & drop accident scene photos here</p>
253
+ <span class="sub-text">or click to browse β€’ Max 20 photos β€’ JPG, PNG, WebP</span>
254
+ <input type="file" id="file-input" multiple accept="image/*" capture="environment" hidden>
255
+ </div>
256
+
257
+ <div id="photo-preview" class="photo-preview hidden">
258
+ <div id="photo-thumbnails" class="photo-thumbnails"></div>
259
+ <span id="photo-count" class="photo-count">0 photos selected</span>
260
+ </div>
261
+
262
+ <div class="form-actions">
263
+ <button class="btn-primary btn-large" id="btn-create-case" disabled>
264
+ <i class="fa-solid fa-shield-halved"></i> Create Case & Upload Photos
265
+ </button>
266
+ </div>
267
+
268
+ <div id="upload-progress" class="progress-section hidden">
269
+ <div class="progress-bar">
270
+ <div class="progress-fill" id="upload-progress-fill"></div>
271
+ </div>
272
+ <span id="upload-status-text">Uploading photos...</span>
273
+ </div>
274
+ </div>
275
+ </section>
276
+
277
+ <!-- INS Dashboard View -->
278
+ <section id="view-ins-dashboard" class="view hidden">
279
+ <div class="view-header">
280
+ <h2><i class="fa-solid fa-chart-line"></i> Active Cases</h2>
281
+ <button class="btn-secondary" id="btn-refresh-cases-ins">
282
+ <i class="fa-solid fa-rotate-right"></i>
283
+ </button>
284
+ </div>
285
+
286
+ <div id="ins-cases-grid" class="cases-grid">
287
+ <!-- Cases injected here -->
288
+ </div>
289
+
290
+ <div id="ins-empty-state-dashboard" class="empty-state">
291
+ <i class="fa-solid fa-folder-open"></i>
292
+ <h3>No Cases Yet</h3>
293
+ <p>Create a new case to begin accident analysis.</p>
294
+ <button class="btn-primary" id="btn-ins-empty-new-case">
295
+ <i class="fa-solid fa-plus"></i> Create First Case
296
+ </button>
297
+ </div>
298
+ </section>
299
+
300
+ <!-- INS New Case View -->
301
+ <section id="view-ins-new-case" class="view hidden">
302
+ <div class="view-header">
303
+ <h2><i class="fa-solid fa-plus-circle"></i> New Accident Case</h2>
304
+ </div>
305
+
306
+ <div class="form-container glass">
307
+ <div class="form-grid">
308
+ <div class="form-group">
309
+ <label for="ins-case-number">Case / Incident Number *</label>
310
+ <input type="text" id="ins-case-number" placeholder="e.g., ACC-2026-001">
311
+ </div>
312
+ <div class="form-group">
313
+ <label for="ins-officer-name">Adjuster Name</label>
314
+ <input type="text" id="ins-officer-name" placeholder="e.g., Adjuster Smith">
315
+ </div>
316
+ <div class="form-group">
317
+ <label for="ins-incident-location">Location</label>
318
+ <input type="text" id="ins-incident-location" placeholder="e.g., Main St & 5th Ave">
319
+ </div>
320
+ <div class="form-group">
321
+ <label for="ins-incident-date">Incident Date</label>
322
+ <input type="date" id="ins-incident-date">
323
+ </div>
324
+ <div class="form-group full-width">
325
+ <label for="ins-officer-notes">Adjuster Notes</label>
326
+ <textarea id="ins-officer-notes" rows="3"
327
+ placeholder="Initial observations, weather conditions, etc."></textarea>
328
+ </div>
329
+ </div>
330
+
331
+ <div class="drop-zone" id="ins-drop-zone">
332
+ <i class="fa-solid fa-camera"></i>
333
+ <p>Drag & drop accident scene photos here</p>
334
+ <span class="sub-text">or click to browse β€’ Max 20 photos β€’ JPG, PNG, WebP</span>
335
+ <input type="file" id="ins-file-input" multiple accept="image/*" capture="environment" hidden>
336
+ </div>
337
+
338
+ <div id="ins-photo-preview" class="photo-preview hidden">
339
+ <div id="ins-photo-thumbnails" class="photo-thumbnails"></div>
340
+ <span id="ins-photo-count" class="photo-count">0 photos selected</span>
341
+ </div>
342
+
343
+ <div class="form-actions">
344
+ <button class="btn-primary btn-large" id="btn-ins-create-case" disabled>
345
+ <i class="fa-solid fa-file-invoice-dollar"></i> Create Case & Upload Photos
346
+ </button>
347
+ </div>
348
+
349
+ <div id="ins-upload-progress" class="progress-section hidden">
350
+ <div class="progress-bar">
351
+ <div class="progress-fill" id="ins-upload-progress-fill"></div>
352
+ </div>
353
+ <span id="ins-upload-status-text">Uploading photos...</span>
354
+ </div>
355
+ </div>
356
+ </section>
357
+
358
+ <!-- Case Detail View -->
359
+ <section id="view-case-detail" class="view hidden">
360
+ <div class="view-header">
361
+ <button class="btn-back" id="btn-back-dashboard">
362
+ <i class="fa-solid fa-arrow-left"></i>
363
+ </button>
364
+ <h2 id="case-detail-title">Case Details</h2>
365
+ <div class="header-actions">
366
+ <button class="btn-primary" id="btn-run-analysis" disabled>
367
+ <i class="fa-solid fa-brain"></i> Run AI Analysis
368
+ </button>
369
+ <button class="btn-secondary" id="btn-edit-case">
370
+ <i class="fa-solid fa-pen-to-square"></i> Edit
371
+ </button>
372
+ <button class="btn-secondary" id="btn-view-report" disabled>
373
+ <i class="fa-solid fa-file-lines"></i> View Report
374
+ </button>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Case Info Bar -->
379
+ <div class="case-info-bar glass">
380
+ <div class="info-chip">
381
+ <i class="fa-solid fa-hashtag"></i>
382
+ <span id="detail-case-number">β€”</span>
383
+ </div>
384
+ <div class="info-chip">
385
+ <i class="fa-solid fa-user-shield"></i>
386
+ <span id="detail-officer">β€”</span>
387
+ </div>
388
+ <div class="info-chip">
389
+ <i class="fa-solid fa-location-dot"></i>
390
+ <span id="detail-location">β€”</span>
391
+ </div>
392
+ <div class="info-chip">
393
+ <i class="fa-solid fa-calendar"></i>
394
+ <span id="detail-date">β€”</span>
395
+ </div>
396
+ <div class="info-chip status-chip" id="detail-status-chip">
397
+ <i class="fa-solid fa-circle"></i>
398
+ <span id="detail-status">β€”</span>
399
+ </div>
400
+ </div>
401
+
402
+ <!-- Analysis Panels -->
403
+ <div class="analysis-grid">
404
+ <!-- Photo Gallery Panel -->
405
+ <div class="panel glass" id="panel-photos">
406
+ <div class="panel-header">
407
+ <h3><i class="fa-solid fa-images"></i> Scene Photos</h3>
408
+ <div class="panel-actions">
409
+ <span class="badge" id="photo-badge">0</span>
410
+ <button class="btn-icon-sm" id="btn-add-photos-inline" title="Add Photos">
411
+ <i class="fa-solid fa-plus"></i>
412
+ </button>
413
+ </div>
414
+ </div>
415
+ <div id="detail-photos-grid" class="detail-photos-grid">
416
+ <!-- Photo thumbnails injected here -->
417
+ </div>
418
+ </div>
419
+
420
+ <!-- Scene Analysis Panel -->
421
+ <div class="panel glass" id="panel-analysis">
422
+ <div class="panel-header">
423
+ <h3><i class="fa-solid fa-eye"></i> Scene Analysis</h3>
424
+ </div>
425
+ <div id="analysis-content" class="analysis-content">
426
+ <p class="placeholder-text">Run analysis to see AI observations.</p>
427
+ </div>
428
+ </div>
429
+
430
+ <!-- Violations Panel -->
431
+ <div class="panel glass" id="panel-violations">
432
+ <div class="panel-header">
433
+ <h3><i class="fa-solid fa-gavel"></i> Violations Detected</h3>
434
+ <span class="badge danger" id="violation-badge">0</span>
435
+ </div>
436
+ <div id="violations-list" class="violations-list">
437
+ <p class="placeholder-text">No violations detected yet.</p>
438
+ </div>
439
+ </div>
440
+
441
+ <!-- Fault Analysis Panel -->
442
+ <div class="panel glass" id="panel-fault">
443
+ <div class="panel-header">
444
+ <h3><i class="fa-solid fa-scale-balanced"></i> Fault Analysis</h3>
445
+ </div>
446
+ <div id="fault-content" class="fault-content">
447
+ <p class="placeholder-text">Run analysis to determine fault.</p>
448
+ </div>
449
+ </div>
450
+ </div>
451
+
452
+ <!-- Analysis Progress Overlay -->
453
+ <div id="analysis-overlay" class="analysis-overlay hidden">
454
+ <div class="overlay-content glass">
455
+ <div class="spinner-large"></div>
456
+ <h3 id="analysis-step">Analyzing scene photos...</h3>
457
+ <p id="analysis-detail">This may take a few minutes depending on the number of photos.</p>
458
+ </div>
459
+ </div>
460
+ </section>
461
+
462
+ <!-- Report View -->
463
+ <section id="view-report" class="view hidden">
464
+ <div class="view-header">
465
+ <button class="btn-back" id="btn-back-case">
466
+ <i class="fa-solid fa-arrow-left"></i>
467
+ </button>
468
+ <h2><i class="fa-solid fa-file-lines"></i> Incident Report</h2>
469
+ </div>
470
+ <div id="report-content" class="report-content glass">
471
+ <!-- Report injected here -->
472
+ </div>
473
+ </section>
474
+
475
+ <!-- Rules View -->
476
+ <section id="view-rules" class="view hidden">
477
+ <div class="view-header">
478
+ <h2><i class="fa-solid fa-book-open"></i> Traffic Rules Reference</h2>
479
+ </div>
480
+ <div id="rules-content" class="rules-content">
481
+ <!-- Rules injected here -->
482
+ </div>
483
+ </section>
484
+
485
+ </main>
486
+ </div>
487
+
488
+ <!-- Toast Container -->
489
+ <div id="toast-container"></div>
490
+
491
+ <!-- Edit Case Modal -->
492
+ <div id="modal-edit-case" class="modal hidden">
493
+ <div class="modal-content glass">
494
+ <div class="modal-header">
495
+ <h2><i class="fa-solid fa-pen-to-square"></i> Edit Case Details</h2>
496
+ <button class="btn-close" id="btn-close-edit-modal">&times;</button>
497
+ </div>
498
+ <div class="modal-body">
499
+ <div class="form-grid">
500
+ <div class="form-group">
501
+ <label for="edit-officer-name">Officer Name</label>
502
+ <input type="text" id="edit-officer-name" placeholder="e.g., Officer Smith">
503
+ </div>
504
+ <div class="form-group">
505
+ <label for="edit-incident-location">Location</label>
506
+ <input type="text" id="edit-incident-location" placeholder="e.g., Main St & 5th Ave">
507
+ </div>
508
+ <div class="form-group">
509
+ <label for="edit-incident-date">Incident Date</label>
510
+ <input type="date" id="edit-incident-date">
511
+ </div>
512
+ <div class="form-group full-width">
513
+ <label for="edit-officer-notes">Officer Notes</label>
514
+ <textarea id="edit-officer-notes" rows="4" placeholder="Update observations..."></textarea>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ <div class="modal-footer">
519
+ <button class="btn-secondary" id="btn-cancel-edit">Cancel</button>
520
+ <button class="btn-primary" id="btn-save-case">Save Changes</button>
521
+ </div>
522
+ </div>
523
+ </div>
524
+
525
+ <!-- Add Photos Modal/Overlay -->
526
+ <div id="modal-add-photos" class="modal hidden">
527
+ <div class="modal-content glass">
528
+ <div class="modal-header">
529
+ <h2><i class="fa-solid fa-images"></i> Add More Photos</h2>
530
+ <button class="btn-close" id="btn-close-photos-modal">&times;</button>
531
+ </div>
532
+ <div class="modal-body">
533
+ <div class="drop-zone" id="edit-drop-zone">
534
+ <i class="fa-solid fa-camera"></i>
535
+ <p>Drag & drop additional photos here</p>
536
+ <span class="sub-text">or click to browse</span>
537
+ <input type="file" id="edit-file-input" multiple accept="image/*" capture="environment" hidden>
538
+ </div>
539
+ <div id="edit-photo-preview" class="photo-preview hidden">
540
+ <div id="edit-photo-thumbnails" class="photo-thumbnails"></div>
541
+ <span id="edit-photo-count" class="photo-count">0 photos selected</span>
542
+ </div>
543
+ <div id="edit-upload-progress" class="progress-section hidden">
544
+ <div class="progress-bar">
545
+ <div class="progress-fill" id="edit-upload-progress-fill"></div>
546
+ </div>
547
+ <span id="edit-upload-status-text">Uploading photos...</span>
548
+ </div>
549
+ </div>
550
+ <div class="modal-footer">
551
+ <button class="btn-secondary" id="btn-cancel-photos">Cancel</button>
552
+ <button class="btn-primary" id="btn-upload-more" disabled>Upload Photos</button>
553
+ </div>
554
+ </div>
555
+ </div>
556
+
557
+ <script src="js/alt_app.js"></script>
558
+ </body>
559
+
560
+ </html>
js/alt_app.js ADDED
@@ -0,0 +1,1010 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TraceScene β€” Frontend Application
3
+ *
4
+ * Single-page application for accident case management,
5
+ * photo upload, AI analysis, and report viewing.
6
+ */
7
+
8
+ const API_BASE = '/api';
9
+
10
+ // ── Mock Backend Interceptor ───────────────────────────────────────────
11
+ // This simulates the AI backend purely in the browser for the public UI demo
12
+ const originalFetch = window.fetch;
13
+ let mockNextId = 1;
14
+ let mockCases = {};
15
+
16
+ window.fetch = async (url, options = {}) => {
17
+ // Only intercept our API calls
18
+ if (typeof url === 'string' && url.startsWith('/api')) {
19
+ console.log(`[Mock Demo API] ${options.method || 'GET'} ${url}`);
20
+
21
+ // Helper to delay response
22
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
23
+
24
+ // 1. Health check
25
+ if (url === '/api/health') {
26
+ return new Response(JSON.stringify({ model_loaded: true, device: "Browser Demo", rules_loaded: 43 }), { status: 200 });
27
+ }
28
+
29
+ // 2. Get cases
30
+ if (url === '/api/cases' && (options.method === 'GET' || !options.method)) {
31
+ const casesList = Object.values(mockCases);
32
+ return new Response(JSON.stringify({ cases: casesList }), { status: 200 });
33
+ }
34
+
35
+ // 3. Create case
36
+ if (url === '/api/cases' && options.method === 'POST') {
37
+ const formData = options.body;
38
+ const newCase = {
39
+ id: mockNextId++,
40
+ case_number: formData.get('case_number'),
41
+ officer_name: formData.get('officer_name'),
42
+ location: formData.get('location'),
43
+ incident_date: formData.get('incident_date'),
44
+ notes: formData.get('notes'),
45
+ status: 'new',
46
+ created_at: new Date().toISOString()
47
+ };
48
+ mockCases[newCase.id] = { case: newCase, photos: [], violations: [], fault_analysis: null, analyses: [] };
49
+ return new Response(JSON.stringify(newCase), { status: 200 });
50
+ }
51
+
52
+ // 3.5 Delete case
53
+ if (url.match(/^\/api\/cases\/\d+$/) && options.method === 'DELETE') {
54
+ const id = parseInt(url.split('/').pop());
55
+ delete mockCases[id];
56
+ return new Response(JSON.stringify({ detail: "Deleted" }), { status: 200 });
57
+ }
58
+
59
+ // 4. Upload photos
60
+ if (url.match(/^\/api\/cases\/\d+\/photos$/) && options.method === 'POST') {
61
+ const id = parseInt(url.split('/')[3]);
62
+ const formData = options.body;
63
+ const files = formData.getAll('files');
64
+ await delay(800); // Simulate upload
65
+
66
+ files.forEach(f => {
67
+ mockCases[id].photos.push({
68
+ id: Math.random().toString(36).substr(2, 9),
69
+ filename: f.name,
70
+ blob_url: URL.createObjectURL(f)
71
+ });
72
+ });
73
+ mockCases[id].case.photo_count = mockCases[id].photos.length;
74
+ mockCases[id].case.status = 'ready';
75
+
76
+ return new Response(JSON.stringify({ count: files.length }), { status: 200 });
77
+ }
78
+
79
+ // 5. Get case details
80
+ if (url.match(/^\/api\/cases\/\d+$/) && (options.method === 'GET' || !options.method)) {
81
+ const id = parseInt(url.split('/').pop());
82
+ if (!mockCases[id]) return new Response(JSON.stringify({ detail: "Not found" }), { status: 404 });
83
+ return new Response(JSON.stringify(mockCases[id]), { status: 200 });
84
+ }
85
+
86
+ // 6. Run AI Analysis
87
+ if (url.match(/^\/api\/cases\/\d+\/analyze$/) && options.method === 'POST') {
88
+ const id = parseInt(url.split('/')[3]);
89
+ await delay(3000); // Simulate heavy AI reasoning
90
+
91
+ mockCases[id].case.status = 'complete';
92
+
93
+ // Generate mock analysis data based on the uploaded photos
94
+ mockCases[id].analyses = mockCases[id].photos.map(p => ({
95
+ filename: p.filename,
96
+ raw_analysis: `[AI Observation]\nSubject: Vehicle A and Vehicle B.\nAction: Vehicle A exhibits front-end damage consistent with forward impact. Vehicle B exhibits rear damage. Road conditions appear clear.`
97
+ }));
98
+
99
+ mockCases[id].violations = [
100
+ {
101
+ rule_id: "Article 18",
102
+ rule_title: "Failure to Leave Safe Distance",
103
+ severity: "HIGH",
104
+ confidence: 0.94,
105
+ party_label: "Vehicle A",
106
+ evidence_summary: "Visual estimation of skid marks and impact deformation indicates Vehicle A failed to maintain adequate stopping distance."
107
+ }
108
+ ];
109
+
110
+ mockCases[id].fault_analysis = {
111
+ determined: true,
112
+ primary_fault_party: "Vehicle A",
113
+ probable_cause: "Rear-end collision due to insufficient following distance.",
114
+ fault_distribution_json: { "Vehicle A": 100, "Vehicle B": 0 },
115
+ overall_confidence: 0.92,
116
+ analysis_summary: "Based on the visual evidence, Vehicle A is fully at fault for failing to stop in time."
117
+ };
118
+
119
+ return new Response(JSON.stringify({ violations_found: 1 }), { status: 200 });
120
+ }
121
+ }
122
+
123
+ // Intercept photo image requests to return the stored blob URL instead
124
+ if (typeof url === 'string' && url.match(/^\/api\/photos\/.+\/image$/)) {
125
+ const photoId = url.split('/')[3];
126
+ let foundPhotoURL = null;
127
+ Object.values(mockCases).forEach(c => {
128
+ const p = c.photos.find(p => p.id === photoId);
129
+ if (p) foundPhotoURL = p.blob_url;
130
+ });
131
+ if (foundPhotoURL) {
132
+ // Fetch the blob URL to return a proper image response
133
+ return originalFetch(foundPhotoURL);
134
+ }
135
+ }
136
+
137
+ // Fallback for non-API requests
138
+ return originalFetch(url, options);
139
+ };
140
+
141
+ // ── State ─────────────────────────────────────────────────────────────
142
+
143
+ let currentView = 'landing';
144
+ let currentCaseId = null;
145
+ let currentCaseData = null;
146
+ let currentVertical = 'le'; // Store full case data for editing
147
+ let selectedFiles = [];
148
+ let additionalFiles = []; // For add photos modal
149
+
150
+ // ── Init ──────────────────────────────────────────────────────────────
151
+
152
+ document.addEventListener('DOMContentLoaded', () => {
153
+ initNavigation();
154
+ initDropZone();
155
+ initButtons();
156
+ initAccordions();
157
+ loadHealth();
158
+ loadCases();
159
+ });
160
+
161
+ // ── Accordions ────────────────────────────────────────────────────────
162
+
163
+ function initAccordions() {
164
+ document.querySelectorAll('.accordion-header').forEach(btn => {
165
+ btn.addEventListener('click', () => {
166
+ const item = btn.closest('.accordion-item');
167
+ const isActive = item.classList.contains('active');
168
+
169
+ // Close all other accordions
170
+ document.querySelectorAll('.accordion-item').forEach(i => {
171
+ i.classList.remove('active');
172
+ i.querySelector('.accordion-header').setAttribute('aria-expanded', 'false');
173
+ });
174
+
175
+ // Toggle current
176
+ if (!isActive) {
177
+ item.classList.add('active');
178
+ btn.setAttribute('aria-expanded', 'true');
179
+ }
180
+ });
181
+ });
182
+ }
183
+
184
+ // ── Navigation ────────────────────────────────────────────────────────
185
+
186
+ function initNavigation() {
187
+ document.querySelectorAll('.nav-item[data-view]').forEach(btn => {
188
+ btn.addEventListener('click', () => {
189
+ const view = btn.dataset.view;
190
+ switchView(view);
191
+ document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
192
+ btn.classList.add('active');
193
+ });
194
+ });
195
+ }
196
+
197
+ function switchView(viewName) {
198
+ document.querySelectorAll('.view').forEach(v => {
199
+ v.classList.remove('active');
200
+ v.classList.add('hidden');
201
+ });
202
+ const target = document.getElementById(`view-${viewName}`);
203
+ if (target) {
204
+ target.classList.remove('hidden');
205
+ target.classList.add('active');
206
+ }
207
+ currentView = viewName;
208
+
209
+ if (viewName === 'le-dashboard') loadCases('le');
210
+ if (viewName === 'ins-dashboard') loadCases('ins');
211
+ if (viewName === 'rules') loadRules();
212
+ }
213
+
214
+ // ── Buttons ───────────────────────────────────────────────────────────
215
+
216
+ function initButtons() {
217
+ // LE Dashboard
218
+ document.getElementById('btn-refresh-cases')?.addEventListener('click', () => loadCases('le'));
219
+ document.getElementById('btn-le-empty-new-case')?.addEventListener('click', () => {
220
+ switchView('le-new-case');
221
+ document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
222
+ document.querySelector('[data-view="le-new-case"]')?.classList.add('active');
223
+ });
224
+
225
+ // INS Dashboard
226
+ document.getElementById('btn-refresh-cases-ins')?.addEventListener('click', () => loadCases('ins'));
227
+ document.getElementById('btn-ins-empty-new-case')?.addEventListener('click', () => {
228
+ switchView('ins-new-case');
229
+ document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
230
+ document.querySelector('[data-view="ins-new-case"]')?.classList.add('active');
231
+ });
232
+
233
+ // New Case Forms
234
+ document.getElementById('btn-create-case')?.addEventListener('click', () => createCase('le'));
235
+ document.getElementById('btn-ins-create-case')?.addEventListener('click', () => createCase('ins'));
236
+
237
+ // Case Detail
238
+ document.getElementById('btn-back-dashboard')?.addEventListener('click', () => switchView(currentVertical === 'ins' ? 'ins-dashboard' : 'le-dashboard'));
239
+ document.getElementById('btn-run-analysis')?.addEventListener('click', runAnalysis);
240
+ document.getElementById('btn-view-report')?.addEventListener('click', viewReport);
241
+ document.getElementById('btn-edit-case')?.addEventListener('click', openEditModal);
242
+ document.getElementById('btn-add-photos-inline')?.addEventListener('click', openAddPhotosModal);
243
+
244
+ // Edit Modal
245
+ document.getElementById('btn-close-edit-modal')?.addEventListener('click', closeEditModal);
246
+ document.getElementById('btn-cancel-edit')?.addEventListener('click', closeEditModal);
247
+ document.getElementById('btn-save-case')?.addEventListener('click', saveCaseChanges);
248
+
249
+ // Photos Modal
250
+ document.getElementById('btn-close-photos-modal')?.addEventListener('click', closePhotosModal);
251
+ document.getElementById('btn-cancel-photos')?.addEventListener('click', closePhotosModal);
252
+ document.getElementById('btn-upload-more')?.addEventListener('click', uploadMorePhotos);
253
+
254
+ // Form logic
255
+ document.getElementById('case-number')?.addEventListener('input', () => validateForm('le'));
256
+ document.getElementById('ins-case-number')?.addEventListener('input', () => validateForm('ins'));
257
+ initEditDropZone();
258
+ }
259
+
260
+ function initEditDropZone() {
261
+ const dropZone = document.getElementById('edit-drop-zone');
262
+ const fileInput = document.getElementById('edit-file-input');
263
+ if (!dropZone || !fileInput) return;
264
+
265
+ dropZone.addEventListener('click', () => fileInput.click());
266
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
267
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
268
+ dropZone.addEventListener('drop', e => {
269
+ e.preventDefault();
270
+ dropZone.classList.remove('drag-over');
271
+ handleAdditionalFiles(Array.from(e.dataTransfer.files));
272
+ });
273
+ fileInput.addEventListener('change', () => {
274
+ handleAdditionalFiles(Array.from(fileInput.files));
275
+ });
276
+ }
277
+
278
+ function validateForm(vertical = 'le') {
279
+ const cid = vertical === 'le' ? 'case-number' : 'ins-case-number';
280
+ const bid = vertical === 'le' ? 'btn-create-case' : 'btn-ins-create-case';
281
+ const caseNum = document.getElementById(cid)?.value.trim();
282
+ const btn = document.getElementById(bid);
283
+ if (btn) btn.disabled = !caseNum;
284
+ }
285
+
286
+ // ── Health ─────────────────────────────────────────────────────────────
287
+
288
+ async function loadHealth() {
289
+ try {
290
+ const resp = await fetch(`${API_BASE}/health`);
291
+ const data = await resp.json();
292
+ document.getElementById('status-model').textContent = data.model_loaded ? 'βœ“ Ready' : 'βœ— Not loaded';
293
+ document.getElementById('status-model').style.color = data.model_loaded ? '#22c55e' : '#ef4444';
294
+ document.getElementById('status-device').textContent = data.device || 'β€”';
295
+ document.getElementById('status-rules').textContent = `${data.rules_loaded} rules`;
296
+ } catch {
297
+ document.getElementById('status-model').textContent = 'βœ— Offline';
298
+ document.getElementById('status-model').style.color = '#ef4444';
299
+ }
300
+ }
301
+
302
+ // ── Cases ─────────────────────────────────────────────────────────────
303
+
304
+ async function loadCases(vertical = 'le') {
305
+ currentVertical = vertical;
306
+ try {
307
+ const resp = await fetch(`${API_BASE}/cases`);
308
+ const data = await resp.json();
309
+ renderCases(data.cases || [], vertical);
310
+ } catch (e) {
311
+ showToast('Failed to load cases', 'error');
312
+ }
313
+ }
314
+
315
+ function renderCases(cases, vertical) {
316
+ const grid = document.getElementById(vertical === 'le' ? 'le-cases-grid' : 'ins-cases-grid');
317
+ const empty = document.getElementById(vertical === 'le' ? 'le-empty-state-dashboard' : 'ins-empty-state-dashboard');
318
+
319
+ if (!cases.length) {
320
+ grid.innerHTML = '';
321
+ grid.style.display = 'none';
322
+ empty.style.display = 'flex';
323
+ return;
324
+ }
325
+
326
+ grid.style.display = 'grid';
327
+ empty.style.display = 'none';
328
+
329
+ grid.innerHTML = cases.map(c => `
330
+ <div class="case-card" onclick="openCase(${c.id})">
331
+ <button class="btn-delete-card" onclick="event.stopPropagation(); deleteCase(${c.id}, '${escHtml(c.case_number)}', currentVertical)" title="Delete Case">
332
+ <i class="fa-solid fa-trash-can"></i>
333
+ </button>
334
+ <div class="card-header">
335
+ <span class="case-number">${escHtml(c.case_number)}</span>
336
+ <span class="status-badge ${c.status}">
337
+ <i class="fa-solid fa-circle"></i> ${c.status}
338
+ </span>
339
+ </div>
340
+ <div class="case-meta">
341
+ ${c.officer_name ? `<span><i class="fa-solid fa-user-shield"></i> ${escHtml(c.officer_name)}</span>` : ''}
342
+ ${c.location ? `<span><i class="fa-solid fa-location-dot"></i> ${escHtml(c.location)}</span>` : ''}
343
+ ${c.incident_date ? `<span><i class="fa-solid fa-calendar"></i> ${c.incident_date}</span>` : ''}
344
+ </div>
345
+ <div class="card-footer">
346
+ <span class="photo-count"><i class="fa-solid fa-camera"></i> ${c.photo_count || 0} photos</span>
347
+ <span style="font-size:0.72rem;color:var(--text-muted)">${formatDate(c.created_at)}</span>
348
+ </div>
349
+ </div>
350
+ `).join('');
351
+ }
352
+
353
+ async function deleteCase(id, caseNum, vertical = 'le') {
354
+ if (!confirm(`Are you sure you want to delete Case ${caseNum}? This will permanently remove all photos and analysis data.`)) {
355
+ return;
356
+ }
357
+
358
+ try {
359
+ const resp = await fetch(`${API_BASE}/cases/${id}`, { method: 'DELETE' });
360
+ if (!resp.ok) throw new Error('Delete failed');
361
+ showToast(`Case ${caseNum} deleted`, 'success');
362
+ loadCases();
363
+ } catch (e) {
364
+ showToast(e.message, 'error');
365
+ }
366
+ }
367
+
368
+ // ── Create Case ───────────────────────────────────────────────────────
369
+
370
+ async function createCase(vertical = 'le') {
371
+ const cid = vertical === 'le' ? 'case-number' : 'ins-case-number';
372
+ const oid = vertical === 'le' ? 'officer-name' : 'ins-officer-name';
373
+ const lid = vertical === 'le' ? 'incident-location' : 'ins-incident-location';
374
+ const did = vertical === 'le' ? 'incident-date' : 'ins-incident-date';
375
+ const nid = vertical === 'le' ? 'officer-notes' : 'ins-officer-notes';
376
+
377
+ const caseNumber = document.getElementById(cid).value.trim();
378
+ if (!caseNumber) return showToast('Case number is required', 'error');
379
+
380
+ const formData = new FormData();
381
+ formData.append('case_number', caseNumber);
382
+ formData.append('officer_name', document.getElementById(oid).value.trim());
383
+ formData.append('location', document.getElementById(lid).value.trim());
384
+ formData.append('incident_date', document.getElementById(did).value);
385
+ formData.append('notes', document.getElementById(nid).value.trim());
386
+
387
+ try {
388
+ const resp = await fetch(`${API_BASE}/cases`, { method: 'POST', body: formData });
389
+ if (!resp.ok) {
390
+ const err = await resp.json();
391
+ throw new Error(err.detail || 'Failed to create case');
392
+ }
393
+ const data = await resp.json();
394
+ const caseId = data.id;
395
+
396
+ showToast('Case created', 'success');
397
+
398
+ // Upload photos if selected
399
+ if (selectedFiles.length > 0) {
400
+ await uploadPhotos(caseId, vertical);
401
+ }
402
+
403
+ // Clear form
404
+ document.getElementById(cid).value = '';
405
+ document.getElementById(oid).value = '';
406
+ document.getElementById(lid).value = '';
407
+ document.getElementById(did).value = '';
408
+ document.getElementById(nid).value = '';
409
+ selectedFiles = [];
410
+ document.getElementById(vertical === 'le' ? 'photo-preview' : 'ins-photo-preview').classList.add('hidden');
411
+
412
+ // Open the case detail
413
+ openCase(caseId);
414
+
415
+ } catch (e) {
416
+ showToast(e.message, 'error');
417
+ }
418
+ }
419
+
420
+ async function uploadPhotos(caseId, vertical = 'le') {
421
+ const pfx = vertical === 'le' ? '' : 'ins-';
422
+ const progressSection = document.getElementById(`${pfx}upload-progress`);
423
+ const progressFill = document.getElementById(`${pfx}upload-progress-fill`);
424
+ const statusText = document.getElementById(`${pfx}upload-status-text`);
425
+
426
+ progressSection.classList.remove('hidden');
427
+ statusText.textContent = `Uploading ${selectedFiles.length} photos...`;
428
+
429
+ const formData = new FormData();
430
+ selectedFiles.forEach(f => formData.append('files', f));
431
+
432
+ try {
433
+ progressFill.style.width = '50%';
434
+ const resp = await fetch(`${API_BASE}/cases/${caseId}/photos`, {
435
+ method: 'POST',
436
+ body: formData,
437
+ });
438
+
439
+ if (!resp.ok) throw new Error('Upload failed');
440
+
441
+ const data = await resp.json();
442
+ progressFill.style.width = '100%';
443
+ statusText.textContent = `${data.count} photos uploaded βœ“`;
444
+ showToast(`${data.count} photos uploaded`, 'success');
445
+ return data;
446
+ } catch (e) {
447
+ statusText.textContent = 'Upload failed';
448
+ showToast('Photo upload failed', 'error');
449
+ throw e;
450
+ }
451
+ }
452
+
453
+ // ── Drop Zone ─────────────────────────────────────────────────────────
454
+
455
+ function initDropZone() {
456
+ setupZone('drop-zone', 'file-input', 'photo-preview', 'photo-thumbnails', 'photo-count');
457
+ setupZone('ins-drop-zone', 'ins-file-input', 'ins-photo-preview', 'ins-photo-thumbnails', 'ins-photo-count');
458
+ }
459
+
460
+ function setupZone(dzId, inId, prvId, tnId, ctId) {
461
+ const dropZone = document.getElementById(dzId);
462
+ const fileInput = document.getElementById(inId);
463
+ if (!dropZone || !fileInput) return;
464
+
465
+ dropZone.addEventListener('click', () => fileInput.click());
466
+
467
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
468
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
469
+ dropZone.addEventListener('drop', e => {
470
+ e.preventDefault();
471
+ dropZone.classList.remove('drag-over');
472
+ handleFiles(Array.from(e.dataTransfer.files), prvId, tnId, ctId);
473
+ });
474
+
475
+ fileInput.addEventListener('change', () => {
476
+ handleFiles(Array.from(fileInput.files), prvId, tnId, ctId, prefix);
477
+ });
478
+ }
479
+
480
+ function handleFiles(files, prvId = 'photo-preview', tnId = 'photo-thumbnails', ctId = 'photo-count', prefix = 'le') {
481
+ const imageFiles = files.filter(f => f.type.startsWith('image/'));
482
+ if (!imageFiles.length) return showToast('No image files found', 'error');
483
+
484
+ selectedFiles = imageFiles.slice(0, 20); // Max 20
485
+
486
+ // Show previews
487
+ const preview = document.getElementById(prvId);
488
+ const thumbs = document.getElementById(tnId);
489
+ const count = document.getElementById(ctId);
490
+
491
+ preview.classList.remove('hidden');
492
+ count.textContent = `${selectedFiles.length} photo${selectedFiles.length > 1 ? 's' : ''} selected`;
493
+
494
+ thumbs.innerHTML = '';
495
+ selectedFiles.forEach(file => {
496
+ const div = document.createElement('div');
497
+ div.className = 'photo-thumbnail';
498
+ const img = document.createElement('img');
499
+ img.src = URL.createObjectURL(file);
500
+ div.appendChild(img);
501
+ thumbs.appendChild(div);
502
+ });
503
+
504
+ validateForm(prefix);
505
+ }
506
+
507
+ // ── Open Case Detail ──────────────────────────────────────────────────
508
+
509
+ async function openCase(caseId) {
510
+ currentCaseId = caseId;
511
+ switchView('case-detail');
512
+
513
+ try {
514
+ const resp = await fetch(`${API_BASE}/cases/${caseId}`);
515
+ const data = await resp.json();
516
+ currentCaseData = data.case;
517
+ renderCaseDetail(data);
518
+ } catch (e) {
519
+ showToast('Failed to load case', 'error');
520
+ }
521
+ }
522
+
523
+ // ── Edit Case ─────────────────────────────────────────────────────────
524
+
525
+ function openEditModal() {
526
+ if (!currentCaseData) return;
527
+ document.getElementById('edit-officer-name').value = currentCaseData.officer_name || '';
528
+ document.getElementById('edit-incident-location').value = currentCaseData.location || '';
529
+ document.getElementById('edit-incident-date').value = currentCaseData.incident_date || '';
530
+ document.getElementById('edit-officer-notes').value = currentCaseData.notes || '';
531
+ document.getElementById('modal-edit-case').classList.remove('hidden');
532
+ }
533
+
534
+ function closeEditModal() {
535
+ document.getElementById('modal-edit-case').classList.add('hidden');
536
+ }
537
+
538
+ async function saveCaseChanges() {
539
+ if (!currentCaseId) return;
540
+
541
+ const formData = new FormData();
542
+ formData.append('officer_name', document.getElementById('edit-officer-name').value.trim());
543
+ formData.append('location', document.getElementById('edit-incident-location').value.trim());
544
+ formData.append('incident_date', document.getElementById('edit-incident-date').value);
545
+ formData.append('notes', document.getElementById('edit-officer-notes').value.trim());
546
+
547
+ try {
548
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}`, {
549
+ method: 'PUT',
550
+ body: formData
551
+ });
552
+
553
+ if (!resp.ok) throw new Error('Failed to update case');
554
+
555
+ showToast('Case updated', 'success');
556
+ closeEditModal();
557
+ openCase(currentCaseId); // Refresh
558
+ } catch (e) {
559
+ showToast(e.message, 'error');
560
+ }
561
+ }
562
+
563
+ // ── Add Photos ────────────────────────────────────────────────────────
564
+
565
+ function openAddPhotosModal() {
566
+ additionalFiles = [];
567
+ document.getElementById('edit-photo-preview').classList.add('hidden');
568
+ document.getElementById('edit-upload-progress').classList.add('hidden');
569
+ document.getElementById('btn-upload-more').disabled = true;
570
+ document.getElementById('modal-add-photos').classList.remove('hidden');
571
+ }
572
+
573
+ function closePhotosModal() {
574
+ document.getElementById('modal-add-photos').classList.add('hidden');
575
+ }
576
+
577
+ function handleAdditionalFiles(files) {
578
+ const imageFiles = files.filter(f => f.type.startsWith('image/'));
579
+ if (!imageFiles.length) return showToast('No image files found', 'error');
580
+
581
+ additionalFiles = imageFiles.slice(0, 20);
582
+
583
+ const preview = document.getElementById('edit-photo-preview');
584
+ const thumbs = document.getElementById('edit-photo-thumbnails');
585
+ const count = document.getElementById('edit-photo-count');
586
+
587
+ preview.classList.remove('hidden');
588
+ count.textContent = `${additionalFiles.length} photo${additionalFiles.length > 1 ? 's' : ''} selected`;
589
+
590
+ thumbs.innerHTML = '';
591
+ additionalFiles.forEach(file => {
592
+ const div = document.createElement('div');
593
+ div.className = 'photo-thumbnail';
594
+ const img = document.createElement('img');
595
+ img.src = URL.createObjectURL(file);
596
+ div.appendChild(img);
597
+ thumbs.appendChild(div);
598
+ });
599
+
600
+ document.getElementById('btn-upload-more').disabled = additionalFiles.length === 0;
601
+ }
602
+
603
+ async function uploadMorePhotos() {
604
+ if (!currentCaseId || !additionalFiles.length) return;
605
+
606
+ const progressSection = document.getElementById('edit-upload-progress');
607
+ const progressFill = document.getElementById('edit-upload-progress-fill');
608
+ const statusText = document.getElementById('edit-upload-status-text');
609
+
610
+ progressSection.classList.remove('hidden');
611
+ document.getElementById('btn-upload-more').disabled = true;
612
+
613
+ const formData = new FormData();
614
+ additionalFiles.forEach(f => formData.append('files', f));
615
+
616
+ try {
617
+ progressFill.style.width = '50%';
618
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/photos`, {
619
+ method: 'POST',
620
+ body: formData,
621
+ });
622
+
623
+ if (!resp.ok) throw new Error('Upload failed');
624
+
625
+ progressFill.style.width = '100%';
626
+ statusText.textContent = `Upload complete βœ“`;
627
+ showToast('Photos added successfully', 'success');
628
+
629
+ setTimeout(() => {
630
+ closePhotosModal();
631
+ openCase(currentCaseId); // Refresh
632
+ }, 1000);
633
+ } catch (e) {
634
+ statusText.textContent = 'Upload failed';
635
+ showToast('Photo upload failed', 'error');
636
+ document.getElementById('btn-upload-more').disabled = false;
637
+ }
638
+ }
639
+
640
+ function renderCaseDetail(data) {
641
+ const c = data.case;
642
+
643
+ // Header
644
+ document.getElementById('case-detail-title').textContent = `Case: ${c.case_number}`;
645
+
646
+ // Info bar
647
+ document.getElementById('detail-case-number').textContent = c.case_number;
648
+ document.getElementById('detail-officer').textContent = c.officer_name || 'N/A';
649
+ document.getElementById('detail-location').textContent = c.location || 'N/A';
650
+ document.getElementById('detail-date').textContent = c.incident_date || 'N/A';
651
+ document.getElementById('detail-status').textContent = c.status;
652
+
653
+ const statusChip = document.getElementById('detail-status-chip');
654
+ statusChip.className = `info-chip status-chip ${c.status}`;
655
+
656
+ // Enable/disable buttons
657
+ const btnAnalysis = document.getElementById('btn-run-analysis');
658
+ const btnReport = document.getElementById('btn-view-report');
659
+ btnAnalysis.disabled = !data.photos?.length;
660
+ btnReport.disabled = c.status !== 'complete';
661
+
662
+ // Photos
663
+ const photosGrid = document.getElementById('detail-photos-grid');
664
+ document.getElementById('photo-badge').textContent = data.photos?.length || 0;
665
+
666
+ if (data.photos?.length) {
667
+ photosGrid.innerHTML = data.photos.map(p => `
668
+ <div class="detail-photo">
669
+ <img src="${API_BASE.replace('/api', '')}/api/photos/${p.id}/image" alt="${escHtml(p.filename)}" loading="lazy">
670
+ </div>
671
+ `).join('');
672
+ } else {
673
+ photosGrid.innerHTML = '<p class="placeholder-text">No photos uploaded yet.</p>';
674
+ }
675
+
676
+ // Analyses
677
+ const analysisContent = document.getElementById('analysis-content');
678
+ if (data.analyses?.length) {
679
+ analysisContent.innerHTML = data.analyses.map(a => `
680
+ <div class="analysis-photo-section">
681
+ <div class="analysis-photo-label">πŸ“· ${escHtml(a.filename || 'Photo')}</div>
682
+ <pre>${escHtml(a.raw_analysis || '')}</pre>
683
+ </div>
684
+ `).join('');
685
+ } else {
686
+ analysisContent.innerHTML = '<p class="placeholder-text">Run analysis to see AI observations.</p>';
687
+ }
688
+
689
+ // Violations
690
+ const violationsList = document.getElementById('violations-list');
691
+ const violationBadge = document.getElementById('violation-badge');
692
+ violationBadge.textContent = data.violations?.length || 0;
693
+
694
+ if (data.violations?.length) {
695
+ violationsList.innerHTML = data.violations.map(v => `
696
+ <div class="violation-card ${v.severity || 'MEDIUM'}">
697
+ <div class="violation-header">
698
+ <span class="violation-title">${escHtml(v.rule_title)}</span>
699
+ <span class="severity-tag ${v.severity || 'MEDIUM'}">${v.severity || 'MEDIUM'}</span>
700
+ </div>
701
+ <div class="violation-meta">
702
+ <span><i class="fa-solid fa-hashtag"></i> ${v.rule_id}</span>
703
+ <span><i class="fa-solid fa-percent"></i> ${Math.round(v.confidence * 100)}% confidence</span>
704
+ ${v.party_label ? `<span><i class="fa-solid fa-car"></i> ${escHtml(v.party_label)}</span>` : ''}
705
+ </div>
706
+ ${v.evidence_summary ? `<div class="violation-evidence">${escHtml(v.evidence_summary)}</div>` : ''}
707
+ </div>
708
+ `).join('');
709
+ } else {
710
+ violationsList.innerHTML = '<p class="placeholder-text">No violations detected yet.</p>';
711
+ }
712
+
713
+ // Fault Analysis
714
+ renderFaultAnalysis(data.fault_analysis, data.parties);
715
+ }
716
+
717
+ function renderFaultAnalysis(fault, parties) {
718
+ const content = document.getElementById('fault-content');
719
+
720
+ if (!fault) {
721
+ content.innerHTML = '<p class="placeholder-text">Run analysis to determine fault.</p>';
722
+ return;
723
+ }
724
+
725
+ let html = '';
726
+
727
+ // Fault distribution bars
728
+ if (fault.fault_distribution_json) {
729
+ let dist = {};
730
+ try {
731
+ dist = typeof fault.fault_distribution_json === 'string'
732
+ ? JSON.parse(fault.fault_distribution_json)
733
+ : fault.fault_distribution_json;
734
+ } catch { dist = {}; }
735
+
736
+ const entries = Object.entries(dist);
737
+ if (entries.length) {
738
+ html += '<div class="fault-party-bars">';
739
+ entries.forEach(([label, pct], i) => {
740
+ html += `
741
+ <div class="party-bar">
742
+ <div class="party-bar-label">
743
+ <span class="party-name">${escHtml(label)}</span>
744
+ <span class="party-pct">${pct}%</span>
745
+ </div>
746
+ <div class="party-bar-track">
747
+ <div class="party-bar-fill ${i === 0 ? 'primary' : 'secondary'}"
748
+ style="width: ${pct}%"></div>
749
+ </div>
750
+ </div>
751
+ `;
752
+ });
753
+ html += '</div>';
754
+ }
755
+ }
756
+
757
+ // Probable cause
758
+ if (fault.probable_cause) {
759
+ html += `
760
+ <div class="fault-cause">
761
+ <h4><i class="fa-solid fa-magnifying-glass-chart"></i> Probable Cause</h4>
762
+ <p>${escHtml(fault.probable_cause)}</p>
763
+ </div>
764
+ `;
765
+ }
766
+
767
+ // Confidence
768
+ if (fault.overall_confidence != null) {
769
+ const pct = Math.round(fault.overall_confidence * 100);
770
+ html += `
771
+ <div class="confidence-meter">
772
+ <span>Confidence:</span>
773
+ <div class="meter-bar">
774
+ <div class="meter-fill" style="width: ${pct}%"></div>
775
+ </div>
776
+ <span>${pct}%</span>
777
+ </div>
778
+ `;
779
+ }
780
+
781
+ // Summary
782
+ if (fault.analysis_summary) {
783
+ html += `<p style="margin-top:0.8rem;font-size:0.82rem;color:var(--text-secondary)">
784
+ ${escHtml(fault.analysis_summary)}</p>`;
785
+ }
786
+
787
+ content.innerHTML = html || '<p class="placeholder-text">No fault analysis available.</p>';
788
+ }
789
+
790
+ // ── Run Analysis ──────────────────────────────────────────────────────
791
+
792
+ async function runAnalysis() {
793
+ if (!currentCaseId) return;
794
+
795
+ const overlay = document.getElementById('analysis-overlay');
796
+ const stepEl = document.getElementById('analysis-step');
797
+ const detailEl = document.getElementById('analysis-detail');
798
+
799
+ overlay.classList.remove('hidden');
800
+ stepEl.textContent = 'Analyzing accident scene photos...';
801
+ detailEl.textContent = 'Running AI vision analysis on each photo. This may take several minutes.';
802
+
803
+ try {
804
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/analyze`, {
805
+ method: 'POST',
806
+ });
807
+
808
+ if (!resp.ok) {
809
+ const err = await resp.json();
810
+ throw new Error(err.detail || 'Analysis failed');
811
+ }
812
+
813
+ const data = await resp.json();
814
+ overlay.classList.add('hidden');
815
+ showToast(`Analysis complete: ${data.violations_found} violations found`, 'success');
816
+
817
+ // Reload case detail
818
+ openCase(currentCaseId);
819
+
820
+ } catch (e) {
821
+ overlay.classList.add('hidden');
822
+ showToast(`Analysis failed: ${e.message}`, 'error');
823
+ }
824
+ }
825
+
826
+ // ── Report ────────────────────────────────────────────────────────────
827
+
828
+ async function viewReport() {
829
+ if (!currentCaseId) return;
830
+
831
+ switchView('report');
832
+
833
+ try {
834
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/report`);
835
+ const report = await resp.json();
836
+ renderReport(report);
837
+ } catch (e) {
838
+ showToast('Failed to load report', 'error');
839
+ }
840
+ }
841
+
842
+ function renderReport(report) {
843
+ const content = document.getElementById('report-content');
844
+
845
+ let html = `
846
+ <div class="report-header">
847
+ <h2><i class="fa-solid fa-shield-halved"></i> ${report.report_type || 'Incident Report'}</h2>
848
+ <p class="report-subtitle">Case ${escHtml(report.case?.case_number || '')} β€’ Generated by AI</p>
849
+ </div>
850
+
851
+ <div class="report-disclaimer">
852
+ <i class="fa-solid fa-triangle-exclamation"></i> ${escHtml(report.disclaimer || '')}
853
+ </div>
854
+ `;
855
+
856
+ // Stats
857
+ const stats = report.statistics || {};
858
+ html += `
859
+ <div class="report-stat-grid">
860
+ <div class="report-stat">
861
+ <div class="stat-number">${stats.total_photos || 0}</div>
862
+ <div class="stat-label">Photos</div>
863
+ </div>
864
+ <div class="report-stat">
865
+ <div class="stat-number">${stats.total_violations || 0}</div>
866
+ <div class="stat-label">Violations</div>
867
+ </div>
868
+ <div class="report-stat">
869
+ <div class="stat-number">${stats.critical_violations || 0}</div>
870
+ <div class="stat-label">Critical</div>
871
+ </div>
872
+ <div class="report-stat">
873
+ <div class="stat-number">${stats.parties_identified || 0}</div>
874
+ <div class="stat-label">Parties</div>
875
+ </div>
876
+ </div>
877
+ `;
878
+
879
+ // Case Info
880
+ html += `<h3>Case Information</h3>`;
881
+ html += `<p><strong>Case Number:</strong> ${escHtml(report.case?.case_number || 'N/A')}</p>`;
882
+ html += `<p><strong>Officer:</strong> ${escHtml(report.case?.officer_name || 'N/A')}</p>`;
883
+ html += `<p><strong>Location:</strong> ${escHtml(report.case?.location || 'N/A')}</p>`;
884
+ html += `<p><strong>Date:</strong> ${report.case?.incident_date || 'N/A'}</p>`;
885
+ if (report.case?.notes) html += `<p><strong>Notes:</strong> ${escHtml(report.case.notes)}</p>`;
886
+
887
+ // Scene Summary
888
+ if (report.scene_summary) {
889
+ html += `<h3>Scene Analysis</h3>`;
890
+ html += `<p style="white-space:pre-wrap">${escHtml(report.scene_summary)}</p>`;
891
+ }
892
+
893
+ // Parties
894
+ if (report.parties?.length) {
895
+ html += `<h3>Parties Involved</h3>`;
896
+ report.parties.forEach(p => {
897
+ html += `<h4>${escHtml(p.label)}</h4>`;
898
+ html += `<p>Type: ${p.vehicle_type || 'Unknown'} β€’ Color: ${p.vehicle_color || 'Unknown'}</p>`;
899
+ });
900
+ }
901
+
902
+ // Violations
903
+ if (report.violations?.list?.length) {
904
+ html += `<h3>Traffic Violations Detected</h3>`;
905
+ report.violations.list.forEach(v => {
906
+ html += `
907
+ <div class="violation-card ${v.severity}" style="margin-bottom:0.5rem">
908
+ <div class="violation-header">
909
+ <span class="violation-title">${escHtml(v.title)}</span>
910
+ <span class="severity-tag ${v.severity}">${v.severity}</span>
911
+ </div>
912
+ <p style="font-size:0.82rem;color:var(--text-secondary);margin-top:0.3rem">
913
+ Party: ${escHtml(v.party)} β€’ Confidence: ${Math.round(v.confidence * 100)}%
914
+ </p>
915
+ ${v.evidence ? `<p style="font-size:0.78rem;color:var(--text-muted);font-style:italic">${escHtml(v.evidence)}</p>` : ''}
916
+ </div>
917
+ `;
918
+ });
919
+ }
920
+
921
+ // Fault Analysis
922
+ if (report.fault_analysis?.determined) {
923
+ html += `<h3>Fault Analysis</h3>`;
924
+ html += `<p><strong>Primary Fault:</strong> ${escHtml(report.fault_analysis.primary_fault_party || 'Undetermined')}</p>`;
925
+
926
+ if (report.fault_analysis.fault_distribution) {
927
+ html += `<p><strong>Distribution:</strong> `;
928
+ html += Object.entries(report.fault_analysis.fault_distribution)
929
+ .map(([k, v]) => `${k}: ${v}%`).join(' β€’ ');
930
+ html += `</p>`;
931
+ }
932
+
933
+ html += `<p><strong>Confidence:</strong> ${Math.round((report.fault_analysis.overall_confidence || 0) * 100)}%</p>`;
934
+
935
+ if (report.fault_analysis.probable_cause) {
936
+ html += `<h4>Probable Cause</h4>`;
937
+ html += `<p>${escHtml(report.fault_analysis.probable_cause)}</p>`;
938
+ }
939
+ }
940
+
941
+ content.innerHTML = html;
942
+ }
943
+
944
+ // ── Rules ─────────────────────────────────────────────────────────────
945
+
946
+ async function loadRules() {
947
+ try {
948
+ const resp = await fetch(`${API_BASE}/rules`);
949
+ const data = await resp.json();
950
+ renderRules(data);
951
+ } catch (e) {
952
+ showToast('Failed to load rules', 'error');
953
+ }
954
+ }
955
+
956
+ function renderRules(data) {
957
+ const content = document.getElementById('rules-content');
958
+
959
+ if (!data.categories?.length) {
960
+ content.innerHTML = '<p class="placeholder-text">No rules loaded.</p>';
961
+ return;
962
+ }
963
+
964
+ content.innerHTML = data.categories.map(cat => `
965
+ <div class="rule-category">
966
+ <div class="rule-category-header">
967
+ <i class="fa-solid fa-gavel"></i>
968
+ ${escHtml(cat.name)}
969
+ <span class="rule-count">${cat.rule_count} rules</span>
970
+ </div>
971
+ <div class="rule-list">
972
+ ${cat.rules.map(r => `
973
+ <div class="rule-item">
974
+ <span class="rule-id">${r.id}</span>
975
+ <span>${escHtml(r.title)}</span>
976
+ <span class="severity-tag ${r.severity}" style="margin-left:auto">${r.severity}</span>
977
+ </div>
978
+ `).join('')}
979
+ </div>
980
+ </div>
981
+ `).join('');
982
+ }
983
+
984
+ // ── Toast ─────────────────────────────────────────────────────────────
985
+
986
+ function showToast(message, type = 'info') {
987
+ const container = document.getElementById('toast-container');
988
+ const toast = document.createElement('div');
989
+ toast.className = `toast ${type}`;
990
+ toast.textContent = message;
991
+ container.appendChild(toast);
992
+ setTimeout(() => toast.remove(), 4000);
993
+ }
994
+
995
+ // ── Helpers ───────────────────────────────────────────────────────────
996
+
997
+ function escHtml(str) {
998
+ if (!str) return '';
999
+ const div = document.createElement('div');
1000
+ div.textContent = str;
1001
+ return div.innerHTML;
1002
+ }
1003
+
1004
+ function formatDate(dateStr) {
1005
+ if (!dateStr) return '';
1006
+ try {
1007
+ const d = new Date(dateStr);
1008
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
1009
+ } catch { return dateStr; }
1010
+ }
js/app.js ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TraceScene β€” Frontend Application
3
+ *
4
+ * Single-page application for accident case management,
5
+ * photo upload, AI analysis, and report viewing.
6
+ */
7
+
8
+ const API_BASE = '/api';
9
+
10
+ // ── State ─────────────────────────────────────────────────────────────
11
+
12
+ let currentView = 'landing';
13
+ let currentCaseId = null;
14
+ let currentCaseData = null; // Store full case data for editing
15
+ let selectedFiles = [];
16
+ let additionalFiles = []; // For add photos modal
17
+
18
+ // ── Init ──────────────────────────────────────────────────────────────
19
+
20
+ document.addEventListener('DOMContentLoaded', () => {
21
+ initNavigation();
22
+ initDropZone();
23
+ initButtons();
24
+ initAccordions();
25
+ loadHealth();
26
+ loadCases();
27
+ });
28
+
29
+ // ── Accordions ────────────────────────────────────────────────────────
30
+
31
+ function initAccordions() {
32
+ document.querySelectorAll('.accordion-header').forEach(btn => {
33
+ btn.addEventListener('click', () => {
34
+ const item = btn.closest('.accordion-item');
35
+ const isActive = item.classList.contains('active');
36
+
37
+ // Close all other accordions
38
+ document.querySelectorAll('.accordion-item').forEach(i => {
39
+ i.classList.remove('active');
40
+ i.querySelector('.accordion-header').setAttribute('aria-expanded', 'false');
41
+ });
42
+
43
+ // Toggle current
44
+ if (!isActive) {
45
+ item.classList.add('active');
46
+ btn.setAttribute('aria-expanded', 'true');
47
+ }
48
+ });
49
+ });
50
+ }
51
+
52
+ // ── Navigation ────────────────────────────────────────────────────────
53
+
54
+ function initNavigation() {
55
+ document.querySelectorAll('.nav-item[data-view]').forEach(btn => {
56
+ btn.addEventListener('click', () => {
57
+ const view = btn.dataset.view;
58
+ switchView(view);
59
+ document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
60
+ btn.classList.add('active');
61
+ });
62
+ });
63
+ }
64
+
65
+ function switchView(viewName) {
66
+ document.querySelectorAll('.view').forEach(v => {
67
+ v.classList.remove('active');
68
+ v.classList.add('hidden');
69
+ });
70
+ const target = document.getElementById(`view-${viewName}`);
71
+ if (target) {
72
+ target.classList.remove('hidden');
73
+ target.classList.add('active');
74
+ }
75
+ currentView = viewName;
76
+
77
+ if (viewName === 'dashboard') loadCases();
78
+ if (viewName === 'rules') loadRules();
79
+ }
80
+
81
+ // ── Buttons ───────────────────────────────────────────────────────────
82
+
83
+ function initButtons() {
84
+ // Dashboard
85
+ document.getElementById('btn-refresh-cases')?.addEventListener('click', loadCases);
86
+ document.getElementById('btn-empty-new-case')?.addEventListener('click', () => {
87
+ switchView('new-case');
88
+ document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
89
+ document.querySelector('[data-view="new-case"]')?.classList.add('active');
90
+ });
91
+
92
+ // New Case
93
+ document.getElementById('btn-create-case')?.addEventListener('click', createCase);
94
+
95
+ // Case Detail
96
+ document.getElementById('btn-back-dashboard')?.addEventListener('click', () => switchView('dashboard'));
97
+ document.getElementById('btn-run-analysis')?.addEventListener('click', runAnalysis);
98
+ document.getElementById('btn-view-report')?.addEventListener('click', viewReport);
99
+ document.getElementById('btn-edit-case')?.addEventListener('click', openEditModal);
100
+ document.getElementById('btn-add-photos-inline')?.addEventListener('click', openAddPhotosModal);
101
+
102
+ // Edit Modal
103
+ document.getElementById('btn-close-edit-modal')?.addEventListener('click', closeEditModal);
104
+ document.getElementById('btn-cancel-edit')?.addEventListener('click', closeEditModal);
105
+ document.getElementById('btn-save-case')?.addEventListener('click', saveCaseChanges);
106
+
107
+ // Photos Modal
108
+ document.getElementById('btn-close-photos-modal')?.addEventListener('click', closePhotosModal);
109
+ document.getElementById('btn-cancel-photos')?.addEventListener('click', closePhotosModal);
110
+ document.getElementById('btn-upload-more')?.addEventListener('click', uploadMorePhotos);
111
+
112
+ // Form logic
113
+ document.getElementById('case-number')?.addEventListener('input', validateForm);
114
+ initEditDropZone();
115
+ }
116
+
117
+ function initEditDropZone() {
118
+ const dropZone = document.getElementById('edit-drop-zone');
119
+ const fileInput = document.getElementById('edit-file-input');
120
+ if (!dropZone || !fileInput) return;
121
+
122
+ dropZone.addEventListener('click', () => fileInput.click());
123
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
124
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
125
+ dropZone.addEventListener('drop', e => {
126
+ e.preventDefault();
127
+ dropZone.classList.remove('drag-over');
128
+ handleAdditionalFiles(Array.from(e.dataTransfer.files));
129
+ });
130
+ fileInput.addEventListener('change', () => {
131
+ handleAdditionalFiles(Array.from(fileInput.files));
132
+ });
133
+ }
134
+
135
+ function validateForm() {
136
+ const caseNum = document.getElementById('case-number')?.value.trim();
137
+ const btn = document.getElementById('btn-create-case');
138
+ if (btn) btn.disabled = !caseNum;
139
+ }
140
+
141
+ // ── Health ─────────────────────────────────────────────────────────────
142
+
143
+ async function loadHealth() {
144
+ try {
145
+ const resp = await fetch(`${API_BASE}/health`);
146
+ const data = await resp.json();
147
+ document.getElementById('status-model').textContent = data.model_loaded ? 'βœ“ Ready' : 'βœ— Not loaded';
148
+ document.getElementById('status-model').style.color = data.model_loaded ? '#22c55e' : '#ef4444';
149
+ document.getElementById('status-device').textContent = data.device || 'β€”';
150
+ document.getElementById('status-rules').textContent = `${data.rules_loaded} rules`;
151
+ } catch {
152
+ document.getElementById('status-model').textContent = 'βœ— Offline';
153
+ document.getElementById('status-model').style.color = '#ef4444';
154
+ }
155
+ }
156
+
157
+ // ── Cases ─────────────────────────────────────────────────────────────
158
+
159
+ async function loadCases() {
160
+ try {
161
+ const resp = await fetch(`${API_BASE}/cases`);
162
+ const data = await resp.json();
163
+ renderCases(data.cases || []);
164
+ } catch (e) {
165
+ showToast('Failed to load cases', 'error');
166
+ }
167
+ }
168
+
169
+ function renderCases(cases) {
170
+ const grid = document.getElementById('cases-grid');
171
+ const empty = document.getElementById('empty-state-dashboard');
172
+
173
+ if (!cases.length) {
174
+ grid.innerHTML = '';
175
+ grid.style.display = 'none';
176
+ empty.style.display = 'flex';
177
+ return;
178
+ }
179
+
180
+ grid.style.display = 'grid';
181
+ empty.style.display = 'none';
182
+
183
+ grid.innerHTML = cases.map(c => `
184
+ <div class="case-card" onclick="openCase(${c.id})">
185
+ <button class="btn-delete-card" onclick="event.stopPropagation(); deleteCase(${c.id}, '${escHtml(c.case_number)}')" title="Delete Case">
186
+ <i class="fa-solid fa-trash-can"></i>
187
+ </button>
188
+ <div class="card-header">
189
+ <span class="case-number">${escHtml(c.case_number)}</span>
190
+ <span class="status-badge ${c.status}">
191
+ <i class="fa-solid fa-circle"></i> ${c.status}
192
+ </span>
193
+ </div>
194
+ <div class="case-meta">
195
+ ${c.officer_name ? `<span><i class="fa-solid fa-user-shield"></i> ${escHtml(c.officer_name)}</span>` : ''}
196
+ ${c.location ? `<span><i class="fa-solid fa-location-dot"></i> ${escHtml(c.location)}</span>` : ''}
197
+ ${c.incident_date ? `<span><i class="fa-solid fa-calendar"></i> ${c.incident_date}</span>` : ''}
198
+ </div>
199
+ <div class="card-footer">
200
+ <span class="photo-count"><i class="fa-solid fa-camera"></i> ${c.photo_count || 0} photos</span>
201
+ <span style="font-size:0.72rem;color:var(--text-muted)">${formatDate(c.created_at)}</span>
202
+ </div>
203
+ </div>
204
+ `).join('');
205
+ }
206
+
207
+ async function deleteCase(id, caseNum) {
208
+ if (!confirm(`Are you sure you want to delete Case ${caseNum}? This will permanently remove all photos and analysis data.`)) {
209
+ return;
210
+ }
211
+
212
+ try {
213
+ const resp = await fetch(`${API_BASE}/cases/${id}`, { method: 'DELETE' });
214
+ if (!resp.ok) throw new Error('Delete failed');
215
+ showToast(`Case ${caseNum} deleted`, 'success');
216
+ loadCases();
217
+ } catch (e) {
218
+ showToast(e.message, 'error');
219
+ }
220
+ }
221
+
222
+ // ── Create Case ───────────────────────────────────────────────────────
223
+
224
+ async function createCase() {
225
+ const caseNumber = document.getElementById('case-number').value.trim();
226
+ if (!caseNumber) return showToast('Case number is required', 'error');
227
+
228
+ const formData = new FormData();
229
+ formData.append('case_number', caseNumber);
230
+ formData.append('officer_name', document.getElementById('officer-name').value.trim());
231
+ formData.append('location', document.getElementById('incident-location').value.trim());
232
+ formData.append('incident_date', document.getElementById('incident-date').value);
233
+ formData.append('notes', document.getElementById('officer-notes').value.trim());
234
+
235
+ try {
236
+ const resp = await fetch(`${API_BASE}/cases`, { method: 'POST', body: formData });
237
+ if (!resp.ok) {
238
+ const err = await resp.json();
239
+ throw new Error(err.detail || 'Failed to create case');
240
+ }
241
+ const data = await resp.json();
242
+ const caseId = data.id;
243
+
244
+ showToast('Case created', 'success');
245
+
246
+ // Upload photos if selected
247
+ if (selectedFiles.length > 0) {
248
+ await uploadPhotos(caseId);
249
+ }
250
+
251
+ // Clear form
252
+ document.getElementById('case-number').value = '';
253
+ document.getElementById('officer-name').value = '';
254
+ document.getElementById('incident-location').value = '';
255
+ document.getElementById('incident-date').value = '';
256
+ document.getElementById('officer-notes').value = '';
257
+ selectedFiles = [];
258
+ document.getElementById('photo-preview').classList.add('hidden');
259
+
260
+ // Open the case detail
261
+ openCase(caseId);
262
+
263
+ } catch (e) {
264
+ showToast(e.message, 'error');
265
+ }
266
+ }
267
+
268
+ async function uploadPhotos(caseId) {
269
+ const progressSection = document.getElementById('upload-progress');
270
+ const progressFill = document.getElementById('upload-progress-fill');
271
+ const statusText = document.getElementById('upload-status-text');
272
+
273
+ progressSection.classList.remove('hidden');
274
+ statusText.textContent = `Uploading ${selectedFiles.length} photos...`;
275
+
276
+ const formData = new FormData();
277
+ selectedFiles.forEach(f => formData.append('files', f));
278
+
279
+ try {
280
+ progressFill.style.width = '50%';
281
+ const resp = await fetch(`${API_BASE}/cases/${caseId}/photos`, {
282
+ method: 'POST',
283
+ body: formData,
284
+ });
285
+
286
+ if (!resp.ok) throw new Error('Upload failed');
287
+
288
+ const data = await resp.json();
289
+ progressFill.style.width = '100%';
290
+ statusText.textContent = `${data.count} photos uploaded βœ“`;
291
+ showToast(`${data.count} photos uploaded`, 'success');
292
+ return data;
293
+ } catch (e) {
294
+ statusText.textContent = 'Upload failed';
295
+ showToast('Photo upload failed', 'error');
296
+ throw e;
297
+ }
298
+ }
299
+
300
+ // ── Drop Zone ─────────────────────────────────────────────────────────
301
+
302
+ function initDropZone() {
303
+ const dropZone = document.getElementById('drop-zone');
304
+ const fileInput = document.getElementById('file-input');
305
+ if (!dropZone || !fileInput) return;
306
+
307
+ dropZone.addEventListener('click', () => fileInput.click());
308
+
309
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
310
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
311
+ dropZone.addEventListener('drop', e => {
312
+ e.preventDefault();
313
+ dropZone.classList.remove('drag-over');
314
+ handleFiles(Array.from(e.dataTransfer.files));
315
+ });
316
+
317
+ fileInput.addEventListener('change', () => {
318
+ handleFiles(Array.from(fileInput.files));
319
+ });
320
+ }
321
+
322
+ function handleFiles(files) {
323
+ const imageFiles = files.filter(f => f.type.startsWith('image/'));
324
+ if (!imageFiles.length) return showToast('No image files found', 'error');
325
+
326
+ selectedFiles = imageFiles.slice(0, 20); // Max 20
327
+
328
+ // Show previews
329
+ const preview = document.getElementById('photo-preview');
330
+ const thumbs = document.getElementById('photo-thumbnails');
331
+ const count = document.getElementById('photo-count');
332
+
333
+ preview.classList.remove('hidden');
334
+ count.textContent = `${selectedFiles.length} photo${selectedFiles.length > 1 ? 's' : ''} selected`;
335
+
336
+ thumbs.innerHTML = '';
337
+ selectedFiles.forEach(file => {
338
+ const div = document.createElement('div');
339
+ div.className = 'photo-thumbnail';
340
+ const img = document.createElement('img');
341
+ img.src = URL.createObjectURL(file);
342
+ div.appendChild(img);
343
+ thumbs.appendChild(div);
344
+ });
345
+
346
+ validateForm();
347
+ }
348
+
349
+ // ── Open Case Detail ──────────────────────────────────────────────────
350
+
351
+ async function openCase(caseId) {
352
+ currentCaseId = caseId;
353
+ switchView('case-detail');
354
+
355
+ try {
356
+ const resp = await fetch(`${API_BASE}/cases/${caseId}`);
357
+ const data = await resp.json();
358
+ currentCaseData = data.case;
359
+ renderCaseDetail(data);
360
+ } catch (e) {
361
+ showToast('Failed to load case', 'error');
362
+ }
363
+ }
364
+
365
+ // ── Edit Case ─────────────────────────────────────────────────────────
366
+
367
+ function openEditModal() {
368
+ if (!currentCaseData) return;
369
+ document.getElementById('edit-officer-name').value = currentCaseData.officer_name || '';
370
+ document.getElementById('edit-incident-location').value = currentCaseData.location || '';
371
+ document.getElementById('edit-incident-date').value = currentCaseData.incident_date || '';
372
+ document.getElementById('edit-officer-notes').value = currentCaseData.notes || '';
373
+ document.getElementById('modal-edit-case').classList.remove('hidden');
374
+ }
375
+
376
+ function closeEditModal() {
377
+ document.getElementById('modal-edit-case').classList.add('hidden');
378
+ }
379
+
380
+ async function saveCaseChanges() {
381
+ if (!currentCaseId) return;
382
+
383
+ const formData = new FormData();
384
+ formData.append('officer_name', document.getElementById('edit-officer-name').value.trim());
385
+ formData.append('location', document.getElementById('edit-incident-location').value.trim());
386
+ formData.append('incident_date', document.getElementById('edit-incident-date').value);
387
+ formData.append('notes', document.getElementById('edit-officer-notes').value.trim());
388
+
389
+ try {
390
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}`, {
391
+ method: 'PUT',
392
+ body: formData
393
+ });
394
+
395
+ if (!resp.ok) throw new Error('Failed to update case');
396
+
397
+ showToast('Case updated', 'success');
398
+ closeEditModal();
399
+ openCase(currentCaseId); // Refresh
400
+ } catch (e) {
401
+ showToast(e.message, 'error');
402
+ }
403
+ }
404
+
405
+ // ── Add Photos ────────────────────────────────────────────────────────
406
+
407
+ function openAddPhotosModal() {
408
+ additionalFiles = [];
409
+ document.getElementById('edit-photo-preview').classList.add('hidden');
410
+ document.getElementById('edit-upload-progress').classList.add('hidden');
411
+ document.getElementById('btn-upload-more').disabled = true;
412
+ document.getElementById('modal-add-photos').classList.remove('hidden');
413
+ }
414
+
415
+ function closePhotosModal() {
416
+ document.getElementById('modal-add-photos').classList.add('hidden');
417
+ }
418
+
419
+ function handleAdditionalFiles(files) {
420
+ const imageFiles = files.filter(f => f.type.startsWith('image/'));
421
+ if (!imageFiles.length) return showToast('No image files found', 'error');
422
+
423
+ additionalFiles = imageFiles.slice(0, 20);
424
+
425
+ const preview = document.getElementById('edit-photo-preview');
426
+ const thumbs = document.getElementById('edit-photo-thumbnails');
427
+ const count = document.getElementById('edit-photo-count');
428
+
429
+ preview.classList.remove('hidden');
430
+ count.textContent = `${additionalFiles.length} photo${additionalFiles.length > 1 ? 's' : ''} selected`;
431
+
432
+ thumbs.innerHTML = '';
433
+ additionalFiles.forEach(file => {
434
+ const div = document.createElement('div');
435
+ div.className = 'photo-thumbnail';
436
+ const img = document.createElement('img');
437
+ img.src = URL.createObjectURL(file);
438
+ div.appendChild(img);
439
+ thumbs.appendChild(div);
440
+ });
441
+
442
+ document.getElementById('btn-upload-more').disabled = additionalFiles.length === 0;
443
+ }
444
+
445
+ async function uploadMorePhotos() {
446
+ if (!currentCaseId || !additionalFiles.length) return;
447
+
448
+ const progressSection = document.getElementById('edit-upload-progress');
449
+ const progressFill = document.getElementById('edit-upload-progress-fill');
450
+ const statusText = document.getElementById('edit-upload-status-text');
451
+
452
+ progressSection.classList.remove('hidden');
453
+ document.getElementById('btn-upload-more').disabled = true;
454
+
455
+ const formData = new FormData();
456
+ additionalFiles.forEach(f => formData.append('files', f));
457
+
458
+ try {
459
+ progressFill.style.width = '50%';
460
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/photos`, {
461
+ method: 'POST',
462
+ body: formData,
463
+ });
464
+
465
+ if (!resp.ok) throw new Error('Upload failed');
466
+
467
+ progressFill.style.width = '100%';
468
+ statusText.textContent = `Upload complete βœ“`;
469
+ showToast('Photos added successfully', 'success');
470
+
471
+ setTimeout(() => {
472
+ closePhotosModal();
473
+ openCase(currentCaseId); // Refresh
474
+ }, 1000);
475
+ } catch (e) {
476
+ statusText.textContent = 'Upload failed';
477
+ showToast('Photo upload failed', 'error');
478
+ document.getElementById('btn-upload-more').disabled = false;
479
+ }
480
+ }
481
+
482
+ function renderCaseDetail(data) {
483
+ const c = data.case;
484
+
485
+ // Header
486
+ document.getElementById('case-detail-title').textContent = `Case: ${c.case_number}`;
487
+
488
+ // Info bar
489
+ document.getElementById('detail-case-number').textContent = c.case_number;
490
+ document.getElementById('detail-officer').textContent = c.officer_name || 'N/A';
491
+ document.getElementById('detail-location').textContent = c.location || 'N/A';
492
+ document.getElementById('detail-date').textContent = c.incident_date || 'N/A';
493
+ document.getElementById('detail-status').textContent = c.status;
494
+
495
+ const statusChip = document.getElementById('detail-status-chip');
496
+ statusChip.className = `info-chip status-chip ${c.status}`;
497
+
498
+ // Enable/disable buttons
499
+ const btnAnalysis = document.getElementById('btn-run-analysis');
500
+ const btnReport = document.getElementById('btn-view-report');
501
+ btnAnalysis.disabled = !data.photos?.length;
502
+ btnReport.disabled = c.status !== 'complete';
503
+
504
+ // Photos
505
+ const photosGrid = document.getElementById('detail-photos-grid');
506
+ document.getElementById('photo-badge').textContent = data.photos?.length || 0;
507
+
508
+ if (data.photos?.length) {
509
+ photosGrid.innerHTML = data.photos.map(p => `
510
+ <div class="detail-photo">
511
+ <img src="${API_BASE.replace('/api', '')}/api/photos/${p.id}/image" alt="${escHtml(p.filename)}" loading="lazy">
512
+ </div>
513
+ `).join('');
514
+ } else {
515
+ photosGrid.innerHTML = '<p class="placeholder-text">No photos uploaded yet.</p>';
516
+ }
517
+
518
+ // Analyses
519
+ const analysisContent = document.getElementById('analysis-content');
520
+ if (data.analyses?.length) {
521
+ analysisContent.innerHTML = data.analyses.map(a => `
522
+ <div class="analysis-photo-section">
523
+ <div class="analysis-photo-label">πŸ“· ${escHtml(a.filename || 'Photo')}</div>
524
+ <pre>${escHtml(a.raw_analysis || '')}</pre>
525
+ </div>
526
+ `).join('');
527
+ } else {
528
+ analysisContent.innerHTML = '<p class="placeholder-text">Run analysis to see AI observations.</p>';
529
+ }
530
+
531
+ // Violations
532
+ const violationsList = document.getElementById('violations-list');
533
+ const violationBadge = document.getElementById('violation-badge');
534
+ violationBadge.textContent = data.violations?.length || 0;
535
+
536
+ if (data.violations?.length) {
537
+ violationsList.innerHTML = data.violations.map(v => `
538
+ <div class="violation-card ${v.severity || 'MEDIUM'}">
539
+ <div class="violation-header">
540
+ <span class="violation-title">${escHtml(v.rule_title)}</span>
541
+ <span class="severity-tag ${v.severity || 'MEDIUM'}">${v.severity || 'MEDIUM'}</span>
542
+ </div>
543
+ <div class="violation-meta">
544
+ <span><i class="fa-solid fa-hashtag"></i> ${v.rule_id}</span>
545
+ <span><i class="fa-solid fa-percent"></i> ${Math.round(v.confidence * 100)}% confidence</span>
546
+ ${v.party_label ? `<span><i class="fa-solid fa-car"></i> ${escHtml(v.party_label)}</span>` : ''}
547
+ </div>
548
+ ${v.evidence_summary ? `<div class="violation-evidence">${escHtml(v.evidence_summary)}</div>` : ''}
549
+ </div>
550
+ `).join('');
551
+ } else {
552
+ violationsList.innerHTML = '<p class="placeholder-text">No violations detected yet.</p>';
553
+ }
554
+
555
+ // Fault Analysis
556
+ renderFaultAnalysis(data.fault_analysis, data.parties);
557
+ }
558
+
559
+ function renderFaultAnalysis(fault, parties) {
560
+ const content = document.getElementById('fault-content');
561
+
562
+ if (!fault) {
563
+ content.innerHTML = '<p class="placeholder-text">Run analysis to determine fault.</p>';
564
+ return;
565
+ }
566
+
567
+ let html = '';
568
+
569
+ // Fault distribution bars
570
+ if (fault.fault_distribution_json) {
571
+ let dist = {};
572
+ try {
573
+ dist = typeof fault.fault_distribution_json === 'string'
574
+ ? JSON.parse(fault.fault_distribution_json)
575
+ : fault.fault_distribution_json;
576
+ } catch { dist = {}; }
577
+
578
+ const entries = Object.entries(dist);
579
+ if (entries.length) {
580
+ html += '<div class="fault-party-bars">';
581
+ entries.forEach(([label, pct], i) => {
582
+ html += `
583
+ <div class="party-bar">
584
+ <div class="party-bar-label">
585
+ <span class="party-name">${escHtml(label)}</span>
586
+ <span class="party-pct">${pct}%</span>
587
+ </div>
588
+ <div class="party-bar-track">
589
+ <div class="party-bar-fill ${i === 0 ? 'primary' : 'secondary'}"
590
+ style="width: ${pct}%"></div>
591
+ </div>
592
+ </div>
593
+ `;
594
+ });
595
+ html += '</div>';
596
+ }
597
+ }
598
+
599
+ // Probable cause
600
+ if (fault.probable_cause) {
601
+ html += `
602
+ <div class="fault-cause">
603
+ <h4><i class="fa-solid fa-magnifying-glass-chart"></i> Probable Cause</h4>
604
+ <p>${escHtml(fault.probable_cause)}</p>
605
+ </div>
606
+ `;
607
+ }
608
+
609
+ // Confidence
610
+ if (fault.overall_confidence != null) {
611
+ const pct = Math.round(fault.overall_confidence * 100);
612
+ html += `
613
+ <div class="confidence-meter">
614
+ <span>Confidence:</span>
615
+ <div class="meter-bar">
616
+ <div class="meter-fill" style="width: ${pct}%"></div>
617
+ </div>
618
+ <span>${pct}%</span>
619
+ </div>
620
+ `;
621
+ }
622
+
623
+ // Summary
624
+ if (fault.analysis_summary) {
625
+ html += `<p style="margin-top:0.8rem;font-size:0.82rem;color:var(--text-secondary)">
626
+ ${escHtml(fault.analysis_summary)}</p>`;
627
+ }
628
+
629
+ content.innerHTML = html || '<p class="placeholder-text">No fault analysis available.</p>';
630
+ }
631
+
632
+ // ── Run Analysis ──────────────────────────────────────────────────────
633
+
634
+ async function runAnalysis() {
635
+ if (!currentCaseId) return;
636
+
637
+ const overlay = document.getElementById('analysis-overlay');
638
+ const stepEl = document.getElementById('analysis-step');
639
+ const detailEl = document.getElementById('analysis-detail');
640
+
641
+ overlay.classList.remove('hidden');
642
+ stepEl.textContent = 'Analyzing accident scene photos...';
643
+ detailEl.textContent = 'Running AI vision analysis on each photo. This may take several minutes.';
644
+
645
+ try {
646
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/analyze`, {
647
+ method: 'POST',
648
+ });
649
+
650
+ if (!resp.ok) {
651
+ const err = await resp.json();
652
+ throw new Error(err.detail || 'Analysis failed');
653
+ }
654
+
655
+ const data = await resp.json();
656
+ overlay.classList.add('hidden');
657
+ showToast(`Analysis complete: ${data.violations_found} violations found`, 'success');
658
+
659
+ // Reload case detail
660
+ openCase(currentCaseId);
661
+
662
+ } catch (e) {
663
+ overlay.classList.add('hidden');
664
+ showToast(`Analysis failed: ${e.message}`, 'error');
665
+ }
666
+ }
667
+
668
+ // ── Report ────────────────────────────────────────────────────────────
669
+
670
+ async function viewReport() {
671
+ if (!currentCaseId) return;
672
+
673
+ switchView('report');
674
+
675
+ try {
676
+ const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/report`);
677
+ const report = await resp.json();
678
+ renderReport(report);
679
+ } catch (e) {
680
+ showToast('Failed to load report', 'error');
681
+ }
682
+ }
683
+
684
+ function renderReport(report) {
685
+ const content = document.getElementById('report-content');
686
+
687
+ let html = `
688
+ <div class="report-header">
689
+ <h2><i class="fa-solid fa-shield-halved"></i> ${report.report_type || 'Incident Report'}</h2>
690
+ <p class="report-subtitle">Case ${escHtml(report.case?.case_number || '')} β€’ Generated by AI</p>
691
+ </div>
692
+
693
+ <div class="report-disclaimer">
694
+ <i class="fa-solid fa-triangle-exclamation"></i> ${escHtml(report.disclaimer || '')}
695
+ </div>
696
+ `;
697
+
698
+ // Stats
699
+ const stats = report.statistics || {};
700
+ html += `
701
+ <div class="report-stat-grid">
702
+ <div class="report-stat">
703
+ <div class="stat-number">${stats.total_photos || 0}</div>
704
+ <div class="stat-label">Photos</div>
705
+ </div>
706
+ <div class="report-stat">
707
+ <div class="stat-number">${stats.total_violations || 0}</div>
708
+ <div class="stat-label">Violations</div>
709
+ </div>
710
+ <div class="report-stat">
711
+ <div class="stat-number">${stats.critical_violations || 0}</div>
712
+ <div class="stat-label">Critical</div>
713
+ </div>
714
+ <div class="report-stat">
715
+ <div class="stat-number">${stats.parties_identified || 0}</div>
716
+ <div class="stat-label">Parties</div>
717
+ </div>
718
+ </div>
719
+ `;
720
+
721
+ // Case Info
722
+ html += `<h3>Case Information</h3>`;
723
+ html += `<p><strong>Case Number:</strong> ${escHtml(report.case?.case_number || 'N/A')}</p>`;
724
+ html += `<p><strong>Officer:</strong> ${escHtml(report.case?.officer_name || 'N/A')}</p>`;
725
+ html += `<p><strong>Location:</strong> ${escHtml(report.case?.location || 'N/A')}</p>`;
726
+ html += `<p><strong>Date:</strong> ${report.case?.incident_date || 'N/A'}</p>`;
727
+ if (report.case?.notes) html += `<p><strong>Notes:</strong> ${escHtml(report.case.notes)}</p>`;
728
+
729
+ // Scene Summary
730
+ if (report.scene_summary) {
731
+ html += `<h3>Scene Analysis</h3>`;
732
+ html += `<p style="white-space:pre-wrap">${escHtml(report.scene_summary)}</p>`;
733
+ }
734
+
735
+ // Parties
736
+ if (report.parties?.length) {
737
+ html += `<h3>Parties Involved</h3>`;
738
+ report.parties.forEach(p => {
739
+ html += `<h4>${escHtml(p.label)}</h4>`;
740
+ html += `<p>Type: ${p.vehicle_type || 'Unknown'} β€’ Color: ${p.vehicle_color || 'Unknown'}</p>`;
741
+ });
742
+ }
743
+
744
+ // Violations
745
+ if (report.violations?.list?.length) {
746
+ html += `<h3>Traffic Violations Detected</h3>`;
747
+ report.violations.list.forEach(v => {
748
+ html += `
749
+ <div class="violation-card ${v.severity}" style="margin-bottom:0.5rem">
750
+ <div class="violation-header">
751
+ <span class="violation-title">${escHtml(v.title)}</span>
752
+ <span class="severity-tag ${v.severity}">${v.severity}</span>
753
+ </div>
754
+ <p style="font-size:0.82rem;color:var(--text-secondary);margin-top:0.3rem">
755
+ Party: ${escHtml(v.party)} β€’ Confidence: ${Math.round(v.confidence * 100)}%
756
+ </p>
757
+ ${v.evidence ? `<p style="font-size:0.78rem;color:var(--text-muted);font-style:italic">${escHtml(v.evidence)}</p>` : ''}
758
+ </div>
759
+ `;
760
+ });
761
+ }
762
+
763
+ // Fault Analysis
764
+ if (report.fault_analysis?.determined) {
765
+ html += `<h3>Fault Analysis</h3>`;
766
+ html += `<p><strong>Primary Fault:</strong> ${escHtml(report.fault_analysis.primary_fault_party || 'Undetermined')}</p>`;
767
+
768
+ if (report.fault_analysis.fault_distribution) {
769
+ html += `<p><strong>Distribution:</strong> `;
770
+ html += Object.entries(report.fault_analysis.fault_distribution)
771
+ .map(([k, v]) => `${k}: ${v}%`).join(' β€’ ');
772
+ html += `</p>`;
773
+ }
774
+
775
+ html += `<p><strong>Confidence:</strong> ${Math.round((report.fault_analysis.overall_confidence || 0) * 100)}%</p>`;
776
+
777
+ if (report.fault_analysis.probable_cause) {
778
+ html += `<h4>Probable Cause</h4>`;
779
+ html += `<p>${escHtml(report.fault_analysis.probable_cause)}</p>`;
780
+ }
781
+ }
782
+
783
+ content.innerHTML = html;
784
+ }
785
+
786
+ // ── Rules ─────────────────────────────────────────────────────────────
787
+
788
+ async function loadRules() {
789
+ try {
790
+ const resp = await fetch(`${API_BASE}/rules`);
791
+ const data = await resp.json();
792
+ renderRules(data);
793
+ } catch (e) {
794
+ showToast('Failed to load rules', 'error');
795
+ }
796
+ }
797
+
798
+ function renderRules(data) {
799
+ const content = document.getElementById('rules-content');
800
+
801
+ if (!data.categories?.length) {
802
+ content.innerHTML = '<p class="placeholder-text">No rules loaded.</p>';
803
+ return;
804
+ }
805
+
806
+ content.innerHTML = data.categories.map(cat => `
807
+ <div class="rule-category">
808
+ <div class="rule-category-header">
809
+ <i class="fa-solid fa-gavel"></i>
810
+ ${escHtml(cat.name)}
811
+ <span class="rule-count">${cat.rule_count} rules</span>
812
+ </div>
813
+ <div class="rule-list">
814
+ ${cat.rules.map(r => `
815
+ <div class="rule-item">
816
+ <span class="rule-id">${r.id}</span>
817
+ <span>${escHtml(r.title)}</span>
818
+ <span class="severity-tag ${r.severity}" style="margin-left:auto">${r.severity}</span>
819
+ </div>
820
+ `).join('')}
821
+ </div>
822
+ </div>
823
+ `).join('');
824
+ }
825
+
826
+ // ── Toast ─────────────────────────────────────────────────────────────
827
+
828
+ function showToast(message, type = 'info') {
829
+ const container = document.getElementById('toast-container');
830
+ const toast = document.createElement('div');
831
+ toast.className = `toast ${type}`;
832
+ toast.textContent = message;
833
+ container.appendChild(toast);
834
+ setTimeout(() => toast.remove(), 4000);
835
+ }
836
+
837
+ // ── Helpers ───────────────────────────────────────────────────────────
838
+
839
+ function escHtml(str) {
840
+ if (!str) return '';
841
+ const div = document.createElement('div');
842
+ div.textContent = str;
843
+ return div.innerHTML;
844
+ }
845
+
846
+ function formatDate(dateStr) {
847
+ if (!dateStr) return '';
848
+ try {
849
+ const d = new Date(dateStr);
850
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
851
+ } catch { return dateStr; }
852
+ }