@hugpy/video-intelligence-ui 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +9 -0
  3. package/dist/assets/hugpy-mark-BXktFcOQ.png +0 -0
  4. package/dist/assets/index-DuT1AMLn.css +1 -0
  5. package/dist/assets/index-_13ZiBDP.js +4191 -0
  6. package/dist/index.html +71 -0
  7. package/entry.tsx +0 -0
  8. package/index.html +4 -4
  9. package/package.json +42 -4
  10. package/src/assets/hugpy-mark.png +0 -0
  11. package/src/config.ts +0 -0
  12. package/src/demo/DemoBanner.tsx +0 -0
  13. package/src/demo/demo.css +0 -0
  14. package/src/demo/demoFetch.ts +0 -0
  15. package/src/demo/fixtures.ts +0 -0
  16. package/src/demo/identityFixture.ts +0 -0
  17. package/src/demo/index.ts +0 -0
  18. package/src/demo/mediaBase.ts +0 -0
  19. package/src/demo/mode.ts +0 -0
  20. package/src/demo/promptAssist.ts +0 -0
  21. package/src/demo/sampleData.ts +0 -0
  22. package/src/demo/sampleSets.ts +0 -0
  23. package/src/demo/seed.ts +0 -0
  24. package/src/nav/BrandMark.css +0 -0
  25. package/src/nav/BrandMark.tsx +0 -0
  26. package/src/nav/Navbar.css +0 -0
  27. package/src/nav/Navbar.tsx +6 -1
  28. package/src/regions/SpatialRegionEditor.tsx +0 -0
  29. package/src/regions/TemporalRegionEditor.tsx +0 -0
  30. package/src/regions/types.ts +0 -0
  31. package/src/session.ts +0 -0
  32. package/src/share.ts +0 -0
  33. package/src/stations/ActivePanel.tsx +0 -0
  34. package/src/stations/ActiveProcessesStation.tsx +0 -0
  35. package/src/stations/AddFileBar.tsx +0 -0
  36. package/src/stations/AudioCropStation.tsx +0 -0
  37. package/src/stations/ComfyPanel.tsx +0 -0
  38. package/src/stations/ComfyStation.tsx +0 -0
  39. package/src/stations/CondensedKnobStrip.tsx +0 -0
  40. package/src/stations/ConsolePanel.tsx +0 -0
  41. package/src/stations/FrameExtractStation.tsx +0 -0
  42. package/src/stations/GenIdentityBar.tsx +0 -0
  43. package/src/stations/GenerateStation.tsx +2 -1
  44. package/src/stations/GoalComposer.tsx +0 -0
  45. package/src/stations/ImageCropStation.tsx +0 -0
  46. package/src/stations/KnobInfo.tsx +0 -0
  47. package/src/stations/PlaceholderStation.tsx +0 -0
  48. package/src/stations/ScriptFirstStation.tsx +4 -0
  49. package/src/stations/SectionTabs.tsx +0 -0
  50. package/src/stations/SharePanel.tsx +0 -0
  51. package/src/stations/SidebarRegistry.tsx +0 -0
  52. package/src/stations/StationShell.tsx +0 -0
  53. package/src/stations/StudioClipsStation.tsx +0 -0
  54. package/src/stations/TemplateStepsPanel.tsx +190 -0
  55. package/src/stations/ToksPanel.tsx +0 -0
  56. package/src/stations/WorkbenchDrawer.tsx +0 -0
  57. package/src/stations/WorkbenchSidebar.tsx +0 -0
  58. package/src/stations/WorkbenchStation.tsx +0 -0
  59. package/src/stations/comfyEmbed.ts +0 -0
  60. package/src/stations/drawerShell.tsx +0 -0
  61. package/src/stations/ops/AudioCropCore.tsx +0 -0
  62. package/src/stations/ops/CharacterGroupsPanel.tsx +0 -0
  63. package/src/stations/ops/FrameExtractCore.tsx +0 -0
  64. package/src/stations/ops/ImageCropCore.tsx +0 -0
  65. package/src/stations/ops/VideoCropCore.tsx +0 -0
  66. package/src/stations/ops/useVideoCropJobs.ts +0 -0
  67. package/src/stations/oracle/DagPanel.tsx +0 -0
  68. package/src/stations/oracle/OracleStation.tsx +2 -0
  69. package/src/stations/oracle/PerformanceComposer.tsx +119 -0
  70. package/src/stations/oracle/SpatialTab.tsx +0 -0
  71. package/src/stations/oracle/StewardPanel.tsx +0 -0
  72. package/src/stations/oracle/oracle.css +33 -0
  73. package/src/stations/oracle/oracleShared.tsx +0 -0
  74. package/src/stations/oracle/oracleTypes.ts +0 -0
  75. package/src/stations/oracle/useOracle.ts +0 -0
  76. package/src/stations/pollCaps.ts +0 -0
  77. package/src/stations/registry.ts +0 -0
  78. package/src/stations/scriptFirst.css +23 -0
  79. package/src/stations/studio/AngleRingEditor.tsx +0 -0
  80. package/src/stations/studio/IdentitiesStation.tsx +21 -2
  81. package/src/stations/studio/IdentityCreatePanel.tsx +0 -0
  82. package/src/stations/studio/IdentityProfileControls.tsx +0 -0
  83. package/src/stations/studio/StudioActiveProcesses.tsx +0 -0
  84. package/src/stations/studio/StudioAssistLogPanel.tsx +0 -0
  85. package/src/stations/studio/StudioGenerateMode.tsx +0 -0
  86. package/src/stations/studio/StudioGenerateTab.tsx +20 -2
  87. package/src/stations/studio/StudioItemCard.tsx +0 -0
  88. package/src/stations/studio/StudioLibraryTab.tsx +0 -0
  89. package/src/stations/studio/StudioMovieComposer.tsx +0 -0
  90. package/src/stations/studio/StudioMovieSessions.tsx +12 -19
  91. package/src/stations/studio/StudioPlane.tsx +25 -4
  92. package/src/stations/studio/StudioSplicedRow.tsx +0 -0
  93. package/src/stations/studio/StudioViewer.tsx +0 -0
  94. package/src/stations/studio/TurntableViewer.tsx +0 -0
  95. package/src/stations/studio/coordinationBadge.tsx +0 -0
  96. package/src/stations/studio/identityUpload.ts +0 -0
  97. package/src/stations/studio/movieTimeline.ts +0 -0
  98. package/src/stations/studio/promptCard.tsx +0 -0
  99. package/src/stations/studio/studioItem.ts +0 -0
  100. package/src/stations/studio/studioShared.tsx +0 -0
  101. package/src/stations/studio/useIdentityCreateJob.ts +0 -0
  102. package/src/stations/studio/useIdentityProfiles.ts +0 -0
  103. package/src/stations/studio/useReconstruction.ts +0 -0
  104. package/src/stations/types.ts +0 -0
  105. package/src/stations/useAudioCropJobs.ts +0 -0
  106. package/src/stations/useAudioExtractJob.ts +0 -0
  107. package/src/stations/useCropJobs.ts +0 -0
  108. package/src/stations/useFrameJobs.ts +0 -0
  109. package/src/stations/useGenerateJob.ts +0 -0
  110. package/src/stations/useGenerateMovieJob.ts +0 -0
  111. package/src/stations/useGenerateRowJobs.ts +0 -0
  112. package/src/stations/useGenerateSceneJob.ts +0 -0
  113. package/src/stations/useGenerateSceneRowJobs.ts +0 -0
  114. package/src/stations/useScriptFirstRun.ts +0 -0
  115. package/src/style/app.css +49 -0
  116. package/src/transport/client.ts +0 -0
  117. package/src/transport/telemetry.ts +0 -0
  118. package/src/video/DropReceptacle.tsx +0 -0
  119. package/src/video/LengthRow.tsx +0 -0
  120. package/src/video/LibraryGroups.tsx +0 -0
  121. package/src/video/PromptAssistButtons.tsx +6 -4
  122. package/src/video/UnnamedWarn.tsx +0 -0
  123. package/src/video/composerBridge.ts +0 -0
  124. package/src/video/contract.ts +0 -0
  125. package/src/video/coordinationReport.ts +0 -0
  126. package/src/video/jobClock.ts +0 -0
  127. package/src/video/jobTracker.ts +0 -0
  128. package/src/video/mediaLibrary.ts +0 -0
  129. package/src/video/renderCompat.ts +0 -0
  130. package/src/video/sessionForm.ts +0 -0
  131. package/src/video/spreadAssist.ts +0 -0
  132. package/src/video/studioBridge.ts +0 -0
  133. package/src/video/useMediaJobs.ts +0 -0
  134. package/src/video/useModels.ts +0 -0
  135. package/src/video/useMoviePresets.ts +0 -0
  136. package/src/video/useMovieSessions.ts +0 -0
  137. package/src/video/useOperatorHistory.ts +0 -0
  138. package/src/video/usePresets.ts +0 -0
  139. package/src/video/useProjects.ts +0 -0
  140. package/src/video/usePromptAssist.ts +65 -23
  141. package/src/video/useRenderPresets.ts +0 -0
  142. package/src/video/useStudioAssistLog.ts +0 -0
  143. package/src/video/useStudioPresets.ts +0 -0
  144. package/src/video/useToks.ts +0 -0
  145. package/src/video/useWorkerCalls.ts +0 -0
  146. package/tsconfig.json +0 -0
  147. package/vite.config.ts +0 -0
  148. package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
