@hugpy/video-intelligence-ui 0.1.1 → 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.
- package/package.json +1 -3
- package/src/config.ts +36 -0
- package/src/demo/assets/generate/cinema-cavern/movie.mp4 +0 -0
- package/src/demo/assets/generate/cinema-cavern/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/clips/cat-lounge.mp4 +0 -0
- package/src/demo/assets/generate/clips/drone-city.mp4 +0 -0
- package/src/demo/assets/generate/clips/paper-sails.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_02.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_03.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-desert/scene.mp4 +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-vintage/scene.mp4 +0 -0
- package/src/demo/assets/generate2/s0.png +0 -0
- package/src/demo/assets/generate2/s1.png +0 -0
- package/src/demo/assets/generate2/s10.png +0 -0
- package/src/demo/assets/generate2/s11.png +0 -0
- package/src/demo/assets/generate2/s12.png +0 -0
- package/src/demo/assets/generate2/s13.png +0 -0
- package/src/demo/assets/generate2/s14.png +0 -0
- package/src/demo/assets/generate2/s15.png +0 -0
- package/src/demo/assets/generate2/s16.png +0 -0
- package/src/demo/assets/generate2/s17.png +0 -0
- package/src/demo/assets/generate2/s18.png +0 -0
- package/src/demo/assets/generate2/s19.png +0 -0
- package/src/demo/assets/generate2/s2.png +0 -0
- package/src/demo/assets/generate2/s20.png +0 -0
- package/src/demo/assets/generate2/s21.mp4 +0 -0
- package/src/demo/assets/generate2/s22.mp4 +0 -0
- package/src/demo/assets/generate2/s23.mp4 +0 -0
- package/src/demo/assets/generate2/s24.mp4 +0 -0
- package/src/demo/assets/generate2/s25.mp4 +0 -0
- package/src/demo/assets/generate2/s26.mp4 +0 -0
- package/src/demo/assets/generate2/s27.mp4 +0 -0
- package/src/demo/assets/generate2/s28.mp4 +0 -0
- package/src/demo/assets/generate2/s29.mp4 +0 -0
- package/src/demo/assets/generate2/s3.png +0 -0
- package/src/demo/assets/generate2/s30.mp4 +0 -0
- package/src/demo/assets/generate2/s4.png +0 -0
- package/src/demo/assets/generate2/s5.png +0 -0
- package/src/demo/assets/generate2/s6.png +0 -0
- package/src/demo/assets/generate2/s7.png +0 -0
- package/src/demo/assets/generate2/s8.png +0 -0
- package/src/demo/assets/generate2/s9.png +0 -0
- package/src/demo/demoFetch.ts +126 -3
- package/src/demo/fixtures.ts +72 -4
- package/src/demo/promptAssist.ts +25 -0
- package/src/demo/sampleData.ts +960 -0
- package/src/demo/sampleSets.ts +172 -0
- package/src/stations/ActiveProcessesStation.tsx +18 -3
- package/src/stations/GenerateStation.tsx +901 -366
- package/src/stations/GoalComposer.tsx +9 -1
- package/src/stations/ScriptFirstStation.tsx +1081 -0
- package/src/stations/ToksPanel.tsx +188 -0
- package/src/stations/WorkbenchDrawer.tsx +4 -3
- package/src/stations/WorkbenchSidebar.tsx +6 -3
- package/src/stations/oracle/DagPanel.tsx +391 -0
- package/src/stations/oracle/OracleStation.tsx +304 -0
- package/src/stations/oracle/SpatialTab.tsx +185 -0
- package/src/stations/oracle/StewardPanel.tsx +165 -0
- package/src/stations/oracle/oracle.css +467 -0
- package/src/stations/oracle/oracleShared.tsx +160 -0
- package/src/stations/oracle/oracleTypes.ts +304 -0
- package/src/stations/oracle/useOracle.ts +560 -0
- package/src/stations/registry.ts +37 -0
- package/src/stations/scriptFirst.css +350 -0
- package/src/stations/studio/IdentitiesStation.tsx +219 -292
- package/src/stations/studio/IdentityCreatePanel.tsx +390 -0
- package/src/stations/studio/StudioGenerateTab.tsx +10 -17
- package/src/stations/studio/StudioMovieComposer.tsx +296 -59
- package/src/stations/studio/StudioPlane.tsx +35 -3
- package/src/stations/studio/StudioViewer.tsx +116 -14
- package/src/stations/studio/coordinationBadge.tsx +206 -0
- package/src/stations/studio/identityUpload.ts +66 -0
- package/src/stations/studio/promptCard.tsx +362 -59
- package/src/stations/studio/useIdentityCreateJob.ts +184 -0
- package/src/stations/studio/useIdentityProfiles.ts +66 -0
- package/src/stations/useScriptFirstRun.ts +596 -0
- package/src/style/app.css +157 -0
- package/src/video/LengthRow.tsx +189 -0
- package/src/video/coordinationReport.ts +266 -0
- package/src/video/jobClock.ts +104 -0
- package/src/video/spreadAssist.ts +26 -0
- package/src/video/useMediaJobs.ts +26 -0
- package/src/video/useOperatorHistory.ts +5 -0
- package/src/video/useToks.ts +193 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// ToksPanel — the LIVE "tok/s" overview for the generate workbench, contributed
|
|
2
|
+
// as ONE TAB to the unified drawer (drawerShell.tsx → WorkbenchDrawer.tsx), the
|
|
3
|
+
// same window as Console / ComfyUI / Active and framed with the same
|
|
4
|
+
// `.vi-active-panel-body` chrome. It mirrors ActivePanel: a real component whose
|
|
5
|
+
// MOUNT starts the poll, so `keepMounted: false` IS the pause — the 2s cadence
|
|
6
|
+
// runs exactly when the operator is looking at this tab (see ActivePanel's note).
|
|
7
|
+
//
|
|
8
|
+
// Three compact sections, all fed by `useToks` (two same-origin `/api` endpoints
|
|
9
|
+
// polled together every 2s, error-tolerant, mount-gated):
|
|
10
|
+
// 1. Live tail — the last ~25 recent samples, newest on top.
|
|
11
|
+
// 2. Per-worker strip — recent grouped by worker → last tok/s + window avg.
|
|
12
|
+
// 3. Best-config board — /report's top ~10, best mean first.
|
|
13
|
+
// Every field is guarded: the server side is being built in parallel, so a
|
|
14
|
+
// missing number renders as "—", never a crash.
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import type { DrawerTab } from "./drawerShell";
|
|
17
|
+
import {
|
|
18
|
+
useToks,
|
|
19
|
+
rollupByWorker,
|
|
20
|
+
tokAgo,
|
|
21
|
+
TOKS_POLL_MS,
|
|
22
|
+
type TokEntry,
|
|
23
|
+
type TokGroup,
|
|
24
|
+
} from "../video/useToks";
|
|
25
|
+
|
|
26
|
+
const TAIL_LIMIT = 25;
|
|
27
|
+
const BOARD_LIMIT = 10;
|
|
28
|
+
|
|
29
|
+
/** A number to N decimals, or "—" when the server sent nothing honest. */
|
|
30
|
+
function fmt(v: number | null | undefined, digits = 1): string {
|
|
31
|
+
return typeof v === "number" && isFinite(v) ? v.toFixed(digits) : "—";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The ok/err badge for one sample (null ok → no badge, we just don't know). */
|
|
35
|
+
function OkBadge({ ok }: { ok: boolean | null }) {
|
|
36
|
+
if (ok == null) return null;
|
|
37
|
+
return (
|
|
38
|
+
<span
|
|
39
|
+
className={`vi-assistlog-badge ${ok ? "vi-assistlog-badge--ok" : "vi-assistlog-badge--err"}`}
|
|
40
|
+
title={ok ? "completed ok" : "errored"}
|
|
41
|
+
>
|
|
42
|
+
{ok ? "ok" : "err"}
|
|
43
|
+
</span>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function LiveTail({ entries, nowMs }: { entries: TokEntry[]; nowMs: number }) {
|
|
48
|
+
const tail = entries.slice(0, TAIL_LIMIT);
|
|
49
|
+
return (
|
|
50
|
+
<div>
|
|
51
|
+
<h3 className="vi-toks-heading">Live tail</h3>
|
|
52
|
+
{tail.length === 0 ? (
|
|
53
|
+
<p className="vi-knob-hint">No samples yet — run an inference and it lands here.</p>
|
|
54
|
+
) : (
|
|
55
|
+
<ul className="vi-assistlog-list vi-toks-list">
|
|
56
|
+
{tail.map((e, i) => (
|
|
57
|
+
<li
|
|
58
|
+
key={`${e.workerId ?? "?"}-${e.ts ?? i}-${i}`}
|
|
59
|
+
className="vi-assistlog-row vi-toks-row"
|
|
60
|
+
>
|
|
61
|
+
<span className="vi-toks-tok">{fmt(e.tokS)}<span className="vi-knob-hint"> tok/s</span></span>
|
|
62
|
+
<span className="vi-toks-model" title={e.modelKey ?? undefined}>
|
|
63
|
+
{e.modelKey ?? "—"}
|
|
64
|
+
</span>
|
|
65
|
+
<span className="vi-knob-hint vi-toks-worker">{e.workerName ?? e.workerId ?? "—"}</span>
|
|
66
|
+
<span className="vi-knob-hint">ttft {fmt(e.ttftS, 2)}s</span>
|
|
67
|
+
<span className="vi-knob-hint">{e.completionTokens ?? "—"} tok</span>
|
|
68
|
+
{e.configKey ? <span className="vi-assistlog-chip">{e.configKey}</span> : null}
|
|
69
|
+
<OkBadge ok={e.ok} />
|
|
70
|
+
<span className="vi-knob-hint vi-toks-ago">{tokAgo(e.ts, nowMs)}</span>
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function WorkerStrip({ entries }: { entries: TokEntry[] }) {
|
|
80
|
+
const rows = rollupByWorker(entries);
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
<h3 className="vi-toks-heading">Per-worker</h3>
|
|
84
|
+
{rows.length === 0 ? (
|
|
85
|
+
<p className="vi-knob-hint">No workers reporting.</p>
|
|
86
|
+
) : (
|
|
87
|
+
<div className="vi-toks-strip">
|
|
88
|
+
{rows.map((w) => (
|
|
89
|
+
<div key={w.workerId} className="vi-toks-worker-card">
|
|
90
|
+
<div className="vi-toks-worker-name" title={w.modelKey ?? undefined}>
|
|
91
|
+
{w.workerName}
|
|
92
|
+
</div>
|
|
93
|
+
<div className="vi-toks-worker-nums">
|
|
94
|
+
<span className="vi-toks-tok">{fmt(w.lastTokS)}</span>
|
|
95
|
+
<span className="vi-knob-hint"> now · avg {fmt(w.avgTokS)} · n{w.n}</span>
|
|
96
|
+
</div>
|
|
97
|
+
{w.modelKey ? <div className="vi-knob-hint vi-toks-worker-model">{w.modelKey}</div> : null}
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Leaderboard({ groups }: { groups: TokGroup[] }) {
|
|
107
|
+
const top = groups.slice(0, BOARD_LIMIT);
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<h3 className="vi-toks-heading">Best config</h3>
|
|
111
|
+
{top.length === 0 ? (
|
|
112
|
+
<p className="vi-knob-hint">No rollup yet — the report fills in as samples accrue.</p>
|
|
113
|
+
) : (
|
|
114
|
+
<ul className="vi-assistlog-list vi-toks-list">
|
|
115
|
+
{top.map((g, i) => (
|
|
116
|
+
<li
|
|
117
|
+
key={`${g.workerName ?? "?"}-${g.modelKey ?? "?"}-${g.configKey ?? i}`}
|
|
118
|
+
className="vi-assistlog-row vi-toks-row"
|
|
119
|
+
>
|
|
120
|
+
<span className="vi-knob-hint vi-toks-rank">#{i + 1}</span>
|
|
121
|
+
<span className="vi-toks-tok">{fmt(g.meanTokS)}<span className="vi-knob-hint"> mean</span></span>
|
|
122
|
+
<span className="vi-knob-hint">p50 {fmt(g.p50TokS)}</span>
|
|
123
|
+
<span className="vi-knob-hint">p95 {fmt(g.p95TokS)}</span>
|
|
124
|
+
<span className="vi-toks-model">{g.modelKey ?? "—"}</span>
|
|
125
|
+
<span className="vi-knob-hint vi-toks-worker">{g.workerName ?? "—"}</span>
|
|
126
|
+
{g.configKey ? <span className="vi-assistlog-chip">{g.configKey}</span> : null}
|
|
127
|
+
<span className="vi-knob-hint">n{g.n ?? "—"}</span>
|
|
128
|
+
</li>
|
|
129
|
+
))}
|
|
130
|
+
</ul>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The panel body — a station card with the three sections + a live header. */
|
|
137
|
+
export function ToksStation() {
|
|
138
|
+
const { entries, groups, live } = useToks();
|
|
139
|
+
// A 1s clock so the "Ns ago" column ages between the 2s polls, exactly as the
|
|
140
|
+
// Active panel drives its ProcessTimeline.
|
|
141
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const id = window.setInterval(() => setNowMs(Date.now()), 1000);
|
|
144
|
+
return () => window.clearInterval(id);
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<section className="station-card station-wide vi-toks-station">
|
|
149
|
+
<div className="vi-toks-header">
|
|
150
|
+
<h2>tok/s</h2>
|
|
151
|
+
<span className="vi-assistlog-conn vi-toks-cadence">
|
|
152
|
+
<span
|
|
153
|
+
className={`vi-assistlog-dot ${live ? "vi-assistlog-dot--live" : "vi-assistlog-dot--disconnected"}`}
|
|
154
|
+
aria-hidden
|
|
155
|
+
/>
|
|
156
|
+
{live ? "live" : "paused"} · every {Math.round(TOKS_POLL_MS / 1000)}s
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
<p className="vi-knob-hint">
|
|
160
|
+
Live inference throughput across the LLM workers — the newest samples, a
|
|
161
|
+
per-worker last/avg strip, and the best-performing configs by mean tok/s.
|
|
162
|
+
</p>
|
|
163
|
+
<LiveTail entries={entries} nowMs={nowMs} />
|
|
164
|
+
<WorkerStrip entries={entries} />
|
|
165
|
+
<Leaderboard groups={groups} />
|
|
166
|
+
</section>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The tok/s tab in the unified drawer — mount starts the 2s poll, so
|
|
171
|
+
* `keepMounted: false` is the pause (see ActivePanel's polling note). */
|
|
172
|
+
export function toksDrawerTab(): DrawerTab<"toks"> {
|
|
173
|
+
return {
|
|
174
|
+
id: "toks",
|
|
175
|
+
label: "tok/s",
|
|
176
|
+
hint: "Open the live tok/s overview alongside this station — no tab switch",
|
|
177
|
+
toggleClassName: "vi-toks-toggle",
|
|
178
|
+
toggleLabel: "⚡ tok/s",
|
|
179
|
+
ariaLabel: "Live tokens-per-second overview",
|
|
180
|
+
panelClassName: "vi-toks-panel",
|
|
181
|
+
keepMounted: false, // mount IS the pause — same policy as the Active tab
|
|
182
|
+
body: (
|
|
183
|
+
<div className="vi-active-panel-body">
|
|
184
|
+
<ToksStation />
|
|
185
|
+
</div>
|
|
186
|
+
),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -31,14 +31,15 @@
|
|
|
31
31
|
import { useComfyDrawerTab } from "./ComfyPanel";
|
|
32
32
|
import { consoleDrawerTab } from "./ConsolePanel";
|
|
33
33
|
import { activeDrawerTab } from "./ActivePanel";
|
|
34
|
+
import { toksDrawerTab } from "./ToksPanel";
|
|
34
35
|
import { DrawerShell, useDrawerState, type LegacyDrawerKey } from "./drawerShell";
|
|
35
36
|
|
|
36
|
-
type DrawerTabId = "console" | "comfy" | "active";
|
|
37
|
+
type DrawerTabId = "console" | "comfy" | "active" | "toks";
|
|
37
38
|
|
|
38
39
|
// Strip order = the order the panels arrived in (Console t52, ComfyUI 2026-08-04,
|
|
39
40
|
// Active 2026-08-06), which is also the toolbar's existing left-to-right order —
|
|
40
41
|
// so the tab strip and the deep-link row read the same way round.
|
|
41
|
-
const TAB_IDS = ["console", "comfy", "active"] as const;
|
|
42
|
+
const TAB_IDS = ["console", "comfy", "active", "toks"] as const;
|
|
42
43
|
|
|
43
44
|
// ONE key now holds {open, size, tab}, replacing the three per-panel records.
|
|
44
45
|
const STORAGE_KEY = "vi.workbench.drawer.v1";
|
|
@@ -58,7 +59,7 @@ export function WorkbenchDrawer() {
|
|
|
58
59
|
// descriptors. All three are rebuilt every render — a descriptor is just JSX,
|
|
59
60
|
// and an unselected tab's body element is only MOUNTED if the shell renders it.
|
|
60
61
|
const comfy = useComfyDrawerTab();
|
|
61
|
-
const tabs = [consoleDrawerTab(), comfy, activeDrawerTab()];
|
|
62
|
+
const tabs = [consoleDrawerTab(), comfy, activeDrawerTab(), toksDrawerTab()];
|
|
62
63
|
|
|
63
64
|
return (
|
|
64
65
|
<DrawerShell state={drawer} tabs={tabs} tablistLabel="Overlay panels" />
|
|
@@ -1095,10 +1095,13 @@ function LibraryPanel({
|
|
|
1095
1095
|
<ul className="vi-lib-list">
|
|
1096
1096
|
{sec.works.map((w) => (
|
|
1097
1097
|
<WorkSection
|
|
1098
|
-
key={w.key}
|
|
1098
|
+
key={`${sec.id}:${w.key}`}
|
|
1099
1099
|
group={w}
|
|
1100
|
-
|
|
1101
|
-
|
|
1100
|
+
// Expand state is NAMESPACED by type section: origin-keyed works
|
|
1101
|
+
// (uploads/frames/legacy history) can carry the SAME work key in
|
|
1102
|
+
// both Images and Videos, and a shared key would open both.
|
|
1103
|
+
open={openKeys.has(`${sec.id}:${w.key}`)}
|
|
1104
|
+
onToggle={() => toggle(`${sec.id}:${w.key}`)}
|
|
1102
1105
|
onGenerate={onGenerate}
|
|
1103
1106
|
/>
|
|
1104
1107
|
))}
|
|
@@ -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
|
+
}
|