@hugpy/video-intelligence-ui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/package.json +1 -3
  2. package/src/config.ts +36 -0
  3. package/src/demo/assets/generate/cinema-cavern/movie.mp4 +0 -0
  4. package/src/demo/assets/generate/cinema-cavern/seg_00.mp4 +0 -0
  5. package/src/demo/assets/generate/clips/cat-lounge.mp4 +0 -0
  6. package/src/demo/assets/generate/clips/drone-city.mp4 +0 -0
  7. package/src/demo/assets/generate/clips/paper-sails.mp4 +0 -0
  8. package/src/demo/assets/generate/movie-galaxy/movie.mp4 +0 -0
  9. package/src/demo/assets/generate/movie-galaxy/seg_00.mp4 +0 -0
  10. package/src/demo/assets/generate/movie-galaxy/seg_01.mp4 +0 -0
  11. package/src/demo/assets/generate/movie-galaxy/seg_02.mp4 +0 -0
  12. package/src/demo/assets/generate/movie-galaxy/seg_03.mp4 +0 -0
  13. package/src/demo/assets/generate/movie-medow/movie.mp4 +0 -0
  14. package/src/demo/assets/generate/movie-medow/seg_00.mp4 +0 -0
  15. package/src/demo/assets/generate/movie-medow/seg_01.mp4 +0 -0
  16. package/src/demo/assets/generate/scene-desert/frame_00000.png +0 -0
  17. package/src/demo/assets/generate/scene-desert/frame_00001.png +0 -0
  18. package/src/demo/assets/generate/scene-desert/frame_00002.png +0 -0
  19. package/src/demo/assets/generate/scene-desert/frame_00003.png +0 -0
  20. package/src/demo/assets/generate/scene-desert/frame_00004.png +0 -0
  21. package/src/demo/assets/generate/scene-desert/frame_00005.png +0 -0
  22. package/src/demo/assets/generate/scene-desert/scene.mp4 +0 -0
  23. package/src/demo/assets/generate/scene-vintage/frame_00000.png +0 -0
  24. package/src/demo/assets/generate/scene-vintage/frame_00001.png +0 -0
  25. package/src/demo/assets/generate/scene-vintage/frame_00002.png +0 -0
  26. package/src/demo/assets/generate/scene-vintage/frame_00003.png +0 -0
  27. package/src/demo/assets/generate/scene-vintage/frame_00004.png +0 -0
  28. package/src/demo/assets/generate/scene-vintage/frame_00005.png +0 -0
  29. package/src/demo/assets/generate/scene-vintage/scene.mp4 +0 -0
  30. package/src/demo/assets/generate2/s0.png +0 -0
  31. package/src/demo/assets/generate2/s1.png +0 -0
  32. package/src/demo/assets/generate2/s10.png +0 -0
  33. package/src/demo/assets/generate2/s11.png +0 -0
  34. package/src/demo/assets/generate2/s12.png +0 -0
  35. package/src/demo/assets/generate2/s13.png +0 -0
  36. package/src/demo/assets/generate2/s14.png +0 -0
  37. package/src/demo/assets/generate2/s15.png +0 -0
  38. package/src/demo/assets/generate2/s16.png +0 -0
  39. package/src/demo/assets/generate2/s17.png +0 -0
  40. package/src/demo/assets/generate2/s18.png +0 -0
  41. package/src/demo/assets/generate2/s19.png +0 -0
  42. package/src/demo/assets/generate2/s2.png +0 -0
  43. package/src/demo/assets/generate2/s20.png +0 -0
  44. package/src/demo/assets/generate2/s21.mp4 +0 -0
  45. package/src/demo/assets/generate2/s22.mp4 +0 -0
  46. package/src/demo/assets/generate2/s23.mp4 +0 -0
  47. package/src/demo/assets/generate2/s24.mp4 +0 -0
  48. package/src/demo/assets/generate2/s25.mp4 +0 -0
  49. package/src/demo/assets/generate2/s26.mp4 +0 -0
  50. package/src/demo/assets/generate2/s27.mp4 +0 -0
  51. package/src/demo/assets/generate2/s28.mp4 +0 -0
  52. package/src/demo/assets/generate2/s29.mp4 +0 -0
  53. package/src/demo/assets/generate2/s3.png +0 -0
  54. package/src/demo/assets/generate2/s30.mp4 +0 -0
  55. package/src/demo/assets/generate2/s4.png +0 -0
  56. package/src/demo/assets/generate2/s5.png +0 -0
  57. package/src/demo/assets/generate2/s6.png +0 -0
  58. package/src/demo/assets/generate2/s7.png +0 -0
  59. package/src/demo/assets/generate2/s8.png +0 -0
  60. package/src/demo/assets/generate2/s9.png +0 -0
  61. package/src/demo/demoFetch.ts +126 -3
  62. package/src/demo/fixtures.ts +72 -4
  63. package/src/demo/promptAssist.ts +25 -0
  64. package/src/demo/sampleData.ts +960 -0
  65. package/src/demo/sampleSets.ts +172 -0
  66. package/src/stations/ActiveProcessesStation.tsx +18 -3
  67. package/src/stations/GenerateStation.tsx +1031 -364
  68. package/src/stations/GoalComposer.tsx +9 -1
  69. package/src/stations/ScriptFirstStation.tsx +1081 -0
  70. package/src/stations/ToksPanel.tsx +188 -0
  71. package/src/stations/WorkbenchDrawer.tsx +4 -3
  72. package/src/stations/WorkbenchSidebar.tsx +60 -12
  73. package/src/stations/oracle/DagPanel.tsx +391 -0
  74. package/src/stations/oracle/OracleStation.tsx +304 -0
  75. package/src/stations/oracle/SpatialTab.tsx +185 -0
  76. package/src/stations/oracle/StewardPanel.tsx +165 -0
  77. package/src/stations/oracle/oracle.css +467 -0
  78. package/src/stations/oracle/oracleShared.tsx +160 -0
  79. package/src/stations/oracle/oracleTypes.ts +304 -0
  80. package/src/stations/oracle/useOracle.ts +560 -0
  81. package/src/stations/registry.ts +37 -0
  82. package/src/stations/scriptFirst.css +350 -0
  83. package/src/stations/studio/IdentitiesStation.tsx +219 -292
  84. package/src/stations/studio/IdentityCreatePanel.tsx +390 -0
  85. package/src/stations/studio/StudioGenerateTab.tsx +162 -6
  86. package/src/stations/studio/StudioMovieComposer.tsx +763 -318
  87. package/src/stations/studio/{StudioMovieComposer.tsx.bak-sessions-20260812 → StudioMovieComposer.tsx.bak-declutter-20260813} +350 -48
  88. package/src/stations/studio/StudioPlane.tsx +35 -3
  89. package/src/stations/studio/StudioViewer.tsx +116 -14
  90. package/src/stations/studio/coordinationBadge.tsx +206 -0
  91. package/src/stations/studio/identityUpload.ts +66 -0
  92. package/src/stations/studio/promptCard.tsx +362 -59
  93. package/src/stations/studio/useIdentityCreateJob.ts +184 -0
  94. package/src/stations/studio/useIdentityProfiles.ts +66 -0
  95. package/src/stations/useScriptFirstRun.ts +596 -0
  96. package/src/style/app.css +176 -0
  97. package/src/video/LengthRow.tsx +189 -0
  98. package/src/video/LibraryGroups.tsx +81 -0
  99. package/src/video/UnnamedWarn.tsx +146 -0
  100. package/src/video/coordinationReport.ts +266 -0
  101. package/src/video/jobClock.ts +104 -0
  102. package/src/video/spreadAssist.ts +26 -0
  103. package/src/video/useMediaJobs.ts +26 -0
  104. package/src/video/useOperatorHistory.ts +105 -0
  105. package/src/video/useToks.ts +193 -0
  106. package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +0 -228
  107. package/src/stations/GenerateStation.tsx.bak-tester-removal +0 -5170
  108. package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +0 -417
