@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,391 @@
1
+ /**
2
+ * or-k18 — the DAG state list + the per-node drawer.
3
+ *
4
+ * The list is derived ONLY from the run manifest (GET /video/jobs/<id> →
5
+ * result.movie.dag, the VisualResult the performance relay wrote). Per row:
6
+ * node id, state, the artifact it produced (from shots[]), the producer model
7
+ * (from the manifest's receipts when they list the artifact; otherwise the
8
+ * central ledger via GET /api/oracle/producers?ref=), and the lease — which
9
+ * the manifest does NOT export (it lives in NodeRecord in the journal), so
10
+ * that column says so instead of showing a blank that could be mistaken for
11
+ * "no lease".
12
+ *
13
+ * The drawer for a node:
14
+ * • selection decision — POST /api/oracle/selection for the node's
15
+ * capability, candidates by model with every verdict and reason, the
16
+ * ordered step log. This explains what the selector would pick NOW; it is
17
+ * not a replay of the journal's decision, and the drawer says so.
18
+ * • scorecard — confidence, checks, judges, disagreements (from the manifest).
19
+ * • repair plan — the RepairPlan recorded for this node, when one was.
20
+ */
21
+ import { useEffect, useMemo, useState } from "react";
22
+
23
+ import type { DagNodeRow, RunManifest } from "./oracleTypes";
24
+ import {
25
+ capabilityForNode,
26
+ dagRows,
27
+ useProducers,
28
+ useSelectionExplain,
29
+ type ProducerState,
30
+ type RunManifestApi,
31
+ } from "./useOracle";
32
+ import { GapNote, RawJson, ScorecardView, StateChip, Tag, short, when } from "./oracleShared";
33
+
34
+ function ProducerCell({ row, state }: { row: DagNodeRow; state: ProducerState | null }) {
35
+ if (row.producerModel) {
36
+ return (
37
+ <>
38
+ <code>{row.producerModel}</code> <span className="vi-knob-hint">(receipt)</span>
39
+ </>
40
+ );
41
+ }
42
+ if (!row.producedRef) {
43
+ return <span className="vi-knob-hint">{capabilityForNode(row.kind) ? "no artifact recorded" : "model-free node"}</span>;
44
+ }
45
+ if (!state || state.status === "loading") return <span className="vi-knob-hint">asking ledger…</span>;
46
+ if (state.status === "hit") {
47
+ return (
48
+ <>
49
+ <code>{state.producer.model_id}</code>{" "}
50
+ <span className="vi-knob-hint">(ledger{state.producer.worker ? ` · ${state.producer.worker}` : ""})</span>
51
+ </>
52
+ );
53
+ }
54
+ if (state.status === "miss") return <span className="vi-knob-hint">ledger has no producer row for this ref</span>;
55
+ return (
56
+ <span className="vi-knob-hint" title={state.gap.message}>
57
+ unknown — {state.gap.code === "not_found" ? "producers route absent" : state.gap.code}
58
+ </span>
59
+ );
60
+ }
61
+
62
+ function NodeDrawer({
63
+ row,
64
+ manifest,
65
+ producer,
66
+ onClose,
67
+ }: {
68
+ row: DagNodeRow;
69
+ manifest: RunManifest;
70
+ producer: ProducerState | null;
71
+ onClose: () => void;
72
+ }) {
73
+ const capability = capabilityForNode(row.kind);
74
+ const sel = useSelectionExplain();
75
+ const [quality, setQuality] = useState<"preview" | "balanced" | "best">("balanced");
76
+
77
+ useEffect(() => {
78
+ if (capability) sel.explain({ capability, quality, candidates: 3 });
79
+ else sel.clear();
80
+ // sel.explain/clear are stable (useCallback with [] deps)
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [capability, quality, row.nodeId]);
83
+
84
+ const decision = sel.decision;
85
+ const dagRun = manifest.dag?.run;
86
+ const producerModel =
87
+ row.producerModel ?? (producer && producer.status === "hit" ? producer.producer.model_id : null);
88
+
89
+ return (
90
+ <aside className="vi-oracle-drawer" aria-label={`Node ${row.nodeId}`}>
91
+ <h3>
92
+ <code>{row.nodeId}</code>
93
+ <StateChip state={row.state} />
94
+ <button type="button" className="vi-btn vi-btn-sm vi-oracle-close" onClick={onClose}>
95
+ Close
96
+ </button>
97
+ </h3>
98
+ <dl className="vi-oracle-kv">
99
+ <dt>kind</dt>
100
+ <dd>{row.kind}</dd>
101
+ <dt>segment</dt>
102
+ <dd>{row.segmentId ?? "—"}</dd>
103
+ <dt>capability</dt>
104
+ <dd>{capability ?? <span className="vi-knob-hint">model-free (no selector call)</span>}</dd>
105
+ <dt>produced</dt>
106
+ <dd>{row.producedRef ? <code title={row.producedRef}>{short(row.producedRef, 48)}</code> : "—"}</dd>
107
+ <dt>producer</dt>
108
+ <dd>
109
+ <ProducerCell row={row} state={producer} />
110
+ </dd>
111
+ <dt>lease</dt>
112
+ <dd>
113
+ <span className="vi-knob-hint">not in manifest (journal-only: NodeRecord.lease_owner / lease_expires_at)</span>
114
+ </dd>
115
+ <dt>run revision</dt>
116
+ <dd>
117
+ {dagRun ? (
118
+ <>
119
+ {dagRun.revision} · repairs used {dagRun.revisions_used}/{dagRun.repair_budget}
120
+ </>
121
+ ) : (
122
+ "—"
123
+ )}
124
+ </dd>
125
+ </dl>
126
+
127
+ <h4>selection decision</h4>
128
+ {capability ? (
129
+ <>
130
+ <div className="vi-oracle-row">
131
+ <label className="vi-knob-hint">
132
+ quality{" "}
133
+ <select value={quality} onChange={(e) => setQuality(e.target.value as typeof quality)} disabled={sel.busy}>
134
+ <option value="preview">preview</option>
135
+ <option value="balanced">balanced</option>
136
+ <option value="best">best</option>
137
+ </select>
138
+ </label>
139
+ {sel.busy ? <span className="vi-knob-hint">asking the selector…</span> : null}
140
+ </div>
141
+ <p className="vi-oracle-note">
142
+ What the selector would choose for <code>{capability}</code> NOW (POST /api/oracle/selection). It is
143
+ not a replay of this node's journalled decision; the model that actually produced the artifact is the
144
+ producer above{producerModel ? <> (<code>{producerModel}</code>)</> : null}.
145
+ </p>
146
+ <GapNote gap={sel.gap} />
147
+ {decision ? (
148
+ <>
149
+ <div className="vi-oracle-row">
150
+ <Tag kind={decision.gap ? "alarm" : "ok"}>{decision.gap ? "gap — no eligible model" : "decided"}</Tag>
151
+ <Tag>op: {decision.operation}</Tag>
152
+ {decision.explored ? <Tag kind="warn">explored (not the top-ranked)</Tag> : null}
153
+ {decision.spread ? <Tag>spread across candidates</Tag> : null}
154
+ {producerModel && decision.model_id && producerModel !== decision.model_id ? (
155
+ <Tag kind="warn">differs from producer</Tag>
156
+ ) : null}
157
+ </div>
158
+ <p className="vi-oracle-note">
159
+ pick: <code>{decision.model_id ?? "none"}</code>
160
+ {typeof decision.score === "number" ? <> · score {decision.score.toFixed(3)}</> : null}
161
+ {decision.fallback ? <> · fallback <code>{decision.fallback}</code></> : null}
162
+ </p>
163
+ <p className="vi-oracle-note">{decision.rationale}</p>
164
+ <ul className="vi-oracle-candidates">
165
+ {decision.ranked.map((c) => (
166
+ <li key={`r-${c.model_id}`} className={`vi-oracle-candidate${c.selected ? " is-selected" : ""}`}>
167
+ <div className="vi-oracle-candidate-head">
168
+ <code>{c.model_id}</code>
169
+ <Tag kind={c.selected ? "accent" : undefined}>{c.selected ? "selected" : "eligible"}</Tag>
170
+ <code>{c.score.toFixed(3)}</code>
171
+ </div>
172
+ {c.reasons.length ? (
173
+ <ul className="vi-oracle-reasons">
174
+ {c.reasons.map((r, i) => (
175
+ <li key={i}>{r}</li>
176
+ ))}
177
+ </ul>
178
+ ) : null}
179
+ {Object.keys(c.evidence ?? {}).length ? <RawJson label="evidence" value={c.evidence} /> : null}
180
+ </li>
181
+ ))}
182
+ {decision.rejected.map((c) => (
183
+ <li key={`x-${c.model_id}`} className="vi-oracle-candidate is-rejected">
184
+ <div className="vi-oracle-candidate-head">
185
+ <code>{c.model_id}</code>
186
+ <Tag kind="alarm">rejected{c.rejected_at ? ` at ${c.rejected_at}` : ""}</Tag>
187
+ </div>
188
+ {c.reasons.length ? (
189
+ <ul className="vi-oracle-reasons">
190
+ {c.reasons.map((r, i) => (
191
+ <li key={i}>{r}</li>
192
+ ))}
193
+ </ul>
194
+ ) : null}
195
+ </li>
196
+ ))}
197
+ </ul>
198
+ {decision.steps.length ? (
199
+ <ol className="vi-oracle-steps">
200
+ {decision.steps.map((s, i) => (
201
+ <li key={i}>{s}</li>
202
+ ))}
203
+ </ol>
204
+ ) : null}
205
+ <RawJson label="selection json" value={decision} />
206
+ </>
207
+ ) : null}
208
+ </>
209
+ ) : (
210
+ <p className="vi-oracle-note">This node kind runs without a model; there is no selection to explain.</p>
211
+ )}
212
+
213
+ <h4>scorecard</h4>
214
+ <ScorecardView card={row.scorecard} />
215
+
216
+ <h4>repair plan</h4>
217
+ {row.repair ? (
218
+ <>
219
+ <div className="vi-oracle-row">
220
+ <Tag kind={row.repair.repairable ? "warn" : "alarm"}>{row.repair.repairable ? "repairable" : "not repairable"}</Tag>
221
+ <Tag>code: {row.repair.code}</Tag>
222
+ <Tag>budget left: {row.repair.budget_left}</Tag>
223
+ </div>
224
+ <dl className="vi-oracle-kv" style={{ marginTop: "0.4rem" }}>
225
+ <dt>strategy</dt>
226
+ <dd>{row.repair.strategy}</dd>
227
+ <dt>root</dt>
228
+ <dd>{row.repair.root ?? "—"}</dd>
229
+ <dt>path</dt>
230
+ <dd>
231
+ <code>{row.repair.path.join(" → ") || "—"}</code>
232
+ </dd>
233
+ <dt>param changes</dt>
234
+ <dd>
235
+ <code>{Object.keys(row.repair.param_changes ?? {}).length ? JSON.stringify(row.repair.param_changes) : "none"}</code>
236
+ </dd>
237
+ </dl>
238
+ <p className="vi-oracle-note">{row.repair.rationale}</p>
239
+ <RawJson label="repair plan json" value={row.repair} />
240
+ </>
241
+ ) : (
242
+ <p className="vi-oracle-note">
243
+ {row.failed ? "Node failed but no repair plan was recorded for it." : "No repair was diagnosed for this node."}
244
+ </p>
245
+ )}
246
+ </aside>
247
+ );
248
+ }
249
+
250
+ export function DagPanel({ runs }: { runs: RunManifestApi }) {
251
+ const manifest = runs.manifest;
252
+ const rows = useMemo(() => dagRows(manifest), [manifest]);
253
+ const [selected, setSelected] = useState<string | null>(null);
254
+ const producers = useProducers();
255
+
256
+ useEffect(() => {
257
+ setSelected(null);
258
+ }, [runs.jobId]);
259
+
260
+ const dag = manifest?.dag ?? null;
261
+ const selectedRow = rows.find((r) => r.nodeId === selected) ?? null;
262
+
263
+ const counts = useMemo(() => {
264
+ const c: Record<string, number> = {};
265
+ for (const r of rows) c[r.state] = (c[r.state] ?? 0) + 1;
266
+ return c;
267
+ }, [rows]);
268
+
269
+ return (
270
+ <div className={`vi-oracle-split${selectedRow ? " has-drawer" : ""}`}>
271
+ <section className="vi-oracle-panel" aria-label="DAG state">
272
+ <h3>
273
+ DAG
274
+ <span className="vi-oracle-sub">node states from the run manifest (result.movie.dag) — journal truth, not a live poll</span>
275
+ </h3>
276
+ {!manifest ? (
277
+ <p className="vi-oracle-note">Pick a performance run above to read its DAG.</p>
278
+ ) : !dag ? (
279
+ <>
280
+ <p className="vi-oracle-note">
281
+ This run's manifest carries no <code>dag</code> block: the visual stages ran on the linear recipe
282
+ (DAG recipe disabled, or the run stopped at <code>{manifest.stopped_after ?? "a pre-visual stage"}</code>),
283
+ so there are no node states to list. The linear journal's stages are below.
284
+ </p>
285
+ <RawJson label="manifest.stages" value={manifest.stages ?? []} />
286
+ </>
287
+ ) : (
288
+ <>
289
+ <div className="vi-oracle-row">
290
+ <StateChip state={dag.run.state} />
291
+ <Tag kind={dag.ok ? "ok" : "alarm"}>{dag.ok ? "deliverable accepted" : "no accepted deliverable"}</Tag>
292
+ <Tag>
293
+ graph <code>{dag.run.graph_id}</code>
294
+ </Tag>
295
+ <Tag>
296
+ rev {dag.run.revision} · repairs {dag.run.revisions_used}/{dag.run.repair_budget}
297
+ </Tag>
298
+ <Tag>updated {when(dag.run.updated_at)}</Tag>
299
+ {Object.entries(counts).map(([s, n]) => (
300
+ <span key={s}>
301
+ <Tag>
302
+ {n} {s}
303
+ </Tag>
304
+ </span>
305
+ ))}
306
+ </div>
307
+ {dag.run.note ? <p className="vi-oracle-note">note: {dag.run.note}</p> : null}
308
+ <table className="vi-oracle-table" style={{ marginTop: "0.6rem" }}>
309
+ <thead>
310
+ <tr>
311
+ <th>node</th>
312
+ <th>state</th>
313
+ <th>producer model</th>
314
+ <th>lease</th>
315
+ <th>card</th>
316
+ <th>repair</th>
317
+ </tr>
318
+ </thead>
319
+ <tbody>
320
+ {rows.map((r) => (
321
+ <tr
322
+ key={r.nodeId}
323
+ className={`is-clickable${selected === r.nodeId ? " is-selected" : ""}`}
324
+ onClick={() => setSelected(selected === r.nodeId ? null : r.nodeId)}
325
+ tabIndex={0}
326
+ onKeyDown={(e) => {
327
+ if (e.key === "Enter" || e.key === " ") {
328
+ e.preventDefault();
329
+ setSelected(selected === r.nodeId ? null : r.nodeId);
330
+ }
331
+ }}
332
+ aria-selected={selected === r.nodeId}
333
+ >
334
+ <td>
335
+ <code>{r.nodeId}</code>
336
+ </td>
337
+ <td>
338
+ <StateChip state={r.state} />
339
+ </td>
340
+ <td>
341
+ <ProducerCell row={r} state={producers.lookup(r.producedRef)} />
342
+ </td>
343
+ <td>
344
+ <span className="vi-knob-hint" title="NodeRecord.lease_owner is journal-only; the manifest does not export it">
345
+ not exported
346
+ </span>
347
+ </td>
348
+ <td>
349
+ {r.scorecard ? (
350
+ <Tag kind={r.scorecard.hard_pass ? "ok" : "alarm"}>
351
+ {r.scorecard.hard_pass ? "pass" : "fail"}
352
+ {typeof r.scorecard.confidence === "number" ? ` · ${r.scorecard.confidence.toFixed(2)}` : ""}
353
+ </Tag>
354
+ ) : (
355
+ <span className="vi-knob-hint">—</span>
356
+ )}
357
+ </td>
358
+ <td>
359
+ {r.repair ? (
360
+ <Tag kind={r.repair.repairable ? "warn" : "alarm"}>{r.repair.code}</Tag>
361
+ ) : (
362
+ <span className="vi-knob-hint">—</span>
363
+ )}
364
+ </td>
365
+ </tr>
366
+ ))}
367
+ </tbody>
368
+ </table>
369
+ {dag.limitations?.length ? (
370
+ <ul className="vi-oracle-limits">
371
+ {dag.limitations.map((l, i) => (
372
+ <li key={i}>{l}</li>
373
+ ))}
374
+ </ul>
375
+ ) : null}
376
+ {dag.validation ? <RawJson label="graph validation" value={dag.validation} /> : null}
377
+ <RawJson label="manifest.dag json" value={dag} />
378
+ </>
379
+ )}
380
+ </section>
381
+ {selectedRow && manifest ? (
382
+ <NodeDrawer
383
+ row={selectedRow}
384
+ manifest={manifest}
385
+ producer={producers.lookup(selectedRow.producedRef)}
386
+ onClose={() => setSelected(null)}
387
+ />
388
+ ) : null}
389
+ </div>
390
+ );
391
+ }