@rightkit/video-ui 0.0.0-stage → 0.1.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +47 -2
  5. package/dist/appName.d.ts +2 -0
  6. package/dist/appName.js +8 -0
  7. package/dist/components/DesktopWorkspace.d.ts +4 -0
  8. package/dist/components/DesktopWorkspace.js +527 -0
  9. package/dist/components/Player.d.ts +49 -0
  10. package/dist/components/Player.js +335 -0
  11. package/dist/components/RangePreview.d.ts +9 -0
  12. package/dist/components/RangePreview.js +22 -0
  13. package/dist/components/Workspace.css +15 -0
  14. package/dist/demo/DemoSettings.d.ts +1 -0
  15. package/dist/demo/DemoSettings.js +43 -0
  16. package/dist/demo/bindings.d.ts +121 -0
  17. package/dist/demo/bindings.js +57 -0
  18. package/dist/demo/demo.css +124 -0
  19. package/dist/demo/useDemo.d.ts +8 -0
  20. package/dist/demo/useDemo.js +37 -0
  21. package/dist/editor/CaptionInspector.d.ts +8 -0
  22. package/dist/editor/CaptionInspector.js +33 -0
  23. package/dist/editor/ChangesPanel.d.ts +13 -0
  24. package/dist/editor/ChangesPanel.js +87 -0
  25. package/dist/editor/ColourInspector.d.ts +5 -0
  26. package/dist/editor/ColourInspector.js +341 -0
  27. package/dist/editor/Inspector.d.ts +15 -0
  28. package/dist/editor/Inspector.js +202 -0
  29. package/dist/editor/Timeline.d.ts +39 -0
  30. package/dist/editor/Timeline.js +506 -0
  31. package/dist/editor/TimelineEditorView.d.ts +9 -0
  32. package/dist/editor/TimelineEditorView.js +237 -0
  33. package/dist/editor/TranscriptPanel.d.ts +20 -0
  34. package/dist/editor/TranscriptPanel.js +180 -0
  35. package/dist/editor/actions.d.ts +63 -0
  36. package/dist/editor/actions.js +124 -0
  37. package/dist/editor/adapter.d.ts +76 -0
  38. package/dist/editor/adapter.js +181 -0
  39. package/dist/editor/capabilities.d.ts +37 -0
  40. package/dist/editor/capabilities.js +33 -0
  41. package/dist/editor/gesture.d.ts +29 -0
  42. package/dist/editor/gesture.js +119 -0
  43. package/dist/editor/index.d.ts +10 -0
  44. package/dist/editor/index.js +7 -0
  45. package/dist/editor/keys.d.ts +15 -0
  46. package/dist/editor/keys.js +42 -0
  47. package/dist/editor/model.d.ts +88 -0
  48. package/dist/editor/model.js +87 -0
  49. package/dist/editor/transcript.d.ts +27 -0
  50. package/dist/editor/transcript.js +59 -0
  51. package/dist/editor/vendor/hyperframes/PlayheadIndicator.d.ts +9 -0
  52. package/dist/editor/vendor/hyperframes/PlayheadIndicator.js +38 -0
  53. package/dist/editor/vendor/hyperframes/audioWaveformPeaks.d.ts +7 -0
  54. package/dist/editor/vendor/hyperframes/audioWaveformPeaks.js +47 -0
  55. package/dist/editor/vendor/hyperframes/rulerGeometry.d.ts +13 -0
  56. package/dist/editor/vendor/hyperframes/rulerGeometry.js +133 -0
  57. package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.d.ts +33 -0
  58. package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.js +126 -0
  59. package/dist/env.d.ts +66 -0
  60. package/dist/hooks/useEngine.d.ts +10 -0
  61. package/dist/hooks/useEngine.js +68 -0
  62. package/dist/hooks/useHdrToSdr.d.ts +11 -0
  63. package/dist/hooks/useHdrToSdr.js +19 -0
  64. package/dist/hooks/useProgress.d.ts +10 -0
  65. package/dist/hooks/useProgress.js +32 -0
  66. package/dist/index.d.ts +55 -0
  67. package/dist/index.js +57 -0
  68. package/dist/legal/legalConfig.d.ts +38 -0
  69. package/dist/legal/legalConfig.js +77 -0
  70. package/dist/lib/assemble.d.ts +100 -0
  71. package/dist/lib/assemble.js +72 -0
  72. package/dist/lib/asset.d.ts +2 -0
  73. package/dist/lib/asset.js +3 -0
  74. package/dist/lib/dialog.d.ts +4 -0
  75. package/dist/lib/dialog.js +44 -0
  76. package/dist/lib/engine.d.ts +44 -0
  77. package/dist/lib/engine.js +51 -0
  78. package/dist/lib/genright.d.ts +90 -0
  79. package/dist/lib/genright.js +160 -0
  80. package/dist/lib/media.d.ts +1045 -0
  81. package/dist/lib/media.js +171 -0
  82. package/dist/lib/mediaAdapters.d.ts +18 -0
  83. package/dist/lib/mediaAdapters.js +46 -0
  84. package/dist/lib/mic.d.ts +38 -0
  85. package/dist/lib/mic.js +14 -0
  86. package/dist/lib/models.d.ts +53 -0
  87. package/dist/lib/models.js +72 -0
  88. package/dist/lib/modelsBackend.d.ts +3 -0
  89. package/dist/lib/modelsBackend.js +68 -0
  90. package/dist/lib/progress.d.ts +77 -0
  91. package/dist/lib/progress.js +192 -0
  92. package/dist/lib/project.d.ts +138 -0
  93. package/dist/lib/project.js +48 -0
  94. package/dist/lib/services.d.ts +24 -0
  95. package/dist/lib/services.js +38 -0
  96. package/dist/lib/settingsBackend.d.ts +2 -0
  97. package/dist/lib/settingsBackend.js +57 -0
  98. package/dist/lib/shell.d.ts +171 -0
  99. package/dist/lib/shell.js +32 -0
  100. package/dist/styles.css +16 -0
  101. package/dist/ui/icons.d.ts +7 -0
  102. package/dist/ui/icons.js +62 -0
  103. package/dist/ui/index.d.ts +45 -0
  104. package/dist/ui/index.js +43 -0
  105. package/dist/ui/ui.css +104 -0
  106. package/dist/views/Assets.css +99 -0
  107. package/dist/views/AssetsView.d.ts +6 -0
  108. package/dist/views/AssetsView.js +371 -0
  109. package/dist/views/AutoEditView.d.ts +16 -0
  110. package/dist/views/AutoEditView.js +452 -0
  111. package/dist/views/BatchDelivery.d.ts +3 -0
  112. package/dist/views/BatchDelivery.js +331 -0
  113. package/dist/views/BundleExport.d.ts +3 -0
  114. package/dist/views/BundleExport.js +81 -0
  115. package/dist/views/Exports.css +117 -0
  116. package/dist/views/ExportsView.d.ts +12 -0
  117. package/dist/views/ExportsView.js +354 -0
  118. package/dist/views/FinishedEdit.css +546 -0
  119. package/dist/views/Home.css +97 -0
  120. package/dist/views/Home.d.ts +14 -0
  121. package/dist/views/Home.js +192 -0
  122. package/dist/views/Media.css +65 -0
  123. package/dist/views/OnboardingGate.d.ts +10 -0
  124. package/dist/views/OnboardingGate.js +54 -0
  125. package/dist/views/Phrases.css +45 -0
  126. package/dist/views/PhrasesView.d.ts +4 -0
  127. package/dist/views/PhrasesView.js +104 -0
  128. package/dist/views/ProgressView.d.ts +19 -0
  129. package/dist/views/ProgressView.js +36 -0
  130. package/dist/views/Prompt.css +84 -0
  131. package/dist/views/PromptPanel.d.ts +8 -0
  132. package/dist/views/PromptPanel.js +169 -0
  133. package/dist/views/Recipe.css +84 -0
  134. package/dist/views/RecipeLibrary.d.ts +7 -0
  135. package/dist/views/RecipeLibrary.js +104 -0
  136. package/dist/views/Retention.css +49 -0
  137. package/dist/views/RetentionView.d.ts +7 -0
  138. package/dist/views/RetentionView.js +145 -0
  139. package/dist/views/SettingsView.d.ts +9 -0
  140. package/dist/views/SettingsView.js +235 -0
  141. package/dist/views/Shorts.css +112 -0
  142. package/dist/views/ShortsView.d.ts +4 -0
  143. package/dist/views/ShortsView.js +205 -0
  144. package/dist/views/SourcesView.d.ts +6 -0
  145. package/dist/views/SourcesView.js +417 -0
  146. package/dist/views/TimelineView.d.ts +20 -0
  147. package/dist/views/TimelineView.js +12 -0
  148. package/package.json +70 -3