@@ -0,0 +1,467 @@
1
+ /*
2
+ * or-k18 / k114 — the Oracle station.
3
+ *
4
+ * Own file, colocated with the component that imports it (the scriptFirst.css
5
+ * pattern) because app.css is a foreign dirty file today. Everything here is
6
+ * prefixed `vi-oracle-`; the shared primitives (`station-card`, `vi-btn`,
7
+ * `vi-error`, `vi-status`, `vi-knob-hint`) come from app.css and are NOT
8
+ * redefined.
9
+ */
10
+
11
+ .vi-oracle {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: 1.1rem;
15
+ }
16
+
17
+ .vi-oracle-panel {
18
+ border: 1px solid var(--vi-border);
19
+ border-radius: 8px;
20
+ background: var(--vi-surface-raised);
21
+ padding: 0.85rem 1rem 1rem;
22
+ min-width: 0;
23
+ }
24
+
25
+ .vi-oracle-panel > h3 {
26
+ margin: 0 0 0.6rem;
27
+ font-family: var(--vi-mono);
28
+ font-size: 0.82rem;
29
+ letter-spacing: 0.04em;
30
+ text-transform: uppercase;
31
+ color: var(--vi-text-dim);
32
+ display: flex;
33
+ align-items: baseline;
34
+ gap: 0.6rem;
35
+ flex-wrap: wrap;
36
+ }
37
+
38
+ .vi-oracle-panel > h3 .vi-oracle-sub {
39
+ text-transform: none;
40
+ letter-spacing: 0;
41
+ font-size: 0.75rem;
42
+ color: var(--vi-text-dim);
43
+ opacity: 0.85;
44
+ }
45
+
46
+ .vi-oracle-panel > h4 {
47
+ margin: 0.8rem 0 0.4rem;
48
+ font-family: var(--vi-mono);
49
+ font-size: 0.74rem;
50
+ letter-spacing: 0.03em;
51
+ text-transform: uppercase;
52
+ color: var(--vi-text-dim);
53
+ }
54
+
55
+ .vi-oracle-row {
56
+ display: flex;
57
+ gap: 0.6rem;
58
+ flex-wrap: wrap;
59
+ align-items: center;
60
+ }
61
+
62
+ .vi-oracle-row + .vi-oracle-row {
63
+ margin-top: 0.5rem;
64
+ }
65
+
66
+ .vi-oracle-grow {
67
+ flex: 1 1 14rem;
68
+ min-width: 8rem;
69
+ }
70
+
71
+ .vi-oracle-kv {
72
+ display: grid;
73
+ grid-template-columns: minmax(7rem, max-content) 1fr;
74
+ gap: 0.25rem 0.9rem;
75
+ margin: 0;
76
+ font-size: 0.78rem;
77
+ }
78
+
79
+ .vi-oracle-kv dt {
80
+ color: var(--vi-text-dim);
81
+ font-family: var(--vi-mono);
82
+ font-size: 0.7rem;
83
+ }
84
+
85
+ .vi-oracle-kv dd {
86
+ margin: 0;
87
+ word-break: break-word;
88
+ }
89
+
90
+ .vi-oracle-kv code,
91
+ .vi-oracle-table code,
92
+ .vi-oracle-digest {
93
+ font-family: var(--vi-mono);
94
+ font-size: 0.7rem;
95
+ color: var(--vi-text-dim);
96
+ }
97
+
98
+ /* --- tags / severities ---------------------------------------------------- */
99
+
100
+ .vi-oracle-tag {
101
+ display: inline-block;
102
+ font-family: var(--vi-mono);
103
+ font-size: 0.66rem;
104
+ border: 1px solid var(--vi-border);
105
+ border-radius: 999px;
106
+ padding: 0.05rem 0.5rem;
107
+ color: var(--vi-text-dim);
108
+ white-space: nowrap;
109
+ vertical-align: middle;
110
+ }
111
+
112
+ .vi-oracle-tag-info {
113
+ border-color: var(--vi-border);
114
+ }
115
+
116
+ .vi-oracle-tag-warn {
117
+ border-color: #8a7a3a;
118
+ color: #f2d16b;
119
+ }
120
+
121
+ .vi-oracle-tag-alarm {
122
+ border-color: #6b4a4a;
123
+ color: #ff8a8a;
124
+ }
125
+
126
+ .vi-oracle-tag-ok {
127
+ border-color: #3f6b4a;
128
+ color: #7fd88f;
129
+ }
130
+
131
+ .vi-oracle-tag-accent {
132
+ border-color: var(--vi-accent);
133
+ color: var(--vi-accent);
134
+ }
135
+
136
+ /* node / run states — same palette as .vi-status-* so a DAG state reads like a
137
+ job phase elsewhere in the console */
138
+ .vi-oracle-state {
139
+ font-family: var(--vi-mono);
140
+ font-size: 0.68rem;
141
+ text-transform: uppercase;
142
+ letter-spacing: 0.05em;
143
+ color: var(--vi-text-dim);
144
+ }
145
+
146
+ .vi-oracle-state-running,
147
+ .vi-oracle-state-leased,
148
+ .vi-oracle-state-pending,
149
+ .vi-oracle-state-awaiting_approval,
150
+ .vi-oracle-state-paused {
151
+ color: var(--vi-accent);
152
+ }
153
+
154
+ .vi-oracle-state-succeeded,
155
+ .vi-oracle-state-completed {
156
+ color: #7fd88f;
157
+ }
158
+
159
+ .vi-oracle-state-failed,
160
+ .vi-oracle-state-rejected,
161
+ .vi-oracle-state-cancelled {
162
+ color: #ff8a8a;
163
+ }
164
+
165
+ /* --- the steward ---------------------------------------------------------- */
166
+
167
+ .vi-oracle-stale {
168
+ border: 1px solid #8a7a3a;
169
+ background: rgba(242, 209, 107, 0.08);
170
+ color: #f2d16b;
171
+ border-radius: 6px;
172
+ padding: 0.45rem 0.7rem;
173
+ font-size: 0.78rem;
174
+ margin: 0.5rem 0;
175
+ }
176
+
177
+ .vi-oracle-findings {
178
+ list-style: none;
179
+ margin: 0.4rem 0 0;
180
+ padding: 0;
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 0.35rem;
184
+ }
185
+
186
+ .vi-oracle-finding {
187
+ border: 1px solid var(--vi-border);
188
+ border-radius: 6px;
189
+ padding: 0.45rem 0.6rem;
190
+ font-size: 0.78rem;
191
+ background: var(--vi-surface);
192
+ }
193
+
194
+ .vi-oracle-finding-alarm {
195
+ border-color: #6b4a4a;
196
+ }
197
+
198
+ .vi-oracle-finding-warn {
199
+ border-color: #8a7a3a;
200
+ }
201
+
202
+ .vi-oracle-finding-head {
203
+ display: flex;
204
+ gap: 0.5rem;
205
+ align-items: baseline;
206
+ flex-wrap: wrap;
207
+ }
208
+
209
+ .vi-oracle-finding-action {
210
+ margin: 0.25rem 0 0;
211
+ color: var(--vi-text-dim);
212
+ font-size: 0.74rem;
213
+ }
214
+
215
+ /* --- tables --------------------------------------------------------------- */
216
+
217
+ .vi-oracle-table {
218
+ width: 100%;
219
+ border-collapse: collapse;
220
+ font-size: 0.76rem;
221
+ }
222
+
223
+ .vi-oracle-table th,
224
+ .vi-oracle-table td {
225
+ text-align: left;
226
+ padding: 0.3rem 0.5rem;
227
+ border-bottom: 1px solid var(--vi-border);
228
+ vertical-align: top;
229
+ word-break: break-word;
230
+ }
231
+
232
+ .vi-oracle-table th {
233
+ font-family: var(--vi-mono);
234
+ font-size: 0.66rem;
235
+ text-transform: uppercase;
236
+ letter-spacing: 0.04em;
237
+ color: var(--vi-text-dim);
238
+ font-weight: normal;
239
+ }
240
+
241
+ .vi-oracle-table tr.is-selected td {
242
+ background: rgba(138, 180, 255, 0.08);
243
+ }
244
+
245
+ .vi-oracle-table tr.is-clickable {
246
+ cursor: pointer;
247
+ }
248
+
249
+ .vi-oracle-table tr.is-clickable:hover td {
250
+ background: rgba(255, 255, 255, 0.03);
251
+ }
252
+
253
+ /* --- the DAG list + the per-node drawer ----------------------------------- */
254
+
255
+ .vi-oracle-split {
256
+ display: grid;
257
+ grid-template-columns: minmax(0, 1fr);
258
+ gap: 1rem;
259
+ }
260
+
261
+ @media (min-width: 62rem) {
262
+ .vi-oracle-split.has-drawer {
263
+ grid-template-columns: minmax(0, 1fr) minmax(20rem, 26rem);
264
+ }
265
+ }
266
+
267
+ .vi-oracle-drawer {
268
+ border: 1px solid var(--vi-accent);
269
+ border-radius: 8px;
270
+ background: var(--vi-surface-raised);
271
+ padding: 0.85rem 1rem 1rem;
272
+ min-width: 0;
273
+ align-self: start;
274
+ position: sticky;
275
+ top: 0.5rem;
276
+ max-height: calc(100dvh - 8rem);
277
+ overflow: auto;
278
+ }
279
+
280
+ .vi-oracle-drawer > h3 {
281
+ margin: 0 0 0.6rem;
282
+ font-family: var(--vi-mono);
283
+ font-size: 0.82rem;
284
+ letter-spacing: 0.04em;
285
+ text-transform: uppercase;
286
+ color: var(--vi-text);
287
+ display: flex;
288
+ align-items: baseline;
289
+ gap: 0.6rem;
290
+ flex-wrap: wrap;
291
+ }
292
+
293
+ .vi-oracle-drawer > h3 .vi-oracle-close {
294
+ margin-left: auto;
295
+ }
296
+
297
+ .vi-oracle-drawer > h4 {
298
+ margin: 0.9rem 0 0.35rem;
299
+ font-family: var(--vi-mono);
300
+ font-size: 0.72rem;
301
+ letter-spacing: 0.03em;
302
+ text-transform: uppercase;
303
+ color: var(--vi-text-dim);
304
+ }
305
+
306
+ .vi-oracle-candidates {
307
+ list-style: none;
308
+ margin: 0;
309
+ padding: 0;
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: 0.3rem;
313
+ }
314
+
315
+ .vi-oracle-candidate {
316
+ border: 1px solid var(--vi-border);
317
+ border-radius: 6px;
318
+ padding: 0.4rem 0.55rem;
319
+ font-size: 0.76rem;
320
+ background: var(--vi-surface);
321
+ }
322
+
323
+ .vi-oracle-candidate.is-selected {
324
+ border-color: var(--vi-accent);
325
+ }
326
+
327
+ .vi-oracle-candidate.is-rejected {
328
+ opacity: 0.8;
329
+ }
330
+
331
+ .vi-oracle-candidate-head {
332
+ display: flex;
333
+ gap: 0.5rem;
334
+ align-items: baseline;
335
+ flex-wrap: wrap;
336
+ }
337
+
338
+ .vi-oracle-candidate-head code {
339
+ font-family: var(--vi-mono);
340
+ font-size: 0.74rem;
341
+ color: var(--vi-text);
342
+ }
343
+
344
+ .vi-oracle-reasons {
345
+ margin: 0.25rem 0 0;
346
+ padding-left: 1rem;
347
+ color: var(--vi-text-dim);
348
+ font-size: 0.72rem;
349
+ }
350
+
351
+ .vi-oracle-steps {
352
+ margin: 0.25rem 0 0;
353
+ padding-left: 1.2rem;
354
+ font-family: var(--vi-mono);
355
+ font-size: 0.68rem;
356
+ color: var(--vi-text-dim);
357
+ }
358
+
359
+ /* --- scorecard ------------------------------------------------------------ */
360
+
361
+ .vi-oracle-meter {
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 0.5rem;
365
+ font-size: 0.74rem;
366
+ }
367
+
368
+ .vi-oracle-meter-track {
369
+ flex: 1 1 6rem;
370
+ height: 0.35rem;
371
+ border-radius: 999px;
372
+ background: var(--vi-border);
373
+ overflow: hidden;
374
+ }
375
+
376
+ .vi-oracle-meter-fill {
377
+ height: 100%;
378
+ background: var(--vi-accent);
379
+ }
380
+
381
+ /* --- raw JSON (provenance) ------------------------------------------------ */
382
+
383
+ .vi-oracle-raw {
384
+ font-family: var(--vi-mono);
385
+ font-size: 0.68rem;
386
+ white-space: pre-wrap;
387
+ word-break: break-word;
388
+ max-height: 18rem;
389
+ overflow: auto;
390
+ background: var(--vi-surface);
391
+ border: 1px solid var(--vi-border);
392
+ border-radius: 6px;
393
+ padding: 0.5rem;
394
+ color: var(--vi-text-dim);
395
+ margin: 0.3rem 0 0;
396
+ }
397
+
398
+ .vi-oracle-raw-toggle {
399
+ font-size: 0.72rem;
400
+ color: var(--vi-text-dim);
401
+ cursor: pointer;
402
+ margin-top: 0.4rem;
403
+ }
404
+
405
+ .vi-oracle-raw-toggle summary {
406
+ font-family: var(--vi-mono);
407
+ }
408
+
409
+ .vi-oracle-note {
410
+ font-size: 0.76rem;
411
+ color: var(--vi-text-dim);
412
+ margin: 0.4rem 0 0;
413
+ }
414
+
415
+ .vi-oracle-gap {
416
+ border: 1px dashed #6b4a4a;
417
+ border-radius: 6px;
418
+ padding: 0.45rem 0.7rem;
419
+ font-size: 0.76rem;
420
+ color: #ff8a8a;
421
+ margin: 0.4rem 0 0;
422
+ }
423
+
424
+ .vi-oracle-limits {
425
+ margin: 0.4rem 0 0;
426
+ padding-left: 1.1rem;
427
+ font-size: 0.76rem;
428
+ color: var(--vi-text-dim);
429
+ }
430
+
431
+ /* --- tabs (DAG / spatial) ------------------------------------------------- */
432
+
433
+ .vi-oracle-tabs {
434
+ display: flex;
435
+ gap: 0.3rem;
436
+ border-bottom: 1px solid var(--vi-border);
437
+ margin-bottom: 0.8rem;
438
+ }
439
+
440
+ .vi-oracle-tab {
441
+ background: none;
442
+ border: 0;
443
+ border-bottom: 2px solid transparent;
444
+ color: var(--vi-text-dim);
445
+ font-family: var(--vi-mono);
446
+ font-size: 0.74rem;
447
+ letter-spacing: 0.03em;
448
+ text-transform: uppercase;
449
+ padding: 0.4rem 0.7rem;
450
+ cursor: pointer;
451
+ }
452
+
453
+ .vi-oracle-tab[aria-selected="true"] {
454
+ color: var(--vi-text);
455
+ border-bottom-color: var(--vi-accent);
456
+ }
457
+
458
+ .vi-oracle-runs {
459
+ display: flex;
460
+ gap: 0.4rem;
461
+ flex-wrap: wrap;
462
+ }
463
+
464
+ .vi-oracle-runs .vi-btn {
465
+ font-family: var(--vi-mono);
466
+ font-size: 0.7rem;
467
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * or-k18 — tiny render helpers shared by the oracle panels. Nothing here
3
+ * fetches; everything takes the wire object and prints it, including the raw
4
+ * JSON behind a <details> so every intermediate object stays inspectable with
5
+ * its provenance.
6
+ */
7
+ import type { ReactNode } from "react";
8
+
9
+ import type { Gap } from "./useOracle";
10
+ import type { Scorecard } from "./oracleTypes";
11
+
12
+ export function pretty(value: unknown): string {
13
+ try {
14
+ return JSON.stringify(value ?? null, null, 2);
15
+ } catch {
16
+ return String(value ?? "");
17
+ }
18
+ }
19
+
20
+ export function short(text: unknown, n = 14): string {
21
+ const s = String(text ?? "");
22
+ if (!s) return "—";
23
+ return s.length > n ? `${s.slice(0, n)}…` : s;
24
+ }
25
+
26
+ /** ISO string → "2026-08-21 14:03:11" in the viewer's zone; unknown → "—". */
27
+ export function when(iso: unknown): string {
28
+ if (typeof iso !== "string" || !iso) return "—";
29
+ const d = new Date(iso);
30
+ if (Number.isNaN(d.getTime())) return iso;
31
+ return d.toLocaleString(undefined, { hour12: false });
32
+ }
33
+
34
+ export function ago(ms: number | null): string {
35
+ if (ms == null) return "—";
36
+ const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
37
+ if (s < 60) return `${s}s ago`;
38
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
39
+ return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m ago`;
40
+ }
41
+
42
+ export function RawJson({ label, value, open = false }: { label: string; value: unknown; open?: boolean }) {
43
+ return (
44
+ <details className="vi-oracle-raw-toggle" open={open}>
45
+ <summary>{label}</summary>
46
+ <pre className="vi-oracle-raw">{pretty(value)}</pre>
47
+ </details>
48
+ );
49
+ }
50
+
51
+ export function GapNote({ gap, children }: { gap: Gap | null; children?: ReactNode }) {
52
+ if (!gap) return null;
53
+ return (
54
+ <p className="vi-oracle-gap" role="status">
55
+ {gap.message}
56
+ {children}
57
+ </p>
58
+ );
59
+ }
60
+
61
+ export function StateChip({ state }: { state: string }) {
62
+ const safe = state.replace(/[^a-z_]/gi, "_");
63
+ return <span className={`vi-oracle-state vi-oracle-state-${safe}`}>{state || "—"}</span>;
64
+ }
65
+
66
+ export function Tag({ kind, children }: { kind?: "info" | "warn" | "alarm" | "ok" | "accent"; children: ReactNode }) {
67
+ return <span className={`vi-oracle-tag${kind ? ` vi-oracle-tag-${kind}` : ""}`}>{children}</span>;
68
+ }
69
+
70
+ /** Confidence meter + checks + judges + disagreements. Renders nothing made up:
71
+ * a card without `confidence` shows "not reported", not a bar. */
72
+ export function ScorecardView({ card }: { card: Scorecard | null | undefined }) {
73
+ if (!card) return <p className="vi-oracle-note">No scorecard on this node in the manifest.</p>;
74
+ const conf = typeof card.confidence === "number" ? card.confidence : null;
75
+ const disagreements = card.disagreements ?? [];
76
+ return (
77
+ <div>
78
+ <div className="vi-oracle-row">
79
+ <Tag kind={card.hard_pass ? "ok" : "alarm"}>{card.hard_pass ? "hard pass" : "hard fail"}</Tag>
80
+ {card.repair_code ? <Tag kind="warn">repair: {card.repair_code}</Tag> : null}
81
+ </div>
82
+ <div className="vi-oracle-meter" style={{ marginTop: "0.4rem" }}>
83
+ <span>confidence</span>
84
+ {conf == null ? (
85
+ <span className="vi-knob-hint">not reported</span>
86
+ ) : (
87
+ <>
88
+ <div className="vi-oracle-meter-track" aria-hidden>
89
+ <div className="vi-oracle-meter-fill" style={{ width: `${Math.round(conf * 100)}%` }} />
90
+ </div>
91
+ <code>{conf.toFixed(2)}</code>
92
+ </>
93
+ )}
94
+ </div>
95
+ {card.diagnosis ? <p className="vi-oracle-note">diagnosis: {card.diagnosis}</p> : null}
96
+ {card.recommended_repair ? (
97
+ <p className="vi-oracle-note">recommended repair: {card.recommended_repair}</p>
98
+ ) : null}
99
+ <h4 style={{ margin: "0.6rem 0 0.3rem" }} className="vi-oracle-state">
100
+ disagreements ({disagreements.length})
101
+ </h4>
102
+ {disagreements.length ? (
103
+ <ul className="vi-oracle-reasons">
104
+ {disagreements.map((d, i) => (
105
+ <li key={i}>{d}</li>
106
+ ))}
107
+ </ul>
108
+ ) : (
109
+ <p className="vi-oracle-note">none recorded between checks and judges</p>
110
+ )}
111
+ {(card.checks ?? []).length ? (
112
+ <table className="vi-oracle-table" style={{ marginTop: "0.5rem" }}>
113
+ <thead>
114
+ <tr>
115
+ <th>check</th>
116
+ <th>kind</th>
117
+ <th>value</th>
118
+ <th>threshold</th>
119
+ <th>pass</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ {(card.checks ?? []).map((c, i) => (
124
+ <tr key={`${c.name}-${i}`}>
125
+ <td>{c.name}</td>
126
+ <td>
127
+ <code>{c.kind}</code>
128
+ </td>
129
+ <td>
130
+ <code>{short(pretty(c.value), 40)}</code>
131
+ </td>
132
+ <td>
133
+ <code>{short(pretty(c.threshold), 24)}</code>
134
+ </td>
135
+ <td>
136
+ <Tag kind={c.passed ? "ok" : "alarm"}>{c.passed ? "yes" : "no"}</Tag>
137
+ </td>
138
+ </tr>
139
+ ))}
140
+ </tbody>
141
+ </table>
142
+ ) : null}
143
+ {(card.judge_results ?? []).length ? (
144
+ <ul className="vi-oracle-candidates" style={{ marginTop: "0.5rem" }}>
145
+ {(card.judge_results ?? []).map((j, i) => (
146
+ <li key={`${j.judge}-${i}`} className="vi-oracle-candidate">
147
+ <div className="vi-oracle-candidate-head">
148
+ <code>{j.judge}</code>
149
+ <Tag kind={j.verdict === "pass" ? "ok" : "warn"}>{j.verdict}</Tag>
150
+ {typeof j.score === "number" ? <code>score {j.score}</code> : <span className="vi-knob-hint">no score</span>}
151
+ </div>
152
+ {j.rationale ? <p className="vi-oracle-finding-action">{j.rationale}</p> : null}
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ ) : null}
157
+ <RawJson label="scorecard json" value={card} />
158
+ </div>
159
+ );
160
+ }