@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.
- 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 +1031 -364
- 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 +60 -12
- 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 +162 -6
- package/src/stations/studio/StudioMovieComposer.tsx +763 -318
- package/src/stations/studio/{StudioMovieComposer.tsx.bak-sessions-20260812 → StudioMovieComposer.tsx.bak-declutter-20260813} +350 -48
- 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 +176 -0
- package/src/video/LengthRow.tsx +189 -0
- package/src/video/LibraryGroups.tsx +81 -0
- package/src/video/UnnamedWarn.tsx +146 -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 +105 -0
- package/src/video/useToks.ts +193 -0
- package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +0 -228
- package/src/stations/GenerateStation.tsx.bak-tester-removal +0 -5170
- package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +0 -417
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// LIVE tok/s telemetry for the generate workbench's Toks panel. Polls TWO
|
|
2
|
+
// same-origin `/api` endpoints together every 2s, MOUNT-GATED exactly like
|
|
3
|
+
// `useMediaJobs` (mount → immediate fetch + 2s interval; unmount → interval
|
|
4
|
+
// cleared and a mounted-ref flipped so a late response never setStates). A fetch
|
|
5
|
+
// error keeps the last-good data rather than flashing empty, and every field is
|
|
6
|
+
// guarded — the server side is being built in parallel, so missing/extra fields
|
|
7
|
+
// must never throw.
|
|
8
|
+
//
|
|
9
|
+
// • GET /api/llm/toks/recent?limit=100 → { entries:[{ ts, worker_id,
|
|
10
|
+
// worker_name, model_key, tok_s, ttft_s, completion_tokens, config_key, ok }] }
|
|
11
|
+
// newest-first — the live tail + the per-worker strip.
|
|
12
|
+
// • GET /api/llm/toks/report → { groups:[{ worker_name, model_key, config_key,
|
|
13
|
+
// n, mean_tok_s, p50_tok_s, p95_tok_s }] } sorted best mean first — the
|
|
14
|
+
// best-config leaderboard.
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
16
|
+
import { hugpyConfig } from "../config";
|
|
17
|
+
import { request, okValue } from "../transport/client";
|
|
18
|
+
|
|
19
|
+
const POLL_MS = 2000;
|
|
20
|
+
const RECENT_LIMIT = 100;
|
|
21
|
+
|
|
22
|
+
/** One recent inference sample (omit-when-unset tolerated on the wire). */
|
|
23
|
+
export interface TokEntry {
|
|
24
|
+
ts: number | null; // epoch seconds when the sample was recorded
|
|
25
|
+
workerId: string | null;
|
|
26
|
+
workerName: string | null;
|
|
27
|
+
modelKey: string | null;
|
|
28
|
+
tokS: number | null; // tokens/second
|
|
29
|
+
ttftS: number | null; // time-to-first-token, seconds
|
|
30
|
+
completionTokens: number | null;
|
|
31
|
+
configKey: string | null;
|
|
32
|
+
ok: boolean | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One best-config rollup row from /report. */
|
|
36
|
+
export interface TokGroup {
|
|
37
|
+
workerName: string | null;
|
|
38
|
+
modelKey: string | null;
|
|
39
|
+
configKey: string | null;
|
|
40
|
+
n: number | null;
|
|
41
|
+
meanTokS: number | null;
|
|
42
|
+
p50TokS: number | null;
|
|
43
|
+
p95TokS: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function str(v: unknown): string | null {
|
|
47
|
+
return typeof v === "string" && v.trim() ? v : null;
|
|
48
|
+
}
|
|
49
|
+
function num(v: unknown): number | null {
|
|
50
|
+
return typeof v === "number" && isFinite(v) ? v : null;
|
|
51
|
+
}
|
|
52
|
+
function bool(v: unknown): boolean | null {
|
|
53
|
+
return typeof v === "boolean" ? v : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseEntries(raw: unknown): TokEntry[] {
|
|
57
|
+
const entries = (raw as { entries?: unknown } | null)?.entries;
|
|
58
|
+
if (!Array.isArray(entries)) return [];
|
|
59
|
+
const out: TokEntry[] = [];
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (!e || typeof e !== "object") continue;
|
|
62
|
+
const r = e as Record<string, unknown>;
|
|
63
|
+
out.push({
|
|
64
|
+
ts: num(r.ts),
|
|
65
|
+
workerId: str(r.worker_id),
|
|
66
|
+
workerName: str(r.worker_name),
|
|
67
|
+
modelKey: str(r.model_key),
|
|
68
|
+
tokS: num(r.tok_s),
|
|
69
|
+
ttftS: num(r.ttft_s),
|
|
70
|
+
completionTokens: num(r.completion_tokens),
|
|
71
|
+
configKey: str(r.config_key),
|
|
72
|
+
ok: bool(r.ok),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseGroups(raw: unknown): TokGroup[] {
|
|
79
|
+
const groups = (raw as { groups?: unknown } | null)?.groups;
|
|
80
|
+
if (!Array.isArray(groups)) return [];
|
|
81
|
+
const out: TokGroup[] = [];
|
|
82
|
+
for (const g of groups) {
|
|
83
|
+
if (!g || typeof g !== "object") continue;
|
|
84
|
+
const r = g as Record<string, unknown>;
|
|
85
|
+
out.push({
|
|
86
|
+
workerName: str(r.worker_name),
|
|
87
|
+
modelKey: str(r.model_key),
|
|
88
|
+
configKey: str(r.config_key),
|
|
89
|
+
n: num(r.n),
|
|
90
|
+
meanTokS: num(r.mean_tok_s),
|
|
91
|
+
p50TokS: num(r.p50_tok_s),
|
|
92
|
+
p95TokS: num(r.p95_tok_s),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The live tok/s feed: recent samples (newest-first) + the best-config report,
|
|
100
|
+
* refreshed together every 2s while a consumer is mounted. `live` reports whether
|
|
101
|
+
* the last poll cycle succeeded, so the panel can show a live/paused dot.
|
|
102
|
+
*/
|
|
103
|
+
export function useToks(): {
|
|
104
|
+
entries: TokEntry[];
|
|
105
|
+
groups: TokGroup[];
|
|
106
|
+
live: boolean;
|
|
107
|
+
} {
|
|
108
|
+
const [entries, setEntries] = useState<TokEntry[]>([]);
|
|
109
|
+
const [groups, setGroups] = useState<TokGroup[]>([]);
|
|
110
|
+
const [live, setLive] = useState(false);
|
|
111
|
+
const mounted = useRef(true);
|
|
112
|
+
|
|
113
|
+
const load = useCallback(async () => {
|
|
114
|
+
// Both endpoints in parallel; each is independent, so one failing keeps the
|
|
115
|
+
// other's last-good data instead of blanking the whole panel.
|
|
116
|
+
const [recent, report] = await Promise.all([
|
|
117
|
+
request<unknown>(`${hugpyConfig.toksRecentUrl}?limit=${RECENT_LIMIT}`, {
|
|
118
|
+
meta: { specKey: "llm", operation: "llm.toks.recent" },
|
|
119
|
+
}),
|
|
120
|
+
request<unknown>(hugpyConfig.toksReportUrl, {
|
|
121
|
+
meta: { specKey: "llm", operation: "llm.toks.report" },
|
|
122
|
+
}),
|
|
123
|
+
]);
|
|
124
|
+
if (!mounted.current) return;
|
|
125
|
+
if (recent.ok) setEntries(parseEntries(okValue(recent)));
|
|
126
|
+
if (report.ok) setGroups(parseGroups(okValue(report)));
|
|
127
|
+
setLive(recent.ok || report.ok);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
mounted.current = true;
|
|
132
|
+
void load();
|
|
133
|
+
const id = window.setInterval(() => void load(), POLL_MS);
|
|
134
|
+
return () => {
|
|
135
|
+
mounted.current = false;
|
|
136
|
+
window.clearInterval(id);
|
|
137
|
+
};
|
|
138
|
+
}, [load]);
|
|
139
|
+
|
|
140
|
+
return { entries, groups, live };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The poll cadence, exported so the panel header can label it ("every 2s"). */
|
|
144
|
+
export const TOKS_POLL_MS = POLL_MS;
|
|
145
|
+
|
|
146
|
+
/** "12s ago" / "3m ago" from an epoch-seconds ts, "" when unknown. */
|
|
147
|
+
export function tokAgo(ts: number | null, nowMs: number): string {
|
|
148
|
+
if (ts == null) return "";
|
|
149
|
+
const secs = Math.max(0, Math.floor(nowMs / 1000 - ts));
|
|
150
|
+
if (secs < 60) return `${secs}s ago`;
|
|
151
|
+
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
|
152
|
+
return `${Math.floor(secs / 3600)}h ago`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Group recent entries by worker → { last tok/s, avg tok/s over the window }. */
|
|
156
|
+
export interface WorkerRollup {
|
|
157
|
+
workerId: string;
|
|
158
|
+
workerName: string;
|
|
159
|
+
modelKey: string | null;
|
|
160
|
+
lastTokS: number | null;
|
|
161
|
+
avgTokS: number | null;
|
|
162
|
+
n: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function rollupByWorker(entries: TokEntry[]): WorkerRollup[] {
|
|
166
|
+
const byWorker = new Map<string, TokEntry[]>();
|
|
167
|
+
for (const e of entries) {
|
|
168
|
+
const key = e.workerId ?? e.workerName;
|
|
169
|
+
if (!key) continue;
|
|
170
|
+
const list = byWorker.get(key);
|
|
171
|
+
if (list) list.push(e);
|
|
172
|
+
else byWorker.set(key, [e]);
|
|
173
|
+
}
|
|
174
|
+
const out: WorkerRollup[] = [];
|
|
175
|
+
for (const [key, list] of byWorker) {
|
|
176
|
+
// Entries arrive newest-first, so the first tok_s we see is the latest.
|
|
177
|
+
const samples = list.map((e) => e.tokS).filter((v): v is number => v != null);
|
|
178
|
+
const avg =
|
|
179
|
+
samples.length > 0
|
|
180
|
+
? samples.reduce((a, b) => a + b, 0) / samples.length
|
|
181
|
+
: null;
|
|
182
|
+
const first = list[0];
|
|
183
|
+
out.push({
|
|
184
|
+
workerId: key,
|
|
185
|
+
workerName: first?.workerName ?? key,
|
|
186
|
+
modelKey: first?.modelKey ?? null,
|
|
187
|
+
lastTokS: samples.length > 0 ? samples[0] : null,
|
|
188
|
+
avgTokS: avg,
|
|
189
|
+
n: list.length,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
// ACTIVE PROCESSES — the console-wide, placement-aware view of every in-flight
|
|
2
|
-
// media-bus job (studio / movie / identity / generate / crop / extract), fed by
|
|
3
|
-
// GET /video/jobs. Promotes the studio-only StudioActiveProcesses pattern to the
|
|
4
|
-
// whole console and adds the two things that were invisible before:
|
|
5
|
-
//
|
|
6
|
-
// • WHERE a job physically executes — the `placement` line ("ae · cuda:0 ·
|
|
7
|
-
// P-studio"), with an "external" cue when it runs OFF the central box (an ae
|
|
8
|
-
// worker, ComfyUI, or the identity-render service on :9750), plus reserved
|
|
9
|
-
// VRAM when a GPU reservation is held.
|
|
10
|
-
// • The reservation HOLD phase — a job that can't fit the card yet sits as
|
|
11
|
-
// `awaiting_capacity` (with the shortfall reason on hover + the overtaken
|
|
12
|
-
// count), not a fake "running".
|
|
13
|
-
// • WHAT it is doing right now — a moving progress bar (segments done plus the
|
|
14
|
-
// within-segment denoise fraction the runner reports) and the TAIL of the job's
|
|
15
|
-
// stage timeline, so a running render shows its last few events instead of an
|
|
16
|
-
// opaque "running" chip.
|
|
17
|
-
// • WHY it broke — a terminal-failed job renders its failure envelope VERBATIM
|
|
18
|
-
// (code + message + the stage it died in), never a generic "failed". Terminal
|
|
19
|
-
// rows arrive only with the "Show finished" filter on.
|
|
20
|
-
//
|
|
21
|
-
// HONEST rendering only: no fabricated progress bars — a numeric progress/stage
|
|
22
|
-
// shows only when the server sends one. Cancel hits the same /video/jobs/<id>/
|
|
23
|
-
// cancel seam every station uses; a held job cancels exactly like any queued job.
|
|
24
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
25
|
-
import type { StationSpec } from "./types";
|
|
26
|
-
import { request } from "../transport/client";
|
|
27
|
-
import { jobCancelUrl } from "../config";
|
|
28
|
-
import { ProcessTimeline, shortId } from "./studio/studioShared";
|
|
29
|
-
import {
|
|
30
|
-
useMediaJobs,
|
|
31
|
-
placementLine,
|
|
32
|
-
isExternal,
|
|
33
|
-
reservedGib,
|
|
34
|
-
elapsedSince,
|
|
35
|
-
progressLabel,
|
|
36
|
-
latestEvent,
|
|
37
|
-
type MediaJob,
|
|
38
|
-
} from "../video/useMediaJobs";
|
|
39
|
-
|
|
40
|
-
function PhaseChip({ job }: { job: MediaJob }) {
|
|
41
|
-
if (job.phase === "awaiting_capacity") {
|
|
42
|
-
const reason = job.hold?.reason ? JSON.stringify(job.hold.reason) : "";
|
|
43
|
-
const overtaken = job.hold?.overtaken ?? 0;
|
|
44
|
-
return (
|
|
45
|
-
<span
|
|
46
|
-
className="vi-status vi-status-awaiting_capacity"
|
|
47
|
-
title={
|
|
48
|
-
reason
|
|
49
|
-
? `Held — waiting for GPU capacity. Shortfall: ${reason}`
|
|
50
|
-
: "Held — waiting for GPU capacity"
|
|
51
|
-
}
|
|
52
|
-
>
|
|
53
|
-
awaiting capacity{overtaken > 0 ? ` · jumped ${overtaken}×` : ""}
|
|
54
|
-
</span>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return <span className={`vi-status vi-status-${job.phase}`}>{job.phase}</span>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The bar + its honest label. Renders nothing when the server sent no number. */
|
|
61
|
-
function ProgressBar({ job }: { job: MediaJob }) {
|
|
62
|
-
if (job.progress == null) return null;
|
|
63
|
-
const pct = Math.max(0, Math.min(100, job.progress * 100));
|
|
64
|
-
return (
|
|
65
|
-
<div className="vi-job-progress">
|
|
66
|
-
<div className="vi-job-progress-track">
|
|
67
|
-
<div className="vi-job-progress-fill" style={{ width: `${pct}%` }} />
|
|
68
|
-
</div>
|
|
69
|
-
<span className="vi-job-progress-label">{progressLabel(job)}</span>
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// `spec` is optional because this body has TWO mounts: the registry's /active
|
|
75
|
-
// station (which always passes one) and the sidebar's ActivePanel overlay
|
|
76
|
-
// (2026-08-06), which mounts it inside the shared drawer shell and has no spec
|
|
77
|
-
// to hand it. The prop was already unused here, so optional costs nothing and
|
|
78
|
-
// keeps ONE renderer for the feed instead of an overlay-only copy.
|
|
79
|
-
export function ActiveProcessesStation({ spec: _spec }: { spec?: StationSpec } = {}) {
|
|
80
|
-
const [showFinished, setShowFinished] = useState(false);
|
|
81
|
-
const { jobs, refetch } = useMediaJobs(showFinished);
|
|
82
|
-
// Expanded rows + a 1s clock, exactly as the studio tab's sibling panel does —
|
|
83
|
-
// ProcessTimeline needs `nowMs` to age its "…since last movement" line.
|
|
84
|
-
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
85
|
-
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
const id = window.setInterval(() => setNowMs(Date.now()), 1000);
|
|
88
|
-
return () => window.clearInterval(id);
|
|
89
|
-
}, []);
|
|
90
|
-
const toggle = useCallback((id: string) => {
|
|
91
|
-
setExpanded((prev) => {
|
|
92
|
-
const n = new Set(prev);
|
|
93
|
-
if (n.has(id)) n.delete(id);
|
|
94
|
-
else n.add(id);
|
|
95
|
-
return n;
|
|
96
|
-
});
|
|
97
|
-
}, []);
|
|
98
|
-
const [cancelBusy, setCancelBusy] = useState<Set<string>>(new Set());
|
|
99
|
-
const mounted = useRef(true);
|
|
100
|
-
|
|
101
|
-
const onCancel = useCallback(
|
|
102
|
-
async (jobId: string) => {
|
|
103
|
-
setCancelBusy((prev) => new Set(prev).add(jobId));
|
|
104
|
-
try {
|
|
105
|
-
await request<unknown>(jobCancelUrl(jobId), {
|
|
106
|
-
method: "POST",
|
|
107
|
-
meta: { specKey: "media", operation: "media.job.cancel" },
|
|
108
|
-
});
|
|
109
|
-
if (!mounted.current) return;
|
|
110
|
-
refetch();
|
|
111
|
-
} finally {
|
|
112
|
-
if (mounted.current)
|
|
113
|
-
setCancelBusy((prev) => {
|
|
114
|
-
const n = new Set(prev);
|
|
115
|
-
n.delete(jobId);
|
|
116
|
-
return n;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
[refetch],
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<section className="station-card station-wide vi-studio-active">
|
|
125
|
-
<h2>Active Processes</h2>
|
|
126
|
-
<p className="vi-knob-hint">
|
|
127
|
-
Every in-flight media build across the console, and WHERE it physically runs.
|
|
128
|
-
A build marked <strong>external</strong> is executing off this box — on the{" "}
|
|
129
|
-
ae worker, ComfyUI, or the identity-render service.
|
|
130
|
-
</p>
|
|
131
|
-
<label className="vi-knob-hint vi-job-filter">
|
|
132
|
-
<input
|
|
133
|
-
type="checkbox"
|
|
134
|
-
checked={showFinished}
|
|
135
|
-
onChange={(e) => setShowFinished(e.target.checked)}
|
|
136
|
-
/>{" "}
|
|
137
|
-
Show finished — recent done/failed/cancelled builds, with each failure's
|
|
138
|
-
full stage timeline and its exact error.
|
|
139
|
-
</label>
|
|
140
|
-
{jobs.length === 0 ? (
|
|
141
|
-
<p className="vi-knob-hint">
|
|
142
|
-
No active builds — enqueue a render and it appears here with its placement.
|
|
143
|
-
</p>
|
|
144
|
-
) : (
|
|
145
|
-
<ul className="vi-studio-active-list">
|
|
146
|
-
{jobs.map((j) => {
|
|
147
|
-
const line = placementLine(j.placement);
|
|
148
|
-
const gib = reservedGib(j.placement);
|
|
149
|
-
const external = isExternal(j.placement);
|
|
150
|
-
const isOpen = expanded.has(j.id);
|
|
151
|
-
const elided = j.stageLogTotal - j.stageLog.length;
|
|
152
|
-
const event = latestEvent(j);
|
|
153
|
-
return (
|
|
154
|
-
<li key={j.id} className="vi-studio-active-row">
|
|
155
|
-
<div className="vi-studio-active-head">
|
|
156
|
-
<button
|
|
157
|
-
type="button"
|
|
158
|
-
className="vi-timeline-toggle"
|
|
159
|
-
onClick={() => toggle(j.id)}
|
|
160
|
-
aria-expanded={isOpen}
|
|
161
|
-
title={isOpen ? "Hide detail" : "Show what it's doing"}
|
|
162
|
-
>
|
|
163
|
-
<span className="vi-timeline-caret" aria-hidden>
|
|
164
|
-
{isOpen ? "▾" : "▸"}
|
|
165
|
-
</span>
|
|
166
|
-
</button>
|
|
167
|
-
<PhaseChip job={j} />
|
|
168
|
-
<span className="vi-studio-active-id">
|
|
169
|
-
{j.name || "job"} · {shortId(j.id)}
|
|
170
|
-
</span>
|
|
171
|
-
</div>
|
|
172
|
-
<div className="vi-studio-active-meta">
|
|
173
|
-
{line ? (
|
|
174
|
-
<span
|
|
175
|
-
className={`vi-placement${external ? " vi-placement-external" : ""}`}
|
|
176
|
-
title={external ? "Runs off the central box" : undefined}
|
|
177
|
-
>
|
|
178
|
-
{external ? "⇄ " : ""}
|
|
179
|
-
{line}
|
|
180
|
-
{external ? " · external" : ""}
|
|
181
|
-
</span>
|
|
182
|
-
) : null}
|
|
183
|
-
{gib ? <span>· {gib}</span> : null}
|
|
184
|
-
{j.stage ? <span>· {j.stage}</span> : null}
|
|
185
|
-
<span>· {elapsedSince(j.createdAt)}</span>
|
|
186
|
-
</div>
|
|
187
|
-
<ProgressBar job={j} />
|
|
188
|
-
{event ? <p className="vi-job-latest">{event}</p> : null}
|
|
189
|
-
{isOpen ? (
|
|
190
|
-
<div className="vi-timeline-panel">
|
|
191
|
-
{/* The SAME renderer the studio tab uses: the failure envelope
|
|
192
|
-
verbatim (code · stage · message), the live "now" line, and
|
|
193
|
-
every stage the render moved through. A live row carries a
|
|
194
|
-
tail; a terminal one carries the whole sequence. */}
|
|
195
|
-
<ProcessTimeline
|
|
196
|
-
stageLog={j.stageLog}
|
|
197
|
-
failure={j.failure}
|
|
198
|
-
currentStage={j.stage}
|
|
199
|
-
lastMovementTs={j.progressedAt}
|
|
200
|
-
terminal={j.terminal}
|
|
201
|
-
nowMs={nowMs}
|
|
202
|
-
/>
|
|
203
|
-
{elided > 0 ? (
|
|
204
|
-
<p className="vi-knob-hint">
|
|
205
|
-
…{elided} earlier event{elided === 1 ? "" : "s"} — open the
|
|
206
|
-
job for its full history.
|
|
207
|
-
</p>
|
|
208
|
-
) : null}
|
|
209
|
-
</div>
|
|
210
|
-
) : null}
|
|
211
|
-
{j.terminal ? null : (
|
|
212
|
-
<button
|
|
213
|
-
type="button"
|
|
214
|
-
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
215
|
-
onClick={() => void onCancel(j.id)}
|
|
216
|
-
disabled={cancelBusy.has(j.id)}
|
|
217
|
-
>
|
|
218
|
-
{cancelBusy.has(j.id) ? "Cancelling…" : "✕ Cancel"}
|
|
219
|
-
</button>
|
|
220
|
-
)}
|
|
221
|
-
</li>
|
|
222
|
-
);
|
|
223
|
-
})}
|
|
224
|
-
</ul>
|
|
225
|
-
)}
|
|
226
|
-
</section>
|
|
227
|
-
);
|
|
228
|
-
}
|