@@ -0,0 +1,371 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Assets — docs/design/app-ui.md §3.7. Generated media library + the host app jobs.
3
+ import { Badge, Button, EmptyState, Field, IconButton, Input, PageShell, SegmentedControl, Select, useToast } from "@rightkit/react-shell/react";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { engineCall, errorMessage, revealPath } from "../lib/engine.js";
6
+ import { captionsShow } from "../lib/media.js";
7
+ import { genRightCallFailed, interpretAssetLibrary, interpretHost, interpretJobs, interpretRecover, placeModesFor, } from "../lib/genright.js";
8
+ import { durationControl, formatUsd, ofKind, parseCatalog, pickModel } from "../lib/models.js";
9
+ import { settingsSet } from "../lib/media.js";
10
+ import { assetUrl } from "../lib/asset.js";
11
+ import { pickFile } from "../lib/dialog.js";
12
+ import { MediaPlayer } from "@rightkit/media-ui";
13
+ import { ImageViewer } from "@rightkit/media-ui/image";
14
+ import { useMediaPlaybackId } from "../lib/mediaAdapters.js";
15
+ import { Drawer, ErrorPanel, Icon, ProgressRow, Thumbnail, fileSize } from "../ui/index.js";
16
+ import { hostAppName } from "../appName.js";
17
+ export function jobActionLabel(j) {
18
+ switch (j.state) {
19
+ case "queued":
20
+ case "running":
21
+ return "Cancel";
22
+ case "ready":
23
+ return "Import now";
24
+ case "failed":
25
+ return "Retry";
26
+ default:
27
+ return null;
28
+ }
29
+ }
30
+ const JOB_TONE = {
31
+ queued: "info",
32
+ running: "info",
33
+ ready: "ok",
34
+ done: "ok",
35
+ failed: "danger",
36
+ cancelled: "warn",
37
+ import_failed: "warn",
38
+ unknown: "warn",
39
+ };
40
+ const FILTERS = [
41
+ { value: "all", label: "All" },
42
+ { value: "broll", label: "B-roll" },
43
+ { value: "image", label: "Images" },
44
+ { value: "voice", label: "Voice" },
45
+ { value: "music", label: "Music" },
46
+ { value: "sfx", label: "SFX" },
47
+ ];
48
+ function filterMatch(kind, f) {
49
+ if (f === "all")
50
+ return true;
51
+ const k = (kind ?? "").toLowerCase();
52
+ if (f === "image")
53
+ return k === "image" || k === "still";
54
+ if (f === "voice")
55
+ return k === "voice" || k === "narration";
56
+ if (f === "music")
57
+ return k === "music" || k === "song";
58
+ if (f === "sfx")
59
+ return k === "sfx" || k === "sound";
60
+ if (f === "broll")
61
+ return k === "broll" || k === "video";
62
+ return true;
63
+ }
64
+ const isAudio = (a) => /^audio\//.test(a.mime) || /\.(wav|mp3|m4a|flac|aac|ogg)$/i.test(a.path);
65
+ const isImage = (a) => /^image\//.test(a.mime) || /\.(png|jpe?g|webp)$/i.test(a.path);
66
+ const fullPath = (dir, a) => (a.path.startsWith("/") ? a.path : `${dir}/${a.path}`);
67
+ /** Library tile: real image/video frame for visuals, waveform-style tile for audio. */
68
+ function AssetTile({ dir, asset }) {
69
+ const p = fullPath(dir, asset);
70
+ if (!asset.exists) {
71
+ return (_jsx("div", { className: "assets-tile", children: _jsx(Icon, { name: "triangle-alert", width: 22, height: 22 }) }));
72
+ }
73
+ if (isImage(asset))
74
+ return _jsx(Thumbnail, { src: assetUrl(p), aspect: "16 / 9" });
75
+ if (isAudio(asset)) {
76
+ return (_jsxs("div", { className: "assets-tile assets-tile--audio", children: [_jsx(Icon, { name: asset.kind === "voice" ? "mic" : "music", width: 22, height: 22 }), _jsx("audio", { src: assetUrl(p), preload: "metadata" })] }));
77
+ }
78
+ return (_jsx("div", { className: "assets-tile", children: _jsx("video", { src: `${assetUrl(p)}#t=0.1`, preload: "metadata", muted: true, playsInline: true }) }));
79
+ }
80
+ // R11: spec menu labels — engine place modes underneath.
81
+ const PLACE_LABEL = {
82
+ append: "At end of timeline",
83
+ broll: "As B-roll over…",
84
+ narration: "As narration",
85
+ music: "As music bed",
86
+ };
87
+ /** Place-on-timeline menu + the fields the chosen mode needs. */
88
+ function PlaceMenu({ dir, asset, onPlaced }) {
89
+ const toast = useToast();
90
+ const modes = placeModesFor(asset.kind);
91
+ const [open, setOpen] = useState(false);
92
+ const [mode, setMode] = useState(null);
93
+ const [start, setStart] = useState("");
94
+ const [link, setLink] = useState("");
95
+ const [gain, setGain] = useState("");
96
+ const [busy, setBusy] = useState(false);
97
+ // B-roll range: caption cues of the timeline, and the anchor/last indices picked from them.
98
+ const [cues, setCues] = useState(null);
99
+ const [cueSel, setCueSel] = useState(null);
100
+ const ref = useRef(null);
101
+ useEffect(() => {
102
+ if (mode !== "broll")
103
+ return;
104
+ let live = true;
105
+ setCues(null);
106
+ setCueSel(null);
107
+ captionsShow(dir)
108
+ .then((r) => live && setCues(r.cues))
109
+ .catch((e) => {
110
+ if (!live)
111
+ return;
112
+ setCues([]);
113
+ toast(errorMessage(e), { kind: "error", ttl: 0 });
114
+ });
115
+ return () => {
116
+ live = false;
117
+ };
118
+ // toast is stable per provider; the cue list reloads only when the mode or project changes.
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, [mode, dir]);
121
+ useEffect(() => {
122
+ if (!open)
123
+ return;
124
+ const close = (e) => {
125
+ if (!ref.current?.contains(e.target)) {
126
+ setOpen(false);
127
+ setMode(null);
128
+ }
129
+ };
130
+ window.addEventListener("mousedown", close);
131
+ return () => window.removeEventListener("mousedown", close);
132
+ }, [open]);
133
+ const num = (s) => {
134
+ const n = Number(s);
135
+ return s.trim() !== "" && Number.isFinite(n) ? n : undefined;
136
+ };
137
+ // The contiguous run of cues picked for B-roll, as timeline seconds (start of first, end of last).
138
+ const cueRange = () => {
139
+ if (!cues || !cueSel)
140
+ return null;
141
+ const lo = Math.min(cueSel.a, cueSel.b);
142
+ const hi = Math.max(cueSel.a, cueSel.b);
143
+ return { start: cues[lo].start_ms / 1000, end: cues[hi].end_ms / 1000 };
144
+ };
145
+ const place = (m) => {
146
+ const range = m === "broll" ? cueRange() : null;
147
+ setBusy(true);
148
+ engineCall("assets.place", {
149
+ dir,
150
+ asset_id: asset.assetId,
151
+ mode: m,
152
+ ...(range ? { start_s: range.start, end_s: range.end } : {}),
153
+ ...(!range && num(start) !== undefined ? { start_s: num(start) } : {}),
154
+ ...(link.trim() ? { link_clip: link.trim() } : {}),
155
+ ...(num(gain) !== undefined ? { gain_db: num(gain) } : {}),
156
+ })
157
+ .then(() => {
158
+ toast("Placed on the timeline", { kind: "success" });
159
+ setOpen(false);
160
+ setMode(null);
161
+ onPlaced();
162
+ })
163
+ .catch((e) => toast(errorMessage(e), { kind: "error", ttl: 0 }))
164
+ .finally(() => setBusy(false));
165
+ };
166
+ const needsRange = mode === "broll";
167
+ const needsLink = mode === "narration";
168
+ return (_jsxs("div", { className: "assets-place", ref: ref, children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setOpen((v) => !v), children: "Place on timeline" }), open && (_jsx("div", { className: "assets-place__pop", children: !mode ? (modes.map((m) => (_jsx("button", { type: "button", onClick: () => (m === "append" ? place(m) : setMode(m)), children: PLACE_LABEL[m] }, m)))) : (_jsxs("div", { className: "assets-place__form", children: [_jsx("span", { className: "cr-overline", children: PLACE_LABEL[mode] }), needsRange && (_jsxs("div", { className: "assets-cues", "data-testid": "broll-cues", children: [cues === null && _jsx("span", { className: "assets-cues__meta", children: "Loading caption cues\u2026" }), cues?.length === 0 && _jsx("span", { className: "assets-cues__meta", children: "No caption cues on the timeline yet." }), cues?.map((c, i) => {
169
+ const lo = cueSel ? Math.min(cueSel.a, cueSel.b) : -1;
170
+ const hi = cueSel ? Math.max(cueSel.a, cueSel.b) : -1;
171
+ const on = i >= lo && i <= hi;
172
+ return (_jsxs("button", { type: "button", "data-testid": "broll-cue", "data-index": i, "data-start-ms": c.start_ms, "data-end-ms": c.end_ms, "aria-pressed": on, title: "Click the first cue, then shift-click the last", onClick: (e) => setCueSel((prev) => (e.shiftKey && prev ? { a: prev.a, b: i } : { a: i, b: i })), children: [_jsxs("span", { className: "assets-cues__time", children: [(c.start_ms / 1000).toFixed(2), "\u2013", (c.end_ms / 1000).toFixed(2), " s"] }), " ", c.text] }, `${c.start_ms}-${i}`));
173
+ }), _jsx("span", { className: "assets-cues__meta", "data-testid": "broll-range", children: (() => {
174
+ const r = cueRange();
175
+ return r ? `B-roll ${r.start.toFixed(2)} s to ${r.end.toFixed(2)} s` : "Pick a start cue, then shift-click an end cue.";
176
+ })() })] })), needsLink && (_jsxs("label", { children: ["Start (s)", _jsx(Input, { value: start, onChange: (e) => setStart(e.target.value), placeholder: "auto" })] })), needsLink && (_jsxs("label", { children: ["Link to clip", _jsx(Input, { value: link, onChange: (e) => setLink(e.target.value), placeholder: "clip id" })] })), mode === "music" && (_jsxs("label", { children: ["Bed level (dB under)", _jsx(Input, { value: gain, onChange: (e) => setGain(e.target.value), placeholder: "24" })] })), _jsxs("div", { className: "assets-place__actions", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setMode(null), children: "Back" }), _jsx(Button, { size: "sm", variant: "primary", onClick: () => place(mode), disabled: (needsRange && cueRange() === null) || (busy), "aria-busy": (busy) || undefined, children: busy ? "Placing…" : "Place" })] })] })) }))] }));
177
+ }
178
+ const GEN_KINDS = [
179
+ { value: "video", label: "Video" },
180
+ { value: "image", label: "Image" },
181
+ { value: "voice", label: "Voice" },
182
+ { value: "music", label: "Music" },
183
+ { value: "sfx", label: "SFX" },
184
+ { value: "avatar", label: "Avatar" },
185
+ { value: "upscale", label: "Upscale" },
186
+ { value: "lipsync", label: "Lipsync" },
187
+ ];
188
+ /** Transform kinds re-process an existing source; the engine request has no input-reference field yet. */
189
+ const NEEDS_SOURCE = new Set(["upscale", "lipsync"]);
190
+ function GenerateDrawer({ dir, open, onClose, onDone }) {
191
+ const toast = useToast();
192
+ const [kind, setKind] = useState("video");
193
+ const [prompt, setPrompt] = useState("");
194
+ const [models, setModels] = useState(null);
195
+ const [model, setModel] = useState(null);
196
+ const [duration, setDuration] = useState(undefined);
197
+ const [busy, setBusy] = useState(false);
198
+ const reqSeq = useRef(0);
199
+ useEffect(() => {
200
+ if (!open)
201
+ return;
202
+ engineCall("genright.models", {})
203
+ .then((r) => setModels(parseCatalog(r).models))
204
+ .catch(() => setModels([]));
205
+ }, [open]);
206
+ // Models are listed per kind from the host app own catalog; a kind with no rows has no model.
207
+ useEffect(() => {
208
+ const wanted = models ? pickModel({ models, structuredDurations: true }, kind) : undefined;
209
+ setModel(wanted ?? null);
210
+ setDuration(wanted ? durationControl(wanted).initial : undefined);
211
+ }, [kind, models]);
212
+ const dur = durationControl(model ?? undefined);
213
+ const needsSource = NEEDS_SOURCE.has(kind);
214
+ const submit = async () => {
215
+ if (!model || !prompt.trim() || needsSource)
216
+ return;
217
+ setBusy(true);
218
+ reqSeq.current += 1;
219
+ const requestId = `ui-gen-${Date.now().toString(36)}-${reqSeq.current}`;
220
+ try {
221
+ await engineCall("generate", {
222
+ dir,
223
+ kind,
224
+ input: {
225
+ provider: model.provider,
226
+ model: model.model,
227
+ prompt: prompt.trim(),
228
+ request_id: requestId,
229
+ ...(duration !== undefined ? { duration_s: duration } : {}),
230
+ },
231
+ });
232
+ toast("Generation finished and imported", { kind: "success" });
233
+ setPrompt("");
234
+ onClose();
235
+ onDone();
236
+ }
237
+ catch (e) {
238
+ toast(errorMessage(e), { kind: "error", ttl: 0 });
239
+ onDone();
240
+ }
241
+ finally {
242
+ setBusy(false);
243
+ }
244
+ };
245
+ const shown = models ? ofKind({ models, structuredDurations: true }, kind) : [];
246
+ const kindLabel = GEN_KINDS.find((k) => k.value === kind)?.label.toLowerCase() ?? kind;
247
+ const modelHelp = models === null
248
+ ? "Loading models…"
249
+ : models.length === 0
250
+ ? `${hostAppName()} reported no models.`
251
+ : shown.length === 0
252
+ ? `${hostAppName()} lists no ${kindLabel} models.`
253
+ : needsSource
254
+ ? "Upscale and lipsync need a source asset; this picker cannot send one yet."
255
+ : undefined;
256
+ return (_jsxs(Drawer, { title: "Generate", open: open, onClose: onClose, footer: _jsxs(Button, { variant: "primary", onClick: () => void submit(), disabled: (!model || !prompt.trim() || needsSource) || (busy), "aria-busy": (busy) || undefined, children: [!(busy) && _jsx(Icon, { name: "sparkles" }), busy ? "Generating…" : "Generate"] }), children: [_jsx(Field, { label: "Kind", children: () => (_jsx(SegmentedControl, { value: kind, options: GEN_KINDS, onChange: setKind, label: "Asset kind" })) }), _jsx(Field, { label: "Prompt", children: (ids) => (_jsx("textarea", { ...ids, className: "rk-input cr-textarea", placeholder: "Describe the asset\u2026", value: prompt, onChange: (e) => setPrompt(e.target.value) })) }), _jsx(Field, { label: "Model", note: modelHelp, children: (ids) => (_jsx("select", { ...ids, className: "rk-input cr-select", value: model ? `${model.provider}/${model.model}` : "", onChange: (e) => {
257
+ const m = shown.find((x) => `${x.provider}/${x.model}` === e.target.value) ?? null;
258
+ setModel(m);
259
+ setDuration(m ? durationControl(m).initial : undefined);
260
+ }, children: shown.map((m) => (_jsxs("option", { value: `${m.provider}/${m.model}`, children: [m.label, " \u00B7 ", m.provider, "/", m.model, m.estUsd !== null ? ` · ${formatUsd(m.estUsd)}` : ""] }, `${m.provider}/${m.model}`))) })) }), model && (_jsx(Field, { label: "Duration", note: dur.type === "range" ? `${dur.min}–${dur.max} s` : undefined, children: () => (dur.type === "set" ? (_jsx(Select, { label: "Duration", value: duration === undefined ? String(dur.options[0]) : String(duration), options: dur.options.map((d) => ({ value: String(d), label: `${d} s` })), onChange: (value) => setDuration(Number(value)) })) : (_jsx(Input, { type: "number", min: dur.type === "range" ? dur.min : 1, max: dur.type === "range" ? dur.max : 600, step: dur.type === "range" ? 1 : 0.5, value: duration ?? dur.initial, onChange: (e) => setDuration(Number(e.target.value)) }))) }))] }));
261
+ }
262
+ export function AssetsView({ dir, onNewVideo }) {
263
+ const toast = useToast();
264
+ const [gr, setGr] = useState({ state: "checking" });
265
+ const [jobs, setJobs] = useState(null);
266
+ const [assets, setAssets] = useState(null);
267
+ const [filter, setFilter] = useState("all");
268
+ const [genOpen, setGenOpen] = useState(false);
269
+ const [preview, setPreview] = useState(null);
270
+ const previewPath = dir && preview && !isImage(preview) ? fullPath(dir, preview) : null;
271
+ const previewPlaybackId = useMediaPlaybackId(previewPath);
272
+ const [busy, setBusy] = useState(null);
273
+ const [error, setError] = useState(null);
274
+ const [recovered, setRecovered] = useState(new Set());
275
+ void onNewVideo;
276
+ const checkStatus = useCallback(() => {
277
+ engineCall("genright.status", {})
278
+ .then((r) => setGr(interpretHost(r)))
279
+ .catch((e) => setGr(genRightCallFailed(errorMessage(e))));
280
+ }, []);
281
+ useEffect(() => {
282
+ let cancelled = false;
283
+ engineCall("genright.status", {})
284
+ .then((r) => !cancelled && setGr(interpretHost(r)))
285
+ .catch((e) => !cancelled && setGr(genRightCallFailed(errorMessage(e))));
286
+ return () => {
287
+ cancelled = true;
288
+ };
289
+ }, []);
290
+ const refresh = useCallback(() => {
291
+ if (!dir)
292
+ return;
293
+ engineCall("genright.jobs", { dir, live: gr.state === "ready" })
294
+ .then((r) => setJobs(interpretJobs(r)))
295
+ .catch((e) => setError(errorMessage(e)));
296
+ engineCall("assets.list", { dir })
297
+ .then((r) => setAssets(interpretAssetLibrary(r).assets))
298
+ .catch((e) => setError(errorMessage(e)));
299
+ }, [dir, gr.state]);
300
+ useEffect(() => {
301
+ refresh();
302
+ }, [refresh]);
303
+ // Poll while jobs are in flight.
304
+ const jobsActive = (jobs ?? []).some((j) => j.state === "queued" || j.state === "running");
305
+ useEffect(() => {
306
+ if (!jobsActive)
307
+ return;
308
+ const t = window.setInterval(refresh, 3000);
309
+ return () => window.clearInterval(t);
310
+ }, [jobsActive, refresh]);
311
+ const locate = async () => {
312
+ const file = await pickFile(`Locate the ${hostAppName()} engine`, []);
313
+ if (!file)
314
+ return;
315
+ try {
316
+ await settingsSet({ genright_engine_path: file });
317
+ toast(`${hostAppName()} path saved — checking…`, { kind: "success" });
318
+ checkStatus();
319
+ }
320
+ catch (e) {
321
+ toast(errorMessage(e), { kind: "error", ttl: 0 });
322
+ }
323
+ };
324
+ const jobAction = (j) => {
325
+ if (!dir)
326
+ return;
327
+ const label = jobActionLabel(j);
328
+ if (!label)
329
+ return;
330
+ setBusy(j.requestId);
331
+ const call = label === "Cancel"
332
+ ? engineCall("genright.cancel", { dir, request_id: j.requestId })
333
+ : engineCall("genright.recover", { dir, request_id: j.requestId, retry_failed: label === "Retry" });
334
+ call
335
+ .then(() => refresh())
336
+ .catch((e) => setError(errorMessage(e)))
337
+ .finally(() => setBusy(null));
338
+ };
339
+ const relink = (a) => {
340
+ if (!dir)
341
+ return;
342
+ setBusy(a.assetId);
343
+ engineCall("assets.relink", { dir, asset_id: a.assetId })
344
+ .then(() => refresh())
345
+ .catch((e) => setError(errorMessage(e)))
346
+ .finally(() => setBusy(null));
347
+ };
348
+ // Recover interrupted jobs once per project open; recovered request ids get
349
+ // the `Recovered` chip in the jobs list.
350
+ const recoveredOnce = useRef(null);
351
+ useEffect(() => {
352
+ if (!dir || gr.state !== "ready" || recoveredOnce.current === dir)
353
+ return;
354
+ recoveredOnce.current = dir;
355
+ engineCall("genright.recover", { dir })
356
+ .then((r) => {
357
+ const done = interpretRecover(r);
358
+ setRecovered(new Set(done.map((d) => d.requestId)));
359
+ const n = done.filter((d) => d.state === "done").length;
360
+ if (n > 0)
361
+ toast(`Recovered ${n} interrupted job${n === 1 ? "" : "s"}`, { kind: "success" });
362
+ refresh();
363
+ })
364
+ .catch(() => undefined);
365
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- once per dir
366
+ }, [dir, gr.state]);
367
+ const shown = (assets ?? []).filter((a) => filterMatch(a.kind, filter));
368
+ return (_jsxs(PageShell, { title: "Assets", actions: _jsxs(_Fragment, { children: [_jsx(SegmentedControl, { value: filter, options: FILTERS, onChange: setFilter, label: "Filter assets" }), _jsxs(Button, { variant: "primary", disabled: !dir || gr.state !== "ready", onClick: () => setGenOpen(true), children: [_jsx(Icon, { name: "sparkles" }), "Generate\u2026"] })] }), maxWidth: 0, subtitle: assets ? `${assets.length} generated asset${assets.length === 1 ? "" : "s"}` : undefined, children: [_jsxs("div", { className: "assets-body", children: [error && _jsx(ErrorPanel, { title: "Assets failed", detail: error }), _jsx("div", { className: "assets-main", children: !dir ? (_jsx(EmptyState, { title: "No project open", icon: _jsx(Icon, { name: "image", width: 28, height: 28 }), children: "Open a project to see its generated assets." })) : assets === null ? (_jsx(ProgressRow, { label: "Loading assets", status: "Loading", fraction: undefined })) : shown.length === 0 ? (_jsx(EmptyState, { title: assets.length === 0 ? "No assets yet" : "Nothing in this filter", action: assets.length === 0 && gr.state === "ready" ? (_jsx(Button, { variant: "primary", onClick: () => setGenOpen(true), children: "Generate\u2026" })) : undefined, icon: _jsx(Icon, { name: "image", width: 28, height: 28 }), children: assets.length === 0
369
+ ? "Generated b-roll, images, voice and music land here."
370
+ : "No generated assets of this kind yet." })) : (_jsx("div", { className: "assets-grid", children: shown.map((a) => (_jsxs("div", { className: "assets-card", children: [_jsx("button", { type: "button", className: "assets-card__preview", "aria-label": `Preview ${a.prompt ?? a.assetId}`, disabled: !a.exists, onClick: () => setPreview(a), children: _jsx(AssetTile, { dir: dir, asset: a }) }), _jsx("div", { className: "assets-card__name cr-clamp-1", title: a.prompt ?? a.assetId, children: a.prompt ?? a.assetId }), _jsxs("div", { className: "assets-card__row", children: [_jsx(Badge, { tone: a.state === "ready" ? "ok" : "warn", children: a.kind ?? "media" }), a.sourceId && _jsx(Badge, { tone: "accent", children: "On timeline" })] }), _jsxs("div", { className: "assets-card__prov cr-muted", children: [hostAppName(), " \u00B7 ", a.model, a.actualUsd !== null ? ` · ${formatUsd(a.actualUsd)}` : "", a.bytes ? ` · ${fileSize(a.bytes)}` : ""] }), _jsxs("div", { className: "assets-card__actions", children: [a.state === "missing" ? (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => relink(a), disabled: (busy === a.assetId), "aria-busy": (busy === a.assetId) || undefined, children: busy === a.assetId ? "Working…" : a.recovery === "relink" ? "Relink" : "Retry import" })) : (a.exists && _jsx(PlaceMenu, { dir: dir, asset: a, onPlaced: refresh })), a.exists && (_jsx(IconButton, { label: "Reveal", onClick: () => void revealPath(fullPath(dir, a)).catch((e) => toast(errorMessage(e), { kind: "error", ttl: 0 })), children: _jsx(Icon, { name: "folder-open" }) }))] })] }, a.assetId))) })) }), gr.state !== "ready" && gr.state !== "checking" && (_jsx(ErrorPanel, { title: gr.state === "missing" ? `${hostAppName()} not found` : `${hostAppName()} unavailable`, detail: gr.detail, actions: _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void locate(), children: "Locate\u2026" }) })), jobs !== null && jobs.length > 0 && (_jsxs("div", { className: "assets-jobs", children: [_jsx("span", { className: "cr-overline", children: "Generation jobs" }), jobs.map((j) => (_jsxs("div", { className: "assets-job", children: [_jsx(ProgressRow, { label: j.prompt || `${j.kind} · ${j.provider}/${j.model}`, status: j.state, tone: JOB_TONE[j.state] ?? "neutral", fraction: j.state === "queued" || j.state === "running" ? undefined : null, error: j.error?.message ?? undefined, onRetry: j.state === "failed" ? () => jobAction(j) : undefined }), recovered.has(j.requestId) && _jsx(Badge, { tone: "accent", children: "Recovered" }), (j.actualUsd ?? j.estUsd) !== null && (_jsx("span", { className: "cr-mono cr-muted assets-job__cost", children: formatUsd(j.actualUsd ?? j.estUsd ?? 0) })), jobActionLabel(j) && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => jobAction(j), disabled: (busy === j.requestId), "aria-busy": (busy === j.requestId) || undefined, children: busy === j.requestId ? "Working…" : jobActionLabel(j) }))] }, j.requestId)))] }))] }), dir && _jsx(GenerateDrawer, { dir: dir, open: genOpen, onClose: () => setGenOpen(false), onDone: refresh }), dir && preview && (_jsx(Drawer, { title: preview.prompt ?? preview.assetId, open: true, onClose: () => setPreview(null), children: isImage(preview) ? (_jsx("div", { className: "assets-image-viewer", children: _jsx(ImageViewer, { path: fullPath(dir, preview), src: assetUrl(fullPath(dir, preview)), displayName: preview.prompt ?? preview.assetId, readOnly: true }) })) : (_jsx(MediaPlayer, { src: fullPath(dir, preview), playbackId: previewPlaybackId, kind: isAudio(preview) ? "audio" : "video", title: preview.prompt ?? preview.assetId, autoPlay: true }, previewPlaybackId ?? "pending")) }))] }));
371
+ }
@@ -0,0 +1,16 @@
1
+ import { type SourceRow, type ReframeSummary } from "../lib/media.js";
2
+ export declare function AutoEditView({ dir, onChanged, onNavigate, }: {
3
+ dir: string;
4
+ onChanged: () => Promise<void>;
5
+ onNavigate?: (to: "recipe" | "finished") => void;
6
+ }): import("react").JSX.Element;
7
+ /** Framing review for one (source, aspect) plan; shared by Edit recipe and Shorts. */
8
+ export declare function FramingDrawer({ dir, source, aspect, plan, autoFraming, onClose, onChanged, }: {
9
+ dir: string;
10
+ source: SourceRow;
11
+ aspect: string;
12
+ plan?: ReframeSummary | "none" | "error";
13
+ autoFraming?: boolean;
14
+ onClose: () => void;
15
+ onChanged: (p: ReframeSummary) => void;
16
+ }): import("react").JSX.Element;