@@ -0,0 +1,71 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Hugpy Video Intelligence — Self-Hosted Video Crop, Frames &amp; Generation</title>
7
+ <meta
8
+ name="description"
9
+ content="Hugpy Video Intelligence: a self-hosted console for spatial and temporal media cropping, frame extraction with explicit fps/quality control, and multimodal image generation — all on your own hardware."
10
+ />
11
+ <meta name="theme-color" content="#050505" />
12
+ <link rel="canonical" href="https://hugpy.ai/video/" />
13
+
14
+ <meta property="og:type" content="website" />
15
+ <meta property="og:site_name" content="Hugpy" />
16
+ <meta property="og:title" content="Hugpy Video Intelligence Studio" />
17
+ <meta
18
+ property="og:description"
19
+ content="Self-hosted video AI on hardware you own: scene and clip workflows, frame extraction, character identities, and image generation. Try the studio in demo mode — nothing to install."
20
+ />
21
+ <meta property="og:url" content="https://hugpy.ai/video/" />
22
+ <meta property="og:image" content="https://hugpy.ai/demo-media/og/video-thumb-luigi.png" />
23
+ <meta property="og:image:width" content="2880" />
24
+ <meta property="og:image:height" content="1512" />
25
+ <meta name="twitter:card" content="summary_large_image" />
26
+ <meta name="twitter:title" content="Hugpy Video Intelligence Studio" />
27
+ <meta name="twitter:image" content="https://hugpy.ai/demo-media/og/video-thumb-luigi.png" />
28
+
29
+ <!-- Same type system as the media arm: Newsreader serif + IBM Plex Mono. -->
30
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
31
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
32
+ <link
33
+ href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Newsreader:ital,wght@0,400;0,500;1,400&display=swap"
34
+ rel="stylesheet"
35
+ />
36
+ <style>
37
+ /* Standalone shell paint — served alone at /video, so painting the page
38
+ chrome here cannot bleed into another site. Keeps the dark theme over
39
+ the entire viewport before the bundle loads. */
40
+ html,
41
+ body {
42
+ margin: 0;
43
+ background: #050505;
44
+ overflow-x: hidden;
45
+ }
46
+ #root {
47
+ min-height: 100dvh;
48
+ background: #050505;
49
+ overflow-x: hidden;
50
+ }
51
+ </style>
52
+ <script type="module" crossorigin src="/video/assets/index-_13ZiBDP.js"></script>
53
+ <link rel="stylesheet" crossorigin href="/video/assets/index-DuT1AMLn.css">
54
+ </head>
55
+ <body>
56
+ <div id="root"></div>
57
+ <noscript>
58
+ <main style="color:#ddd;max-width:42rem;margin:2rem auto;padding:0 1rem;font-family:system-ui,sans-serif">
59
+ <h1>Hugpy Video Intelligence</h1>
60
+ <p>
61
+ A self-hosted video-AI console: crop images and audio spatially/temporally,
62
+ extract frames with explicit fps and quality control, and generate images
63
+ from multimodal prompts — all on your own hardware.
64
+ </p>
65
+ <p><a style="color:#8ab4ff" href="/">Hugpy — Inference You Own</a></p>
66
+ </main>
67
+ </noscript>
68
+ <!-- Self-hosters/servers: replace null with your demo-media base URL string. -->
69
+ <script>window.__HUGPY_MEDIA_BASE__ = window.__HUGPY_MEDIA_BASE__ ?? null;</script>
70
+ </body>
71
+ </html>
package/entry.tsx CHANGED
File without changes
package/index.html CHANGED
@@ -19,12 +19,12 @@
19
19
  content="Self-hosted video AI on hardware you own: scene and clip workflows, frame extraction, character identities, and image generation. Try the studio in demo mode — nothing to install."
