@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,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" />
|
|
@@ -1029,7 +1029,30 @@ function LibraryPanel({
|
|
|
1029
1029
|
items: LibraryItem[];
|
|
1030
1030
|
onGenerate: boolean;
|
|
1031
1031
|
}) {
|
|
1032
|
-
|
|
1032
|
+
// TYPE SECTIONS (operator 2026-08-13): the library splits into Images and
|
|
1033
|
+
// Videos (plus a rare Other for audio/unknown), each holding its work groups
|
|
1034
|
+
// newest-first — groupLibrary already sorts both levels freshest-first, so
|
|
1035
|
+
// the per-type ordering is "most recent on top" by construction.
|
|
1036
|
+
const sections = useMemo(() => {
|
|
1037
|
+
const imgs = items.filter((it) => it.ref.kind === "image");
|
|
1038
|
+
const vids = items.filter((it) => it.ref.kind === "video");
|
|
1039
|
+
const rest = items.filter((it) => it.ref.kind !== "image" && it.ref.kind !== "video");
|
|
1040
|
+
return [
|
|
1041
|
+
{ id: "images", label: "🖼 Images", count: imgs.length, works: groupLibrary(imgs) },
|
|
1042
|
+
{ id: "videos", label: "🎞 Videos", count: vids.length, works: groupLibrary(vids) },
|
|
1043
|
+
{ id: "other", label: "📎 Other", count: rest.length, works: groupLibrary(rest) },
|
|
1044
|
+
].filter((sec) => sec.count > 0);
|
|
1045
|
+
}, [items]);
|
|
1046
|
+
// Both type sections start OPEN (hiding freshly-landed work behind a closed
|
|
1047
|
+
// header is how "it never populates" reports happen); the caret remembers.
|
|
1048
|
+
const [closedTypes, setClosedTypes] = useState<Set<string>>(() => new Set());
|
|
1049
|
+
const toggleType = (id: string) =>
|
|
1050
|
+
setClosedTypes((prev) => {
|
|
1051
|
+
const next = new Set(prev);
|
|
1052
|
+
if (next.has(id)) next.delete(id);
|
|
1053
|
+
else next.add(id);
|
|
1054
|
+
return next;
|
|
1055
|
+
});
|
|
1033
1056
|
// Expand state keyed by work key — the established discipline (a Set that
|
|
1034
1057
|
// survives the library's refresh cadence: the works array is rebuilt on every
|
|
1035
1058
|
// change, but the keys are stable, so an expanded work stays open across a
|
|
@@ -1052,17 +1075,42 @@ function LibraryPanel({
|
|
|
1052
1075
|
}
|
|
1053
1076
|
|
|
1054
1077
|
return (
|
|
1055
|
-
|
|
1056
|
-
{
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1078
|
+
<>
|
|
1079
|
+
{sections.map((sec) => {
|
|
1080
|
+
const isOpen = !closedTypes.has(sec.id);
|
|
1081
|
+
return (
|
|
1082
|
+
<div key={sec.id} className="vi-lib-typegroup">
|
|
1083
|
+
<button
|
|
1084
|
+
type="button"
|
|
1085
|
+
className="vi-lib-group-toggle vi-lib-typegroup-head"
|
|
1086
|
+
aria-expanded={isOpen}
|
|
1087
|
+
onClick={() => toggleType(sec.id)}
|
|
1088
|
+
>
|
|
1089
|
+
<span className="vi-lib-group-caret" aria-hidden>
|
|
1090
|
+
{isOpen ? "▾" : "▸"}
|
|
1091
|
+
</span>{" "}
|
|
1092
|
+
{sec.label} <span className="vi-proc-count">{sec.count}</span>
|
|
1093
|
+
</button>
|
|
1094
|
+
{isOpen && (
|
|
1095
|
+
<ul className="vi-lib-list">
|
|
1096
|
+
{sec.works.map((w) => (
|
|
1097
|
+
<WorkSection
|
|
1098
|
+
key={`${sec.id}:${w.key}`}
|
|
1099
|
+
group={w}
|
|
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}`)}
|
|
1105
|
+
onGenerate={onGenerate}
|
|
1106
|
+
/>
|
|
1107
|
+
))}
|
|
1108
|
+
</ul>
|
|
1109
|
+
)}
|
|
1110
|
+
</div>
|
|
1111
|
+
);
|
|
1112
|
+
})}
|
|
1113
|
+
</>
|
|
1066
1114
|
);
|
|
1067
1115
|
}
|
|
1068
1116
|
|