@hugpy/video-intelligence-ui 0.3.2 → 0.3.4
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/LICENSE +0 -0
- package/README.md +9 -0
- package/dist/assets/hugpy-mark-BXktFcOQ.png +0 -0
- package/dist/assets/index-DMoT8G1q.css +1 -0
- package/dist/assets/index-GeD1mo16.js +4190 -0
- package/dist/index.html +71 -0
- package/entry.tsx +6 -27
- package/index.html +4 -4
- package/package.json +44 -5
- package/src/assets/hugpy-mark.png +0 -0
- package/src/config.ts +11 -27
- package/src/demo/DemoBanner.tsx +0 -0
- package/src/demo/demo.css +0 -0
- package/src/demo/demoFetch.ts +0 -9
- package/src/demo/fixtures.ts +0 -0
- package/src/demo/identityFixture.ts +0 -0
- package/src/demo/index.ts +0 -0
- package/src/demo/mediaBase.ts +0 -0
- package/src/demo/mode.ts +0 -0
- package/src/demo/promptAssist.ts +0 -0
- package/src/demo/sampleData.ts +0 -0
- package/src/demo/sampleSets.ts +0 -0
- package/src/demo/seed.ts +0 -0
- package/src/nav/BrandMark.css +0 -0
- package/src/nav/BrandMark.tsx +0 -0
- package/src/nav/Navbar.css +0 -0
- package/src/nav/Navbar.tsx +8 -3
- package/src/regions/SpatialRegionEditor.tsx +0 -0
- package/src/regions/TemporalRegionEditor.tsx +0 -0
- package/src/regions/types.ts +0 -0
- package/src/session.ts +8 -43
- package/src/share.ts +0 -0
- package/src/stations/ActivePanel.tsx +0 -0
- package/src/stations/ActiveProcessesStation.tsx +0 -0
- package/src/stations/AddFileBar.tsx +0 -0
- package/src/stations/AudioCropStation.tsx +0 -0
- package/src/stations/ComfyPanel.tsx +0 -0
- package/src/stations/ComfyStation.tsx +0 -0
- package/src/stations/CondensedKnobStrip.tsx +0 -0
- package/src/stations/ConsolePanel.tsx +0 -0
- package/src/stations/FrameExtractStation.tsx +0 -0
- package/src/stations/GenIdentityBar.tsx +0 -0
- package/src/stations/GenerateStation.tsx +10 -6
- package/src/stations/GoalComposer.tsx +0 -0
- package/src/stations/ImageCropStation.tsx +0 -0
- package/src/stations/KnobInfo.tsx +0 -0
- package/src/stations/PlaceholderStation.tsx +0 -0
- package/src/stations/ScriptFirstStation.tsx +4 -0
- package/src/stations/SectionTabs.tsx +0 -0
- package/src/stations/SharePanel.tsx +4 -4
- package/src/stations/SidebarRegistry.tsx +0 -0
- package/src/stations/StationShell.tsx +0 -0
- package/src/stations/StudioClipsStation.tsx +0 -0
- package/src/stations/TemplateStepsPanel.tsx +190 -0
- package/src/stations/ToksPanel.tsx +1 -2
- package/src/stations/WorkbenchDrawer.tsx +0 -0
- package/src/stations/WorkbenchSidebar.tsx +8 -261
- package/src/stations/WorkbenchStation.tsx +0 -0
- package/src/stations/comfyEmbed.ts +0 -0
- package/src/stations/drawerShell.tsx +0 -0
- package/src/stations/ops/AudioCropCore.tsx +0 -0
- package/src/stations/ops/CharacterGroupsPanel.tsx +75 -83
- package/src/stations/ops/FrameExtractCore.tsx +0 -0
- package/src/stations/ops/ImageCropCore.tsx +0 -0
- package/src/stations/ops/VideoCropCore.tsx +0 -0
- package/src/stations/ops/useVideoCropJobs.ts +0 -0
- package/src/stations/oracle/DagPanel.tsx +0 -0
- package/src/stations/oracle/OracleStation.tsx +2 -0
- package/src/stations/oracle/PerformanceComposer.tsx +119 -0
- package/src/stations/oracle/SpatialTab.tsx +0 -0
- package/src/stations/oracle/StewardPanel.tsx +0 -0
- package/src/stations/oracle/oracle.css +33 -0
- package/src/stations/oracle/oracleShared.tsx +0 -0
- package/src/stations/oracle/oracleTypes.ts +0 -0
- package/src/stations/oracle/useOracle.ts +0 -0
- package/src/stations/pollCaps.ts +0 -0
- package/src/stations/registry.ts +0 -0
- package/src/stations/scriptFirst.css +23 -0
- package/src/stations/studio/AngleRingEditor.tsx +0 -0
- package/src/stations/studio/IdentitiesStation.tsx +86 -76
- package/src/stations/studio/IdentityCreatePanel.tsx +0 -0
- package/src/stations/studio/IdentityProfileControls.tsx +0 -0
- package/src/stations/studio/StudioActiveProcesses.tsx +0 -0
- package/src/stations/studio/StudioAssistLogPanel.tsx +0 -0
- package/src/stations/studio/StudioGenerateMode.tsx +3 -2
- package/src/stations/studio/StudioGenerateTab.tsx +278 -190
- package/src/stations/studio/StudioItemCard.tsx +0 -0
- package/src/stations/studio/StudioLibraryTab.tsx +0 -0
- package/src/stations/studio/StudioMovieComposer.tsx +321 -46
- package/src/stations/studio/StudioMovieSessions.tsx +12 -19
- package/src/stations/studio/StudioPlane.tsx +28 -5
- package/src/stations/studio/StudioSplicedRow.tsx +0 -0
- package/src/stations/studio/StudioViewer.tsx +6 -11
- package/src/stations/studio/TurntableViewer.tsx +0 -0
- package/src/stations/studio/coordinationBadge.tsx +0 -0
- package/src/stations/studio/identityUpload.ts +0 -0
- package/src/stations/studio/movieTimeline.ts +0 -0
- package/src/stations/studio/promptCard.tsx +0 -0
- package/src/stations/studio/studioItem.ts +0 -0
- package/src/stations/studio/studioShared.tsx +14 -2
- package/src/stations/studio/useIdentityCreateJob.ts +26 -21
- package/src/stations/studio/useIdentityProfiles.ts +0 -0
- package/src/stations/studio/useReconstruction.ts +11 -5
- package/src/stations/types.ts +0 -0
- package/src/stations/useAudioCropJobs.ts +0 -0
- package/src/stations/useAudioExtractJob.ts +0 -0
- package/src/stations/useCropJobs.ts +0 -0
- package/src/stations/useFrameJobs.ts +0 -0
- package/src/stations/useGenerateJob.ts +0 -0
- package/src/stations/useGenerateMovieJob.ts +0 -0
- package/src/stations/useGenerateRowJobs.ts +0 -0
- package/src/stations/useGenerateSceneJob.ts +0 -0
- package/src/stations/useGenerateSceneRowJobs.ts +0 -0
- package/src/stations/useScriptFirstRun.ts +0 -0
- package/src/style/app.css +49 -0
- package/src/transport/client.ts +0 -0
- package/src/transport/telemetry.ts +0 -0
- package/src/video/DropReceptacle.tsx +0 -0
- package/src/video/LengthRow.tsx +0 -0
- package/src/video/LibraryGroups.tsx +0 -0
- package/src/video/PromptAssistButtons.tsx +5 -4
- package/src/video/UnnamedWarn.tsx +0 -0
- package/src/video/composerBridge.ts +0 -0
- package/src/video/contract.ts +0 -0
- package/src/video/coordinationReport.ts +0 -0
- package/src/video/jobClock.ts +0 -0
- package/src/video/jobStream.ts +119 -0
- package/src/video/jobTracker.ts +67 -76
- package/src/video/mediaLibrary.ts +0 -0
- package/src/video/modelsFeed.ts +105 -0
- package/src/video/renderCompat.ts +0 -0
- package/src/video/sessionForm.ts +0 -0
- package/src/video/spreadAssist.ts +0 -0
- package/src/video/studioBridge.ts +0 -0
- package/src/video/studioOracle.ts +48 -0
- package/src/video/useMediaJobs.ts +17 -3
- package/src/video/useModels.ts +12 -53
- package/src/video/useMoviePresets.ts +0 -0
- package/src/video/useMovieSessions.ts +15 -6
- package/src/video/useOperatorHistory.ts +11 -101
- package/src/video/usePresets.ts +0 -0
- package/src/video/useProjects.ts +0 -0
- package/src/video/usePromptAssist.ts +70 -25
- package/src/video/useRenderPresets.ts +0 -0
- package/src/video/useStudioAssistLog.ts +0 -0
- package/src/video/useStudioPresets.ts +0 -0
- package/src/video/useToks.ts +14 -6
- package/src/video/watchVideoJob.ts +43 -0
- package/tsconfig.json +0 -0
- package/vite.config.ts +0 -0
- package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
- package/src/video/useWorkerCalls.ts +0 -165
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template steps — the ordered task outline of a registry task-template
|
|
3
|
+
* (settings NS `task_templates`), rendered so the operator can SEE every step
|
|
4
|
+
* and CHOOSE the model for each one. This is the missing cockpit: the
|
|
5
|
+
* template existed only as API data with no screen behind Generate.
|
|
6
|
+
*
|
|
7
|
+
* Reads: GET /llm/templates (open). Saves one slot at a time via
|
|
8
|
+
* POST /llm/templates/<id>/tasks/<task> {model} (operator-gated) — a 401/403
|
|
9
|
+
* or any refusal is rendered VERBATIM on the row it belongs to; the panel
|
|
10
|
+
* itself never hides. Model options come from the assist discovery endpoint,
|
|
11
|
+
* which already excludes operator-blocked and mistagged models, so a choice
|
|
12
|
+
* made here is a choice the resolver will actually serve.
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useState } from "react";
|
|
15
|
+
|
|
16
|
+
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
17
|
+
import { hugpyConfig } from "../config";
|
|
18
|
+
|
|
19
|
+
type TemplateTask = { name: string; desc: string; model: string | null };
|
|
20
|
+
type TemplateRec = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
active: boolean;
|
|
24
|
+
groups: string[];
|
|
25
|
+
tasks: TemplateTask[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const TEMPLATES_URL = `${hugpyConfig.apiBase}/llm/templates`;
|
|
29
|
+
|
|
30
|
+
export function TemplateStepsPanel({ templateId }: { templateId?: string }) {
|
|
31
|
+
const [templates, setTemplates] = useState<TemplateRec[]>([]);
|
|
32
|
+
const [picked, setPicked] = useState<string | null>(templateId ?? null);
|
|
33
|
+
const [models, setModels] = useState<string[]>([]);
|
|
34
|
+
const [rowBusy, setRowBusy] = useState<string | null>(null);
|
|
35
|
+
const [rowError, setRowError] = useState<Record<string, string>>({});
|
|
36
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
const load = useCallback(async () => {
|
|
39
|
+
const res = await request<unknown>(TEMPLATES_URL, {
|
|
40
|
+
method: "GET",
|
|
41
|
+
timeoutMs: 15_000,
|
|
42
|
+
meta: { operation: "templates.list" },
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
setLoadError(describeAppError(errorOf(res)));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const value = okValue(res) as { templates?: TemplateRec[] } | null;
|
|
49
|
+
const rows = Array.isArray(value?.templates) ? value.templates : [];
|
|
50
|
+
setTemplates(rows);
|
|
51
|
+
setLoadError(null);
|
|
52
|
+
// Keep the caller's pick when it still exists; otherwise prefer the
|
|
53
|
+
// active template, then the first one. Never invent an id.
|
|
54
|
+
setPicked((cur) =>
|
|
55
|
+
cur && rows.some((t) => t.id === cur)
|
|
56
|
+
? cur
|
|
57
|
+
: rows.find((t) => t.active)?.id ?? rows[0]?.id ?? null,
|
|
58
|
+
);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
void load();
|
|
63
|
+
}, [load]);
|
|
64
|
+
|
|
65
|
+
// Offerable generators — same feed the assist picker uses (blocked models
|
|
66
|
+
// are excluded server-side). A discovery failure leaves the dropdowns with
|
|
67
|
+
// just the current value; saving is still possible via "unset".
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
let alive = true;
|
|
70
|
+
(async () => {
|
|
71
|
+
const res = await request<unknown>(hugpyConfig.promptAssistModelsUrl, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
timeoutMs: 15_000,
|
|
74
|
+
meta: { operation: "templates.models" },
|
|
75
|
+
});
|
|
76
|
+
if (!alive || !res.ok) return;
|
|
77
|
+
const value = okValue(res) as { models?: { model: string }[] } | null;
|
|
78
|
+
setModels(
|
|
79
|
+
(Array.isArray(value?.models) ? value.models : []).map((m) => m.model),
|
|
80
|
+
);
|
|
81
|
+
})();
|
|
82
|
+
return () => {
|
|
83
|
+
alive = false;
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const tpl = templates.find((t) => t.id === picked) ?? null;
|
|
88
|
+
|
|
89
|
+
const saveSlot = useCallback(
|
|
90
|
+
async (taskName: string, model: string | null) => {
|
|
91
|
+
if (!tpl) return;
|
|
92
|
+
setRowBusy(taskName);
|
|
93
|
+
const url = `${TEMPLATES_URL}/${encodeURIComponent(
|
|
94
|
+
tpl.id,
|
|
95
|
+
)}/tasks/${encodeURIComponent(taskName)}`;
|
|
96
|
+
const res = await request<unknown>(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify({ model }),
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
timeoutMs: 15_000,
|
|
101
|
+
meta: { operation: "templates.fill" },
|
|
102
|
+
});
|
|
103
|
+
setRowBusy(null);
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
setRowError((e) => ({ ...e, [taskName]: describeAppError(errorOf(res)) }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
setRowError((e) => {
|
|
109
|
+
const { [taskName]: _gone, ...rest } = e;
|
|
110
|
+
return rest;
|
|
111
|
+
});
|
|
112
|
+
void load();
|
|
113
|
+
},
|
|
114
|
+
[tpl, load],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<section className="vi-script-panel">
|
|
119
|
+
<h3>
|
|
120
|
+
Template steps
|
|
121
|
+
<span className="vi-script-sub">
|
|
122
|
+
the ordered plan — every step, and the model it calls
|
|
123
|
+
</span>
|
|
124
|
+
</h3>
|
|
125
|
+
|
|
126
|
+
{loadError && <p className="vi-script-note">{loadError}</p>}
|
|
127
|
+
|
|
128
|
+
<div className="vi-script-row">
|
|
129
|
+
{templates.map((t) => (
|
|
130
|
+
<button
|
|
131
|
+
key={t.id}
|
|
132
|
+
type="button"
|
|
133
|
+
className={`vi-btn vi-btn-sm${t.id === picked ? " vi-btn-on" : ""}`}
|
|
134
|
+
onClick={() => setPicked(t.id)}
|
|
135
|
+
>
|
|
136
|
+
{t.name}
|
|
137
|
+
{t.active ? " · active" : ""}
|
|
138
|
+
</button>
|
|
139
|
+
))}
|
|
140
|
+
{templates.length === 0 && !loadError && (
|
|
141
|
+
<span className="vi-script-note">No templates in the registry.</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{tpl && (
|
|
146
|
+
<>
|
|
147
|
+
<p className="vi-script-note">
|
|
148
|
+
groups (stage order): <code>{tpl.groups.join(" → ") || "—"}</code>
|
|
149
|
+
</p>
|
|
150
|
+
<ol className="vi-template-steps">
|
|
151
|
+
{tpl.tasks.map((task) => (
|
|
152
|
+
<li key={task.name} className="vi-template-step">
|
|
153
|
+
<div className="vi-template-step-head">
|
|
154
|
+
<code>{task.name}</code>
|
|
155
|
+
<select
|
|
156
|
+
className="vi-knob-input"
|
|
157
|
+
value={task.model ?? ""}
|
|
158
|
+
disabled={rowBusy === task.name}
|
|
159
|
+
onChange={(e) =>
|
|
160
|
+
void saveSlot(task.name, e.target.value || null)
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
<option value="">unset — router picks from groups</option>
|
|
164
|
+
{/* Keep a pinned model visible even when discovery
|
|
165
|
+
doesn't offer it right now (worker offline). */}
|
|
166
|
+
{task.model && !models.includes(task.model) && (
|
|
167
|
+
<option value={task.model}>{task.model}</option>
|
|
168
|
+
)}
|
|
169
|
+
{models.map((m) => (
|
|
170
|
+
<option key={m} value={m}>
|
|
171
|
+
{m}
|
|
172
|
+
</option>
|
|
173
|
+
))}
|
|
174
|
+
</select>
|
|
175
|
+
{rowBusy === task.name && (
|
|
176
|
+
<span className="vi-script-note">saving…</span>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
<p className="vi-template-step-desc">{task.desc}</p>
|
|
180
|
+
{rowError[task.name] && (
|
|
181
|
+
<p className="vi-script-note">{rowError[task.name]}</p>
|
|
182
|
+
)}
|
|
183
|
+
</li>
|
|
184
|
+
))}
|
|
185
|
+
</ol>
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</section>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
useToks,
|
|
19
19
|
rollupByWorker,
|
|
20
20
|
tokAgo,
|
|
21
|
-
TOKS_POLL_MS,
|
|
22
21
|
type TokEntry,
|
|
23
22
|
type TokGroup,
|
|
24
23
|
} from "../video/useToks";
|
|
@@ -153,7 +152,7 @@ export function ToksStation() {
|
|
|
153
152
|
className={`vi-assistlog-dot ${live ? "vi-assistlog-dot--live" : "vi-assistlog-dot--disconnected"}`}
|
|
154
153
|
aria-hidden
|
|
155
154
|
/>
|
|
156
|
-
{live ? "live" : "paused"} ·
|
|
155
|
+
{live ? "live" : "paused"} · on activity
|
|
157
156
|
</span>
|
|
158
157
|
</div>
|
|
159
158
|
<p className="vi-knob-hint">
|
|
File without changes
|
|
@@ -42,21 +42,18 @@ import {
|
|
|
42
42
|
cancelJob,
|
|
43
43
|
isJobActive,
|
|
44
44
|
isJobTerminal,
|
|
45
|
-
dismissAllTerminal,
|
|
46
45
|
type TrackedJob,
|
|
47
46
|
type StationId,
|
|
48
47
|
} from "../video/jobTracker";
|
|
49
48
|
import { STILL_RUNNING_MESSAGE } from "./pollCaps";
|
|
50
49
|
import { requestAddPart, requestStage } from "../video/composerBridge";
|
|
51
50
|
import { requestStudioStage } from "../video/studioBridge";
|
|
52
|
-
import { useWorkerCalls, type WorkerCall } from "../video/useWorkerCalls";
|
|
53
51
|
import { useSidebarRegistry } from "./SidebarRegistry";
|
|
54
52
|
import { WorkbenchDrawer } from "./WorkbenchDrawer";
|
|
55
53
|
import { SharePanel } from "./SharePanel";
|
|
56
54
|
import {
|
|
57
55
|
mediaBytesUrl,
|
|
58
56
|
studioClipArchiveUrl,
|
|
59
|
-
llmJobCancelUrl,
|
|
60
57
|
} from "../config";
|
|
61
58
|
import { request, errorOf, describeAppError } from "../transport/client";
|
|
62
59
|
import type { MediaRef } from "../video/contract";
|
|
@@ -73,6 +70,8 @@ const STATION_NAME: Record<StationId, string> = {
|
|
|
73
70
|
frames: "Frames & Models",
|
|
74
71
|
generate: "Generate",
|
|
75
72
|
"generate-tester": "Generate (tester)",
|
|
73
|
+
"generate-scene-parts": "Generate (scene parts)",
|
|
74
|
+
studio: "Studio Clip",
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
// The route a process row links to. It is the station id by default (id === route
|
|
@@ -82,6 +81,7 @@ const STATION_NAME: Record<StationId, string> = {
|
|
|
82
81
|
// default studio sub-tab.
|
|
83
82
|
const STATION_ROUTE: Partial<Record<StationId, string>> = {
|
|
84
83
|
"video-crop": "studio-clips",
|
|
84
|
+
studio: "studio-clips",
|
|
85
85
|
// The tester is a surface INSIDE the Generate station, not a route of its own —
|
|
86
86
|
// without this the default (id === path) would link a tester row to /generate-tester,
|
|
87
87
|
// which resolves to nothing.
|
|
@@ -190,182 +190,10 @@ function fmtAgo(ms: number): string {
|
|
|
190
190
|
return `${m}m ${(total % 60).toString().padStart(2, "0")}s`;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
// max-height with its own scroll, pinned to the BOTTOM (newest line) whenever the
|
|
195
|
-
// tail changes — so a live render's newest output is always in view without the
|
|
196
|
-
// operator chasing it. Empty/absent tail degrades to a quiet "no log yet".
|
|
197
|
-
function WorkerCallLog({ lines }: { lines: string[] }) {
|
|
198
|
-
const ref = useRef<HTMLPreElement | null>(null);
|
|
199
|
-
// Re-pin only when the content actually changes (count + newest line), not on
|
|
200
|
-
// every parent re-render — otherwise a manual scroll-up would be yanked back
|
|
201
|
-
// down every poll tick.
|
|
202
|
-
const key = lines.length ? `${lines.length}:${lines[lines.length - 1]}` : "";
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
const el = ref.current;
|
|
205
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
206
|
-
}, [key]);
|
|
207
|
-
if (lines.length === 0) {
|
|
208
|
-
return <p className="vi-proc-log-empty">No log yet.</p>;
|
|
209
|
-
}
|
|
210
|
-
return (
|
|
211
|
-
<pre className="vi-proc-log" ref={ref} aria-label="Recent log output">
|
|
212
|
-
{lines.join("\n")}
|
|
213
|
-
</pre>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// One in-flight WORKER CALL row (from the fleet-wide /llm/jobs feed) — the calls
|
|
218
|
-
// this browser session did NOT launch (identity reconstruction, another station's
|
|
219
|
-
// render, another transport). Presentational sibling of ProcessRow: no NavLink
|
|
220
|
-
// (these jobs have no station route here), a unified hard-Cancel, and — unlike the
|
|
221
|
-
// old flat row that read green-active even when wedged — an EXPANDABLE panel with a
|
|
222
|
-
// live progress bar, current stage, an honest stalled state, and a scrolling log.
|
|
223
|
-
function WorkerCallRow({
|
|
224
|
-
call,
|
|
225
|
-
now,
|
|
226
|
-
cancelling,
|
|
227
|
-
expanded,
|
|
228
|
-
onToggle,
|
|
229
|
-
onCancel,
|
|
230
|
-
}: {
|
|
231
|
-
call: WorkerCall;
|
|
232
|
-
now: number;
|
|
233
|
-
cancelling: boolean;
|
|
234
|
-
expanded: boolean;
|
|
235
|
-
onToggle: (id: string) => void;
|
|
236
|
-
onCancel: (call: WorkerCall) => void;
|
|
237
|
-
}) {
|
|
238
|
-
const meta = [call.model, call.worker].filter(Boolean).join(" · ");
|
|
239
|
-
// Honest stalled signal: the server said so, OR movement has been silent past the
|
|
240
|
-
// stall window. Recomputed off `now` (the ~1s panel tick), so a live render that
|
|
241
|
-
// freezes flips to stalled on its own without a status change.
|
|
242
|
-
const lastMoveMs = call.updatedAt != null ? Math.max(0, now - call.updatedAt) : null;
|
|
243
|
-
const stalled =
|
|
244
|
-
call.stalled || (lastMoveMs != null && lastMoveMs > WORKER_CALL_STALL_MS);
|
|
245
|
-
const pct = call.progress != null ? Math.round(call.progress * 100) : null;
|
|
246
|
-
// A requested-and-acknowledged cancel also latches the button into "cancelling…"
|
|
247
|
-
// (survives across polls until the row drops), not just the local in-flight click.
|
|
248
|
-
const cancelBusy = cancelling || call.cancelRequested;
|
|
249
|
-
return (
|
|
250
|
-
<li className="vi-proc-item vi-proc-item-worker">
|
|
251
|
-
<div className="vi-proc-top">
|
|
252
|
-
<button
|
|
253
|
-
type="button"
|
|
254
|
-
className="vi-proc-toggle"
|
|
255
|
-
aria-expanded={expanded}
|
|
256
|
-
aria-label={expanded ? "Collapse details" : "Expand — progress, stage, log"}
|
|
257
|
-
onClick={() => onToggle(call.id)}
|
|
258
|
-
>
|
|
259
|
-
<span className="vi-proc-caret" aria-hidden>
|
|
260
|
-
{expanded ? "▾" : "▸"}
|
|
261
|
-
</span>
|
|
262
|
-
</button>
|
|
263
|
-
{stalled ? (
|
|
264
|
-
<span className="vi-proc-stall-dot" aria-hidden />
|
|
265
|
-
) : (
|
|
266
|
-
<span className="vi-proc-spinner" aria-hidden />
|
|
267
|
-
)}
|
|
268
|
-
<span className="vi-proc-station">{humanizeWorkerKind(call.kind)}</span>
|
|
269
|
-
<span className={`vi-status vi-status-${call.status}`}>{call.status}</span>
|
|
270
|
-
{stalled && <span className="vi-proc-stall-badge">stalled</span>}
|
|
271
|
-
{pct != null && <span className="vi-proc-progress">{pct}%</span>}
|
|
272
|
-
<span className="vi-proc-elapsed">{fmtElapsed(now - call.startedAt)}</span>
|
|
273
|
-
</div>
|
|
274
|
-
{meta && <span className="vi-proc-label">{meta}</span>}
|
|
275
|
-
|
|
276
|
-
{expanded && (
|
|
277
|
-
<div className="vi-proc-detail">
|
|
278
|
-
{/* Progress bar: determinate when the feed gives a fraction, otherwise an
|
|
279
|
-
indeterminate "working" sweep — never a fabricated percentage. Goes
|
|
280
|
-
amber when stalled so a frozen bar doesn't read as healthy progress. */}
|
|
281
|
-
<div className="vi-proc-progbar-row">
|
|
282
|
-
<div
|
|
283
|
-
className={`vi-proc-progbar${
|
|
284
|
-
pct == null ? " vi-proc-progbar-indeterminate" : ""
|
|
285
|
-
}${stalled ? " vi-proc-progbar-stalled" : ""}`}
|
|
286
|
-
role="progressbar"
|
|
287
|
-
aria-valuemin={0}
|
|
288
|
-
aria-valuemax={100}
|
|
289
|
-
{...(pct != null ? { "aria-valuenow": pct } : {})}
|
|
290
|
-
>
|
|
291
|
-
<span
|
|
292
|
-
className="vi-proc-progbar-fill"
|
|
293
|
-
style={pct != null ? { width: `${pct}%` } : undefined}
|
|
294
|
-
/>
|
|
295
|
-
</div>
|
|
296
|
-
<span className="vi-proc-stage">
|
|
297
|
-
{call.stage ? call.stage : pct == null ? "working…" : `${pct}%`}
|
|
298
|
-
</span>
|
|
299
|
-
</div>
|
|
300
|
-
|
|
301
|
-
{call.message && <p className="vi-proc-detail-msg">{call.message}</p>}
|
|
302
|
-
|
|
303
|
-
<div className="vi-proc-detail-meta">
|
|
304
|
-
<span
|
|
305
|
-
className={`vi-proc-lastmove${
|
|
306
|
-
stalled ? " vi-proc-lastmove-stalled" : ""
|
|
307
|
-
}`}
|
|
308
|
-
>
|
|
309
|
-
{lastMoveMs != null
|
|
310
|
-
? `last movement ${fmtAgo(lastMoveMs)} ago`
|
|
311
|
-
: "last movement unknown"}
|
|
312
|
-
</span>
|
|
313
|
-
</div>
|
|
314
|
-
|
|
315
|
-
<WorkerCallLog lines={call.logTail} />
|
|
316
|
-
</div>
|
|
317
|
-
)}
|
|
318
|
-
|
|
319
|
-
<div className="vi-proc-msg">
|
|
320
|
-
<button
|
|
321
|
-
type="button"
|
|
322
|
-
className="vi-btn vi-btn-sm vi-proc-btn"
|
|
323
|
-
disabled={cancelBusy}
|
|
324
|
-
onClick={() => onCancel(call)}
|
|
325
|
-
title="Stop this worker call — a step already in flight finishes first"
|
|
326
|
-
>
|
|
327
|
-
{cancelBusy ? "Cancelling…" : "✕ Cancel"}
|
|
328
|
-
</button>
|
|
329
|
-
</div>
|
|
330
|
-
</li>
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// STUDIO SCOPE (operator, 2026-07-31): the studio workbench's Active Processes must
|
|
335
|
-
// list ONLY studio-originated work — NEVER a general fleet model call (a discord chat,
|
|
336
|
-
// a web /v1 completion, another transport's LLM call). Those must not be visible here
|
|
337
|
-
// and must not be cancellable from inside the studio. The fleet-wide /llm/jobs feed
|
|
338
|
-
// carries every transport, so we keep only rows that are RENDER/MEDIA work:
|
|
339
|
-
// * transport === "media" — the media/render bus (the authoritative signal: every
|
|
340
|
-
// job on it is video-arm work — studio clips/movies, scene/image gen, identity
|
|
341
|
-
// reconstruction/mesh/char360, mlt render), OR
|
|
342
|
-
// * a known render/media job KIND (defensive fallback if a row's transport is blank).
|
|
343
|
-
// A general LLM call (transport web/v1/discord/cli, kind chat/completion/…) is dropped.
|
|
344
|
-
// The console-wide ActiveProcessesStation still shows the fleet superset — this scoping
|
|
345
|
-
// is the studio sidebar's alone.
|
|
346
|
-
const STUDIO_MEDIA_KINDS = new Set([
|
|
347
|
-
"studio_i2v",
|
|
348
|
-
"generate_studio_movie",
|
|
349
|
-
"generate_scene",
|
|
350
|
-
"generate_movie",
|
|
351
|
-
"generate_image",
|
|
352
|
-
"identity_reconstruction",
|
|
353
|
-
"identity_mesh_build",
|
|
354
|
-
"identity_video_extract",
|
|
355
|
-
"mlt_render",
|
|
356
|
-
"crop",
|
|
357
|
-
"frame_extract",
|
|
358
|
-
"audio_extract",
|
|
359
|
-
]);
|
|
360
|
-
|
|
361
|
-
function isStudioOriginated(c: WorkerCall): boolean {
|
|
362
|
-
return c.transport === "media" || STUDIO_MEDIA_KINDS.has(c.kind);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** Non-done tracked jobs, newest first — the Active Processes tab body. */
|
|
193
|
+
/** Only live tracked jobs belong in Active Processes. */
|
|
366
194
|
function activeJobsOf(records: TrackedJob[]): TrackedJob[] {
|
|
367
195
|
return records
|
|
368
|
-
.filter((r) => r
|
|
196
|
+
.filter((r) => !isJobTerminal(r))
|
|
369
197
|
.slice()
|
|
370
198
|
.sort((a, b) => b.enqueuedAt - a.enqueuedAt);
|
|
371
199
|
}
|
|
@@ -378,64 +206,8 @@ function ProcessesPanel() {
|
|
|
378
206
|
const records = useJobTracker();
|
|
379
207
|
const jobs = useMemo(() => activeJobsOf(records), [records]);
|
|
380
208
|
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
// launched, so the session jobTracker can't see it. Merged below, DEDUPED against
|
|
384
|
-
// the tracker: a /llm/jobs row whose id equals a TrackedJob.jobId is dropped (the
|
|
385
|
-
// tracker row is richer — station link, progress, provenance — so it wins).
|
|
386
|
-
const { calls, refetch } = useWorkerCalls();
|
|
387
|
-
const trackerIds = useMemo(() => new Set(jobs.map((j) => j.jobId)), [jobs]);
|
|
388
|
-
const workerCalls = useMemo(
|
|
389
|
-
// Deduped against the session tracker AND scoped to studio-originated (media/
|
|
390
|
-
// render) work only — a general fleet model call never surfaces or cancels here.
|
|
391
|
-
() => calls.filter((c) => !trackerIds.has(c.id) && isStudioOriginated(c)),
|
|
392
|
-
[calls, trackerIds],
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
// Terminal rows (failed / cancelled / expired) linger in this list after the
|
|
396
|
-
// active ones drop off — this drives the opt-in "Clear finished" broom below.
|
|
397
|
-
const hasTerminal = useMemo(() => jobs.some(isJobTerminal), [jobs]);
|
|
398
|
-
|
|
399
|
-
// Per-id busy flag for the worker-call Cancel buttons (immediate feedback; the
|
|
400
|
-
// next poll / the forced refetch drops the row once the server observes it).
|
|
401
|
-
const [cancelling, setCancelling] = useState<Set<string>>(new Set());
|
|
402
|
-
const onCancelWorkerCall = useCallback(
|
|
403
|
-
async (call: WorkerCall) => {
|
|
404
|
-
setCancelling((prev) => new Set(prev).add(call.id));
|
|
405
|
-
// ONE unified hard-cancel for every transport: the feed's own cancel route
|
|
406
|
-
// (POST /llm/jobs/<id>/cancel → {cancelled,status}) fans the stop out to the
|
|
407
|
-
// right bus server-side, so the row no longer has to route media vs chat itself.
|
|
408
|
-
await request<unknown>(llmJobCancelUrl(call.id), {
|
|
409
|
-
method: "POST",
|
|
410
|
-
meta: { specKey: "workers", operation: "worker.call.cancel" },
|
|
411
|
-
});
|
|
412
|
-
refetch();
|
|
413
|
-
setCancelling((prev) => {
|
|
414
|
-
const n = new Set(prev);
|
|
415
|
-
n.delete(call.id);
|
|
416
|
-
return n;
|
|
417
|
-
});
|
|
418
|
-
},
|
|
419
|
-
[refetch],
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
// Per-id expanded state for the worker-call rows (progress / stage / stalled /
|
|
423
|
-
// log). Keyed by job id so a row's panel stays open across poll ticks; a job that
|
|
424
|
-
// finishes and drops from the feed just leaves a stale id in the set (harmless).
|
|
425
|
-
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
426
|
-
const toggleExpanded = useCallback((id: string) => {
|
|
427
|
-
setExpanded((prev) => {
|
|
428
|
-
const n = new Set(prev);
|
|
429
|
-
if (n.has(id)) n.delete(id);
|
|
430
|
-
else n.add(id);
|
|
431
|
-
return n;
|
|
432
|
-
});
|
|
433
|
-
}, []);
|
|
434
|
-
|
|
435
|
-
// Tick ~1s while anything is live so every elapsed clock stays current: tracker
|
|
436
|
-
// actives/capped OR any in-flight worker call (those are always live).
|
|
437
|
-
const ticking =
|
|
438
|
-
jobs.some((r) => isJobActive(r) || r.capped) || workerCalls.length > 0;
|
|
209
|
+
// Tick ~1s while anything is live so every elapsed clock stays current.
|
|
210
|
+
const ticking = jobs.some((r) => isJobActive(r) || r.capped);
|
|
439
211
|
const [now, setNow] = useState(() => Date.now());
|
|
440
212
|
useEffect(() => {
|
|
441
213
|
if (!ticking) {
|
|
@@ -452,7 +224,7 @@ function ProcessesPanel() {
|
|
|
452
224
|
// target is stable; the empty copy is suppressed when a station has portaled rows in.
|
|
453
225
|
const extraHost = <div className="vi-proc-extra" ref={reg.setActiveExtraHost} />;
|
|
454
226
|
|
|
455
|
-
const total = jobs.length
|
|
227
|
+
const total = jobs.length;
|
|
456
228
|
|
|
457
229
|
if (total === 0) {
|
|
458
230
|
return (
|
|
@@ -473,36 +245,11 @@ function ProcessesPanel() {
|
|
|
473
245
|
<div className="vi-proc-header">
|
|
474
246
|
<h2 className="vi-proc-title">Active processes</h2>
|
|
475
247
|
<span className="vi-proc-count">{total}</span>
|
|
476
|
-
{/* Only shown when there's a finished/failed row to clear; forgets the
|
|
477
|
-
LOCAL terminal rows only (never server work), and leaves every
|
|
478
|
-
still-running job polling and visible. */}
|
|
479
|
-
{hasTerminal && (
|
|
480
|
-
<button
|
|
481
|
-
type="button"
|
|
482
|
-
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
483
|
-
onClick={() => dismissAllTerminal()}
|
|
484
|
-
title="Remove finished and failed items from this view — your saved files on disk are kept."
|
|
485
|
-
aria-label="Remove finished and failed jobs from this list — still-running jobs stay, and your saved files on disk are kept."
|
|
486
|
-
>
|
|
487
|
-
Clear finished
|
|
488
|
-
</button>
|
|
489
|
-
)}
|
|
490
248
|
</div>
|
|
491
249
|
<ul className="vi-proc-list">
|
|
492
250
|
{jobs.map((job) => (
|
|
493
251
|
<ProcessRow key={job.jobId} job={job} now={now} />
|
|
494
252
|
))}
|
|
495
|
-
{workerCalls.map((call) => (
|
|
496
|
-
<WorkerCallRow
|
|
497
|
-
key={call.id}
|
|
498
|
-
call={call}
|
|
499
|
-
now={now}
|
|
500
|
-
cancelling={cancelling.has(call.id)}
|
|
501
|
-
expanded={expanded.has(call.id)}
|
|
502
|
-
onToggle={toggleExpanded}
|
|
503
|
-
onCancel={onCancelWorkerCall}
|
|
504
|
-
/>
|
|
505
|
-
))}
|
|
506
253
|
</ul>
|
|
507
254
|
{extraHost}
|
|
508
255
|
</>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|