20
20
  />
21
21
  <meta property="og:url" content="https://hugpy.ai/video/" />
22
- <meta property="og:image" content="https://hugpy.ai/demo-media/og/video-thumb.png" />
23
- <meta property="og:image:width" content="2400" />
24
- <meta property="og:image:height" content="1260" />
22
+ <meta property="og:image" content="https://hugpy.ai/demo-media/og/video-thumb-luigi.png" />
23
+ <meta property="og:image:width" content="2880" />
24
+ <meta property="og:image:height" content="1512" />
25
25
  <meta name="twitter:card" content="summary_large_image" />
26
26
  <meta name="twitter:title" content="Hugpy Video Intelligence Studio" />
27
- <meta name="twitter:image" content="https://hugpy.ai/demo-media/og/video-thumb.png" />
27
+ <meta name="twitter:image" content="https://hugpy.ai/demo-media/og/video-thumb-luigi.png" />
28
28
 
29
29
  <!-- Same type system as the media arm: Newsreader serif + IBM Plex Mono. -->
30
30
  <link rel="preconnect" href="https://fonts.googleapis.com" />
package/package.json CHANGED
@@ -1,9 +1,34 @@
1
1
  {
2
2
  "name": "@hugpy/video-intelligence-ui",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
- "description": "Video-intelligence console \u2014 sibling arm of media_intelligence_ui (Vite + Tailwind v4), served at /video. Four stations (image crop, audio crop, frames/models, generate) over the hugpy job bus. Dials the same-origin current-hugpy API (/api).",
6
+ "description": "Hugpy video-intelligence console: image crop, audio crop, frames/models and generate stations over the hugpy-video job bus; a standalone Vite + Tailwind v4 arm served at /video",
7
+ "keywords": [
8
+ "hugpy",
9
+ "llm",
10
+ "self-hosted",
11
+ "react",
12
+ "video",
13
+ "video-generation",
14
+ "console",
15
+ "hugpy-video",
16
+ "hugpy-oracle"
17
+ ],
18
+ "homepage": "https://hugpy.ai",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/hugpy/hugpy.git",
22
+ "directory": "react/video_intelligence_ui"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/hugpy/hugpy/issues"
26
+ },
27
+ "author": {
28
+ "name": "putkoff",
29
+ "email": "support@hugpy.ai",
30
+ "url": "https://hugpy.ai"
31
+ },
7
32
  "scripts": {
8
33
  "dev": "vite",
9
34
  "build": "vite build",
@@ -25,6 +50,7 @@
25
50
  "vite": "^5.4.21"
26
51
  },
27
52
  "files": [
53
+ "dist",
28
54
  "src",
29
55
  "entry.tsx",
30
56
  "index.html",
@@ -32,8 +58,20 @@
32
58
  "tsconfig.json",
33
59
  "LICENSE"
34
60
  ],
35
- "author": "putkoff (hugpy.ai)",
36
61
  "publishConfig": {
37
62
  "access": "public"
63
+ },
64
+ "hugpy": {
65
+ "genre": "video",
66
+ "python": [
67
+ {
68
+ "name": "hugpy-video",
69
+ "url": "https://pypi.org/project/hugpy-video/"
70
+ },
71
+ {
72
+ "name": "hugpy-oracle",
73
+ "url": "https://pypi.org/project/hugpy-oracle/"
74
+ }
75
+ ]
38
76
  }
39
- }
77
+ }
File without changes
package/src/config.ts CHANGED
File without changes
File without changes
package/src/demo/demo.css CHANGED
File without changes
File without changes
File without changes
File without changes
package/src/demo/index.ts CHANGED
File without changes
File without changes
package/src/demo/mode.ts CHANGED
File without changes
File without changes
File without changes
File without changes
package/src/demo/seed.ts CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -19,7 +19,9 @@
19
19
  * demo/mode (`isEmbedded`) import — this arm has no iframe showroom, so the
20
20
  * `target="_top"` escape hatch is unnecessary and every link is a normal nav.
21
21
  */
22
+ import React from "react";
22
23
  import type { ReactNode } from "react";
24
+ import { makeUseArms } from "../../../ui_shared/arms";
23
25
  import BrandMark from "./BrandMark";
24
26
  import { hugpyConfig } from "../config";
25
27
  // The link SET + order come from the ONE shared manifest so this arm can never
@@ -40,7 +42,10 @@ export const onHugpyOrigin =
40
42
  /(^|\.)hugpy\.ai$/i.test(window.location.hostname);
41
43
  export const siteBase = onHugpyOrigin ? "" : hugpyConfig.siteUrl.replace(/\/$/, "");
42
44
 
45
+ const useArms = makeUseArms(React); // arm links show only when their package is installed
46
+
43
47
  export default function Navbar({ children }: { children?: ReactNode }) {
48
+ const arms = useArms(siteBase);
44
49
  return (
45
50
  <nav className="hugpy-navbar">
46
51
  <span className="hugpy-navbar-brand">
@@ -51,7 +56,7 @@ export default function Navbar({ children }: { children?: ReactNode }) {
51
56
  identical sitewide. Rendering is local: every destination is a plain
52
57
  full-navigation <a> to a sibling hugpy SPA, prefixed with the runtime
53
58
  siteBase. Video is the current surface. */}
54
- {buildNavItems({ currentKey: "video", siteBase }).map((item) => (
59
+ {buildNavItems({ currentKey: "video", siteBase, present: arms.present }).map((item) => (
55
60
  <a
56
61
  key={item.key}
57
62
  href={item.href}
File without changes
File without changes
File without changes
package/src/session.ts CHANGED
File without changes
package/src/share.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -5489,7 +5489,8 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
5489
5489
  {assist.assistModels.map((m) => (
5490
5490
  <option key={m.model} value={m.model}>
5491
5491
  {m.model}
5492
- {m.serving ? "" : " · cold"}
5492
+ {m.state === "serving" || m.state === "loaded" ? ""
5493
+ : m.state === "hot" ? " · hot (loads)" : " · cold (downloads)"}
5493
5494
  </option>
5494
5495
  ))}
5495
5496
  </select>
File without changes
File without changes
File without changes
File without changes
@@ -32,6 +32,7 @@ import {
32
32
  type RunState,
33
33
  type SegmentRow,
34
34
  } from "./useScriptFirstRun";
35
+ import { TemplateStepsPanel } from "./TemplateStepsPanel";
35
36
  import "./scriptFirst.css";
36
37
 
37
38
  const SHORT = 12;
@@ -841,6 +842,9 @@ export function ScriptFirstStation({ spec }: { spec: StationSpec }) {
841
842
  )}
842
843
  </section>
843
844
 
845
+ {/* ---- registry template: the ordered steps + per-step model choice ---- */}
846
+ <TemplateStepsPanel templateId="video-script-first" />
847
+
844
848
  {(refusal || journalled) && (
845
849
  <section className="vi-script-panel" role="alert">
846
850
  <h3>
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -19,6 +19,7 @@ import { useState } from "react";
19
19
 
20
20
  import type { StationSpec } from "../types";
21
21
  import { DagPanel } from "./DagPanel";
22
+ import { PerformanceComposer } from "./PerformanceComposer";
22
23
  import { SpatialTab } from "./SpatialTab";
23
24
  import { StewardPanel } from "./StewardPanel";
24
25
  import { GapNote, RawJson, StateChip, Tag, short, when } from "./oracleShared";
@@ -273,6 +274,7 @@ export function OracleStation({ spec }: { spec: StationSpec }) {
273
274
  <p className="station-blurb">{spec.blurb}</p>
274
275
 
275
276
  <StewardPanel api={steward} embedded={runs.manifest?.dag?.steward ?? null} />
277
+ <PerformanceComposer onEnqueued={(jobId) => runs.select(jobId)} />
276
278
  <RunPicker runs={runs} />
277
279
  <RunHeader runs={runs} />
278
280