@vivix-labs/project-studio 0.0.0-stage → 1.0.0-beta

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 (61) hide show
  1. package/dist/Inter-LICENSE.txt +93 -0
  2. package/dist/UI-LICENSE.txt +21 -0
  3. package/dist/assets/35a50-G5EJUNOC.svg +6 -0
  4. package/dist/assets/369ca-BISNKZ3M.svg +6 -0
  5. package/dist/assets/441bc-BCMNQ72W.svg +6 -0
  6. package/dist/assets/483f0-T6ZAGTDG.svg +6 -0
  7. package/dist/assets/6911b-ESLYKSC6.svg +6 -0
  8. package/dist/assets/Inter-LICENSE.txt +93 -0
  9. package/dist/assets/UI-system-LICENSE.txt +21 -0
  10. package/dist/assets/add-JY533O73.svg +5 -0
  11. package/dist/assets/area-P2WOMBT5.svg +9 -0
  12. package/dist/assets/b70bb-BTTOKPOH.svg +6 -0
  13. package/dist/assets/b9919-3S3O6YOT.svg +6 -0
  14. package/dist/assets/calendar-JI6USFRJ.svg +1 -0
  15. package/dist/assets/check-JSRRIXN5.svg +5 -0
  16. package/dist/assets/check-on-content-FQUVG3IZ.svg +5 -0
  17. package/dist/assets/chevron-down-K7YKS2EY.svg +5 -0
  18. package/dist/assets/chevron-left-JQCLK2WZ.svg +3 -0
  19. package/dist/assets/chevron-right-TNE35DBJ.svg +3 -0
  20. package/dist/assets/chevron-up-VBHYAJVB.svg +5 -0
  21. package/dist/assets/clear-XNEZJKFC.svg +5 -0
  22. package/dist/assets/close-GO5F7YFS.svg +10 -0
  23. package/dist/assets/dccd4-CZJXWICW.svg +6 -0
  24. package/dist/assets/dd163-3S3O6YOT.svg +6 -0
  25. package/dist/assets/draw-FOFX5TOM.svg +6 -0
  26. package/dist/assets/e8aed-CZJXWICW.svg +6 -0
  27. package/dist/assets/ea990-CZJXWICW.svg +6 -0
  28. package/dist/assets/expand-IAV75AFA.svg +6 -0
  29. package/dist/assets/fullscreen-L2JCXIN2.svg +6 -0
  30. package/dist/assets/grid-TLG5JYWB.svg +8 -0
  31. package/dist/assets/import-AFBUV2VV.svg +4 -0
  32. package/dist/assets/inter-latin-400-normal-55WT6UWF.woff2 +0 -0
  33. package/dist/assets/inter-latin-500-normal-W62DVTXI.woff2 +0 -0
  34. package/dist/assets/inter-latin-600-normal-R6Y2SZCR.woff2 +0 -0
  35. package/dist/assets/list-5VUAOZUH.svg +9 -0
  36. package/dist/assets/magic-OMME7FOV.svg +9 -0
  37. package/dist/assets/more-AEJ5MHWV.svg +5 -0
  38. package/dist/assets/mute-SXOSCR2U.svg +6 -0
  39. package/dist/assets/pause-2E4UY4FW.svg +6 -0
  40. package/dist/assets/play-WIIT3XQK.svg +5 -0
  41. package/dist/assets/reset-JESMTEMV.svg +6 -0
  42. package/dist/assets/search-JUOSGQRM.svg +6 -0
  43. package/dist/assets/select-VU7WR7B4.svg +7 -0
  44. package/dist/assets/sound-file-YYY6VNYN.svg +3 -0
  45. package/dist/assets/volume-2TYGJL6F.svg +6 -0
  46. package/dist/client.bundle.js +1373 -0
  47. package/dist/client.css +1900 -0
  48. package/dist/client.d.ts +19 -0
  49. package/dist/client.js +3 -0
  50. package/dist/events.d.ts +102 -0
  51. package/dist/events.js +244 -0
  52. package/dist/server.d.ts +16 -0
  53. package/dist/server.js +3502 -0
  54. package/dist/shared-BUdviVVD.d.ts +263 -0
  55. package/dist/shared.d.ts +510 -0
  56. package/dist/shared.js +228 -0
  57. package/package.json +50 -3
  58. package/schemas/billing-notice.json +16 -0
  59. package/schemas/task-notice.json +57 -0
  60. package/studio-plugin.json +40 -0
  61. package/README.md +0 -3
@@ -0,0 +1,1373 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // packages/project-studio/src/cost.ts
12
+ function taskCostLabel(task) {
13
+ const cost = task.cost;
14
+ if (cost?.status === "settled" && cost.credits != null) return `${cost.credits} credits`;
15
+ if (cost?.status === "pending") return "Pending settlement";
16
+ return "-";
17
+ }
18
+ var init_cost = __esm({
19
+ "packages/project-studio/src/cost.ts"() {
20
+ "use strict";
21
+ }
22
+ });
23
+
24
+ // packages/ui-system/dist/icon.js
25
+ import { jsx as _jsx } from "react/jsx-runtime";
26
+ function Icon({ name, size }) {
27
+ return _jsx("span", { "aria-hidden": "true", className: "vx-icon", "data-icon": name, style: size === void 0 ? void 0 : { "--vx-icon-size": `${size}px` } });
28
+ }
29
+ var init_icon = __esm({
30
+ "packages/ui-system/dist/icon.js"() {
31
+ "use strict";
32
+ }
33
+ });
34
+
35
+ // packages/ui-system/dist/button-variants.js
36
+ var buttonVariants;
37
+ var init_button_variants = __esm({
38
+ "packages/ui-system/dist/button-variants.js"() {
39
+ "use strict";
40
+ buttonVariants = {
41
+ "text-filled-sm": { size: 32, appearance: "filled", shape: "text", width: 215 },
42
+ "text-filled-leading-sm": { size: 32, appearance: "filled", shape: "text" },
43
+ "text-ghost-sm": { size: 32, appearance: "ghost", shape: "text" },
44
+ "text-on-content-sm": { size: 32, appearance: "on-content", shape: "text" },
45
+ "text-tonal-sm": { size: 32, appearance: "tonal", shape: "text" },
46
+ // The medium recipes are shared by all dialog footer actions.
47
+ "text-filled-md": { size: 40, appearance: "filled", shape: "text" },
48
+ "text-tonal-md": { size: 40, appearance: "tonal", shape: "text" },
49
+ "text-ghost-md": { size: 40, appearance: "ghost", shape: "text" },
50
+ "icon-ghost-md": { size: 40, appearance: "ghost", shape: "icon" },
51
+ "text-filled-lg": { size: 44, appearance: "filled", shape: "text", width: 412 },
52
+ "text-tonal-lg": { size: 44, appearance: "tonal", shape: "text", width: 412 },
53
+ "text-outline-lg": {
54
+ size: 44,
55
+ appearance: "outline",
56
+ shape: "text",
57
+ width: 412
58
+ },
59
+ "pill-filled": { size: 44, appearance: "filled", shape: "pill" },
60
+ "pill-plain": { size: 48, appearance: "plain", shape: "pill" },
61
+ "pill-small": { size: 32, appearance: "plain", shape: "pill" },
62
+ "icon-outline-sm": { size: 32, appearance: "outline", shape: "icon" },
63
+ "icon-on-content": { size: 24, appearance: "on-content", shape: "icon" },
64
+ "icon-outline-lg": { size: 48, appearance: "outline", shape: "icon" },
65
+ // Compact bare action: reuse the transparent/default and surface-hover ghost treatment.
66
+ "icon-bare-xs": { size: 24, appearance: "ghost", shape: "icon" },
67
+ "icon-ghost-sm": { size: 32, appearance: "ghost", shape: "icon" },
68
+ "icon-ghost-lg": { size: 48, appearance: "ghost", shape: "icon" },
69
+ "icon-circle-sm": { size: 32, appearance: "filled", shape: "circle" },
70
+ "icon-circle-lg": { size: 44, appearance: "filled", shape: "circle" }
71
+ };
72
+ }
73
+ });
74
+
75
+ // packages/ui-system/dist/button.js
76
+ import { jsx as _jsx2, jsxs as _jsxs } from "react/jsx-runtime";
77
+ function ButtonFrame({ label, variant, loading = false, width, children, disabled, type = "button", style, className, ...props }) {
78
+ const spec = buttonVariants[variant];
79
+ const buttonStyle = "width" in spec ? { ...style, "--vx-button-width": `${spec.width}px` } : style;
80
+ return _jsxs("button", { ...props, type, className: className ? `vx-button ${className}` : "vx-button", "data-variant": variant, "data-appearance": spec.appearance, "data-shape": spec.shape, "data-size": spec.size, "data-width": width, "data-state": loading ? "loading" : disabled ? "disabled" : "default", style: buttonStyle, disabled: disabled || loading, "aria-label": label, "aria-busy": loading || void 0, children: [_jsx2("span", { className: "vx-button-content", children }), loading && _jsx2("span", { className: "vx-button-loading", "aria-hidden": "true" })] });
81
+ }
82
+ function Button({ variant = "text-filled-sm", leadingIcon, trailingIcon, ...props }) {
83
+ return _jsxs(ButtonFrame, { ...props, variant, children: [leadingIcon && _jsx2(Icon, { name: leadingIcon }), _jsx2("span", { className: "vx-button-label", children: props.label }), trailingIcon && _jsx2("span", { className: "vx-button-trailing", children: _jsx2(Icon, { name: trailingIcon }) })] });
84
+ }
85
+ var init_button = __esm({
86
+ "packages/ui-system/dist/button.js"() {
87
+ "use strict";
88
+ init_icon();
89
+ init_button_variants();
90
+ }
91
+ });
92
+
93
+ // packages/ui-system/dist/dropdown.js
94
+ import { jsx as _jsx3, jsxs as _jsxs2 } from "react/jsx-runtime";
95
+ import { useEffect, useId, useRef, useState } from "react";
96
+ function Dropdown({ label, options, value, onValueChange, placeholder = "Select an option", size = "medium", width = "fixed", disabled = false }) {
97
+ const id = useId();
98
+ const activeOption = useRef(null);
99
+ const search = useRef({ query: "", time: 0 });
100
+ const [open, setOpen] = useState(false);
101
+ const [highlighted, setHighlighted] = useState(null);
102
+ const enabled = options.filter((option) => !option.disabled);
103
+ const canOpen = !disabled && enabled.length > 0;
104
+ const expanded = canOpen && open;
105
+ const selected = options.find((option) => option.value === value);
106
+ const active = enabled.find((option) => option.value === highlighted) ?? enabled.find((option) => option.value === value) ?? enabled[0];
107
+ const activeIndex = active ? enabled.indexOf(active) : -1;
108
+ useEffect(() => {
109
+ if (expanded)
110
+ activeOption.current?.scrollIntoView({ block: "nearest" });
111
+ }, [expanded, active?.value]);
112
+ useEffect(() => {
113
+ if (!canOpen) {
114
+ setOpen(false);
115
+ setHighlighted(null);
116
+ }
117
+ }, [canOpen]);
118
+ function close() {
119
+ search.current = { query: "", time: 0 };
120
+ setHighlighted(null);
121
+ setOpen(false);
122
+ }
123
+ function choose(option) {
124
+ close();
125
+ if (option && !option.disabled && option.value !== value)
126
+ onValueChange(option.value);
127
+ }
128
+ function handleKey(event) {
129
+ if (!canOpen || event.nativeEvent.isComposing)
130
+ return;
131
+ const { key } = event;
132
+ if (key === "Tab") {
133
+ if (expanded)
134
+ choose(active);
135
+ return;
136
+ }
137
+ if (key === "Escape") {
138
+ if (!expanded)
139
+ return;
140
+ event.preventDefault();
141
+ event.stopPropagation();
142
+ close();
143
+ return;
144
+ }
145
+ if (key === " " || key === "Enter") {
146
+ event.preventDefault();
147
+ if (expanded)
148
+ choose(active);
149
+ else
150
+ setOpen(true);
151
+ return;
152
+ }
153
+ if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
154
+ event.preventDefault();
155
+ if (event.altKey && key === "ArrowUp" && expanded) {
156
+ choose(active);
157
+ return;
158
+ }
159
+ setOpen(true);
160
+ let index = activeIndex;
161
+ if (key === "Home")
162
+ index = 0;
163
+ else if (key === "End")
164
+ index = enabled.length - 1;
165
+ else if (expanded && !event.altKey) {
166
+ const step = key === "ArrowDown" ? 1 : -1;
167
+ index = Math.max(0, Math.min(activeIndex + step, enabled.length - 1));
168
+ }
169
+ setHighlighted(enabled[index]?.value ?? null);
170
+ return;
171
+ }
172
+ if (key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey)
173
+ return;
174
+ event.preventDefault();
175
+ const char = key.toLocaleLowerCase();
176
+ const previous = performance.now() - search.current.time < 500 ? search.current.query : "";
177
+ const repeated = previous.length > 0 && [...previous].every((letter) => letter === char);
178
+ const query = repeated ? char : previous + char;
179
+ search.current = { query, time: performance.now() };
180
+ const start = previous && !repeated ? activeIndex : activeIndex + 1;
181
+ const ordered = [...enabled.slice(start), ...enabled.slice(0, start)];
182
+ const match = ordered.find((option) => option.label.toLocaleLowerCase().startsWith(query));
183
+ setOpen(true);
184
+ if (match)
185
+ setHighlighted(match.value);
186
+ }
187
+ return _jsxs2("div", { className: "vx-dropdown", "data-size": size, "data-width": width, children: [_jsxs2("button", { type: "button", role: "combobox", className: "vx-dropdown-trigger", "aria-label": label, "aria-expanded": expanded, "aria-controls": `${id}-listbox`, "aria-activedescendant": expanded && active ? `${id}-option-${options.indexOf(active)}` : void 0, disabled: !canOpen, onClick: () => expanded ? close() : setOpen(true), onBlur: close, onKeyDown: handleKey, children: [_jsxs2("span", { className: "vx-dropdown-value", children: [selected?.icon && _jsx3(Icon, { name: selected.icon }), _jsx3("span", { children: selected?.label ?? placeholder })] }), _jsx3(Icon, { name: expanded ? "chevron-up" : "chevron-down" })] }), _jsx3("ul", { id: `${id}-listbox`, role: "listbox", "aria-label": label, hidden: !expanded, children: options.map((option, index) => _jsxs2("li", { id: `${id}-option-${index}`, ref: active === option ? activeOption : void 0, role: "option", "aria-selected": option.value === value, "aria-disabled": option.disabled || void 0, "data-active": active === option, onMouseDown: (event) => event.preventDefault(), onClick: () => {
188
+ if (!option.disabled)
189
+ choose(option);
190
+ }, children: [option.icon && _jsx3(Icon, { name: option.icon }), option.label, width === "hug" && option.value === value && _jsx3(Icon, { name: "check" })] }, option.value)) })] });
191
+ }
192
+ var init_dropdown = __esm({
193
+ "packages/ui-system/dist/dropdown.js"() {
194
+ "use strict";
195
+ init_icon();
196
+ }
197
+ });
198
+
199
+ // packages/ui-system/dist/segmented-control.js
200
+ import { jsx as _jsx4, jsxs as _jsxs3 } from "react/jsx-runtime";
201
+ import { useId as useId2 } from "react";
202
+ function SegmentedControl({ label, items, value, onValueChange, size = "medium", width = "fixed", iconOnly = false }) {
203
+ const id = useId2();
204
+ return _jsx4("div", { role: "radiogroup", "aria-label": label, className: "vx-segment", "data-size": size, "data-width": width, "data-icon-only": iconOnly, children: items.map((item) => _jsxs3("label", { children: [_jsx4("input", { type: "radio", name: id, value: item.value, disabled: item.disabled, checked: item.value === value, "aria-label": item.label, onChange: () => onValueChange(item.value) }), _jsxs3("span", { className: "vx-segment-item", children: [item.icon && _jsx4(Icon, { name: item.icon }), (!iconOnly || !item.icon) && item.label] })] }, item.value)) });
205
+ }
206
+ var init_segmented_control = __esm({
207
+ "packages/ui-system/dist/segmented-control.js"() {
208
+ "use strict";
209
+ init_icon();
210
+ }
211
+ });
212
+
213
+ // packages/ui-system/dist/input.js
214
+ import { jsx as _jsx5, jsxs as _jsxs4 } from "react/jsx-runtime";
215
+ function TextInput({ label, value, onValueChange, size = "medium", ...props }) {
216
+ return _jsxs4("label", { className: "vx-field", "data-size": size, children: [_jsx5("span", { children: label }), _jsx5("input", { ...props, type: "text", value, onChange: (event) => onValueChange(event.currentTarget.value) })] });
217
+ }
218
+ var init_input = __esm({
219
+ "packages/ui-system/dist/input.js"() {
220
+ "use strict";
221
+ }
222
+ });
223
+
224
+ // packages/ui-system/dist/index.js
225
+ var init_dist = __esm({
226
+ "packages/ui-system/dist/index.js"() {
227
+ "use strict";
228
+ init_button();
229
+ init_dropdown();
230
+ init_segmented_control();
231
+ init_input();
232
+ }
233
+ });
234
+
235
+ // packages/project-studio/src/ui/reference-picker.tsx
236
+ import * as React from "react";
237
+ function referenceRoles(model, operation) {
238
+ if (model?.modality === "image" && model.requires_explicit_source_roles === false && Array.isArray(model.source_roles) && model.source_roles.length === 0 && operation === "image.edit" && model.operations?.some((o) => o.name === operation)) {
239
+ return [
240
+ {
241
+ role: "implicit-image",
242
+ kind: "image",
243
+ min_items: 1,
244
+ max_items: 1,
245
+ supports_weight: false,
246
+ derived: true
247
+ }
248
+ ];
249
+ }
250
+ return model?.source_roles || [];
251
+ }
252
+ function roleLabel(role) {
253
+ const labels = {
254
+ "implicit-image": "Reference image",
255
+ "first-frame": "First frame",
256
+ "last-frame": "Last frame",
257
+ reference: "Reference file",
258
+ "reference-image": "Reference image",
259
+ "reference-video": "Reference video",
260
+ "reference-audio": "Reference audio",
261
+ style: "Style reference"
262
+ };
263
+ return labels[role.replaceAll("_", "-")] || role.replaceAll("_", " ").replaceAll("-", " ");
264
+ }
265
+ function Thumbnail({ bridge, projectId, studio, asset, version }) {
266
+ const [url, setUrl] = React.useState("");
267
+ const [error, setError] = React.useState("");
268
+ React.useEffect(() => {
269
+ let current = true;
270
+ setUrl("");
271
+ setError("");
272
+ if (asset && version)
273
+ bridge.project(projectId, studio, "files.resolveLocation", {
274
+ ref: { storeId: asset.storeId, objectId: asset.objectId, versionId: version.versionId }
275
+ }).then((value) => {
276
+ if (current) setUrl(value.previewUrl);
277
+ }).catch((e) => {
278
+ if (current) setError(e.message);
279
+ });
280
+ return () => {
281
+ current = false;
282
+ };
283
+ }, [projectId, studio, asset?.objectId, version?.versionId]);
284
+ if (!url) return /* @__PURE__ */ React.createElement("span", { className: "sub" }, error || "Loading\u2026");
285
+ if (asset.data.kind === "image") return /* @__PURE__ */ React.createElement("img", { src: url, alt: asset.data.name });
286
+ if (asset.data.kind === "video")
287
+ return /* @__PURE__ */ React.createElement("video", { src: url, preload: "metadata", muted: true, "aria-label": asset.data.name });
288
+ return /* @__PURE__ */ React.createElement("span", { className: "sub" }, asset.data.kind === "audio" ? "\u266A Audio" : asset.data.name);
289
+ }
290
+ function ReferenceInputs({
291
+ bridge,
292
+ projectId,
293
+ studio,
294
+ capability,
295
+ draft,
296
+ changeDraft,
297
+ attempt,
298
+ onInputKinds
299
+ }) {
300
+ const [assets, setAssets] = React.useState([]);
301
+ const [search, setSearch] = React.useState("");
302
+ const [nextOffset, setNextOffset] = React.useState(null);
303
+ const uploadInput = React.useRef(null);
304
+ const [picker, setPicker] = React.useState(false);
305
+ const [editing, setEditing] = React.useState(null);
306
+ const [assetId, setAssetId] = React.useState("");
307
+ const [versionId, setVersionId] = React.useState("");
308
+ const [role, setRole] = React.useState("");
309
+ const [fixedRole, setFixedRole] = React.useState("");
310
+ const sequence = React.useRef(0);
311
+ const scope = React.useRef("");
312
+ const scopeKey = JSON.stringify([projectId, studio, search]);
313
+ scope.current = scopeKey;
314
+ const load = async (offset = 0) => {
315
+ const current = ++sequence.current;
316
+ const page = await bridge.project(projectId, studio, "assets.list", {
317
+ search,
318
+ limit: 50,
319
+ offset
320
+ });
321
+ if (sequence.current !== current || scope.current !== scopeKey) return;
322
+ setAssets(
323
+ (old) => offset ? [...old, ...page.items.filter((a) => !old.some((x) => x.objectId === a.objectId))] : page.items
324
+ );
325
+ setNextOffset(page.nextOffset);
326
+ };
327
+ React.useEffect(() => {
328
+ void attempt(() => load());
329
+ }, [projectId, studio, search]);
330
+ let inputs = [], error = "";
331
+ try {
332
+ inputs = JSON.parse(draft.inputs);
333
+ if (!Array.isArray(inputs)) throw Error("Reference file could not be read. Select it again.");
334
+ } catch (e) {
335
+ error = e.message;
336
+ }
337
+ const referenceOperation = capability?.modality === "image" && capability.operations?.some((operation) => operation.name === "image.edit") ? "image.edit" : draft.capabilityId;
338
+ const roles = referenceRoles(capability, referenceOperation);
339
+ const asset = assets.find((a) => a.objectId === assetId);
340
+ const compatibleRoles = roles.filter(
341
+ (r) => kinds(r).includes(asset?.data.kind) && (!fixedRole || roleId(r) === fixedRole)
342
+ );
343
+ const selectedRole = compatibleRoles.find((r) => roleId(r) === role);
344
+ React.useEffect(() => {
345
+ if (asset && compatibleRoles.length === 1 && role !== roleId(compatibleRoles[0]))
346
+ setRole(roleId(compatibleRoles[0]));
347
+ }, [assetId, capability, fixedRole, role]);
348
+ React.useEffect(() => {
349
+ if (error) return;
350
+ let current = true;
351
+ const missing = [...new Set(inputs.map((i) => i.ref?.objectId))].filter(
352
+ (id) => id && !assets.some((a) => a.objectId === id)
353
+ );
354
+ if (missing.length)
355
+ void attempt(async () => {
356
+ const records = await Promise.all(
357
+ missing.map((id) => bridge.project(projectId, studio, "assets.get", { assetId: id }))
358
+ );
359
+ if (current)
360
+ setAssets((old) => [
361
+ ...old,
362
+ ...records.filter((a) => !old.some((x) => x.objectId === a.objectId))
363
+ ]);
364
+ });
365
+ onInputKinds(inputs.map((i) => assets.find((a) => a.objectId === i.ref?.objectId)?.data.kind));
366
+ return () => {
367
+ current = false;
368
+ };
369
+ }, [projectId, studio, assets, draft.inputs]);
370
+ const update = (value) => changeDraft({
371
+ inputs: JSON.stringify(
372
+ value.map((item, order) => ({ ...item, order })),
373
+ null,
374
+ 2
375
+ )
376
+ });
377
+ const open = (targetRole = "", index = null) => {
378
+ const existing = index === null ? null : inputs[index];
379
+ setEditing(index);
380
+ setFixedRole(targetRole);
381
+ setRole(existing?.role || targetRole);
382
+ setAssetId(existing?.ref?.objectId || "");
383
+ setVersionId(existing?.ref?.versionId || "");
384
+ setPicker(true);
385
+ };
386
+ const pick = (record) => {
387
+ setAssetId(record.objectId);
388
+ setVersionId(record.versions.at(-1).versionId);
389
+ const choices = roles.filter(
390
+ (r) => kinds(r).includes(record.data.kind) && (!fixedRole || roleId(r) === fixedRole)
391
+ );
392
+ setRole(choices.length === 1 ? roleId(choices[0]) : "");
393
+ };
394
+ const count = inputs.filter((item, index) => index !== editing && item.role === role).length;
395
+ const atLimit = selectedRole && selectedRole.max_items != null && count >= selectedRole.max_items;
396
+ const apply = () => void attempt(async () => {
397
+ if (!asset || !versionId || !selectedRole || atLimit) return;
398
+ const ref2 = { storeId: asset.storeId, objectId: asset.objectId, versionId };
399
+ await bridge.project(projectId, studio, "files.resolveLocation", { ref: ref2 });
400
+ const value = { ...editing === null ? { weight: null } : inputs[editing], ref: ref2, role };
401
+ update(
402
+ editing === null ? [...inputs, value] : inputs.map((item, i) => i === editing ? value : item)
403
+ );
404
+ setPicker(false);
405
+ });
406
+ const upload = (files) => {
407
+ if (!files?.length) return;
408
+ const file = files[0];
409
+ void attempt(async () => {
410
+ const staged = await bridge.stage(file, projectId);
411
+ const created = await bridge.project(
412
+ projectId,
413
+ studio,
414
+ "assets.import",
415
+ {
416
+ name: file.name,
417
+ stagingFileIds: [staged.stagingFileId]
418
+ },
419
+ { requestKey: crypto.randomUUID() }
420
+ );
421
+ const record = await bridge.project(projectId, studio, "assets.get", {
422
+ assetId: created.asset.objectId
423
+ });
424
+ setAssets((old) => [record, ...old.filter((a) => a.objectId !== record.objectId)]);
425
+ setSearch("");
426
+ pick(record);
427
+ });
428
+ };
429
+ const tile = (input, index) => {
430
+ const record = assets.find((a) => a.objectId === input.ref?.objectId);
431
+ const version = record?.versions.find((v) => v.versionId === input.ref?.versionId);
432
+ const declared = roles.find((r) => roleId(r) === input.role);
433
+ return /* @__PURE__ */ React.createElement("div", { className: "reference-tile", key: index }, /* @__PURE__ */ React.createElement(
434
+ "button",
435
+ {
436
+ className: "reference-thumb",
437
+ "aria-label": `Replace Reference file ${index + 1}`,
438
+ onClick: () => open("", index)
439
+ },
440
+ /* @__PURE__ */ React.createElement(
441
+ Thumbnail,
442
+ {
443
+ bridge,
444
+ projectId,
445
+ studio,
446
+ asset: record,
447
+ version
448
+ }
449
+ )
450
+ ), /* @__PURE__ */ React.createElement(
451
+ "button",
452
+ {
453
+ className: "reference-remove",
454
+ "aria-label": `Remove Reference file ${index + 1}`,
455
+ onClick: () => update(inputs.filter((_, i) => i !== index))
456
+ },
457
+ "\xD7"
458
+ ), /* @__PURE__ */ React.createElement("div", { className: "reference-caption" }, /* @__PURE__ */ React.createElement("strong", null, record?.data.name || "Reference file"), /* @__PURE__ */ React.createElement(
459
+ "button",
460
+ {
461
+ className: "plain version-button",
462
+ "aria-label": `Reference file ${index + 1} version`,
463
+ onClick: () => open("", index)
464
+ },
465
+ version ? `v${version.versionNumber}` : "Version",
466
+ " \xB7 ",
467
+ roleLabel(input.role)
468
+ )), !declared && /* @__PURE__ */ React.createElement("p", { className: "reference-warning" }, "Choose a reference supported by this model."), declared?.supports_weight && /* @__PURE__ */ React.createElement("label", { className: "small" }, "Reference weight", /* @__PURE__ */ React.createElement(
469
+ "input",
470
+ {
471
+ type: "number",
472
+ step: "any",
473
+ "aria-label": `Reference file ${index + 1} weight`,
474
+ value: input.weight ?? "",
475
+ onChange: (e) => update(
476
+ inputs.map(
477
+ (r, i) => i === index ? { ...r, weight: e.target.value === "" ? null : Number(e.target.value) } : r
478
+ )
479
+ )
480
+ }
481
+ )), inputs.length > 1 && /* @__PURE__ */ React.createElement("div", { className: "reference-actions" }, /* @__PURE__ */ React.createElement(
482
+ "button",
483
+ {
484
+ disabled: index === 0,
485
+ onClick: () => {
486
+ const value = [...inputs];
487
+ [value[index - 1], value[index]] = [value[index], value[index - 1]];
488
+ update(value);
489
+ }
490
+ },
491
+ "Move earlier"
492
+ ), /* @__PURE__ */ React.createElement(
493
+ "button",
494
+ {
495
+ disabled: index === inputs.length - 1,
496
+ onClick: () => {
497
+ const value = [...inputs];
498
+ [value[index + 1], value[index]] = [value[index], value[index + 1]];
499
+ update(value);
500
+ }
501
+ },
502
+ "Move later"
503
+ )));
504
+ };
505
+ if (!roles.length && !inputs.length) return null;
506
+ const frames = roles.filter(frameRole);
507
+ return /* @__PURE__ */ React.createElement("section", { className: "reference-section" }, /* @__PURE__ */ React.createElement("div", { className: "reference-heading" }, /* @__PURE__ */ React.createElement("h3", null, "Reference file"), /* @__PURE__ */ React.createElement("span", { className: "sub" }, "Added ", inputs.length)), error ? /* @__PURE__ */ React.createElement("p", { role: "alert", className: "error" }, error) : /* @__PURE__ */ React.createElement(React.Fragment, null, frames.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "reference-slot-group" }, frames.map((r) => {
508
+ const id = roleId(r), index = inputs.findIndex((item) => item.role === id);
509
+ return /* @__PURE__ */ React.createElement("div", { className: "reference-slot", key: id }, /* @__PURE__ */ React.createElement("span", null, roleLabel(id), !r.min_items ? " (optional)" : ""), index >= 0 ? tile(inputs[index], index) : /* @__PURE__ */ React.createElement(
510
+ "button",
511
+ {
512
+ className: "reference-add",
513
+ "aria-label": `Choose ${roleLabel(id)}`,
514
+ onClick: () => open(id)
515
+ },
516
+ /* @__PURE__ */ React.createElement("b", null, "+"),
517
+ "Add file"
518
+ ));
519
+ })), /* @__PURE__ */ React.createElement("div", { className: "reference-strip" }, inputs.map(
520
+ (input, index) => frames.some((r) => roleId(r) === input.role) ? null : tile(input, index)
521
+ ), (roles.some((r) => !frameRole(r)) || !roles.length) && /* @__PURE__ */ React.createElement("button", { className: "reference-add", disabled: !roles.length, onClick: () => open() }, /* @__PURE__ */ React.createElement("b", null, "+"), "Add reference")), /* @__PURE__ */ React.createElement("p", { className: "reference-help" }, "Choose project files or upload from your computer. Select a thumbnail to replace it.")), picker && /* @__PURE__ */ React.createElement(
522
+ "div",
523
+ {
524
+ className: "reference-picker-backdrop",
525
+ onKeyDown: (e) => {
526
+ if (e.key === "Escape") setPicker(false);
527
+ }
528
+ },
529
+ /* @__PURE__ */ React.createElement(
530
+ "section",
531
+ {
532
+ className: "reference-picker",
533
+ role: "dialog",
534
+ "aria-modal": "true",
535
+ "aria-label": "Choose Reference file"
536
+ },
537
+ /* @__PURE__ */ React.createElement("div", { className: "reference-picker-header" }, /* @__PURE__ */ React.createElement("h2", null, editing === null ? "Add reference" : "Replace Reference file"), /* @__PURE__ */ React.createElement(
538
+ Button,
539
+ {
540
+ label: "Close",
541
+ variant: "text-ghost-md",
542
+ width: "hug",
543
+ onClick: () => setPicker(false)
544
+ }
545
+ )),
546
+ /* @__PURE__ */ React.createElement("div", { className: "reference-picker-toolbar" }, /* @__PURE__ */ React.createElement(
547
+ TextInput,
548
+ {
549
+ label: "Search project files",
550
+ placeholder: "Search file names",
551
+ value: search,
552
+ onValueChange: setSearch
553
+ }
554
+ ), /* @__PURE__ */ React.createElement(
555
+ Button,
556
+ {
557
+ label: "Upload file",
558
+ variant: "text-tonal-md",
559
+ width: "hug",
560
+ onClick: () => uploadInput.current?.click()
561
+ }
562
+ ), /* @__PURE__ */ React.createElement(
563
+ "input",
564
+ {
565
+ ref: uploadInput,
566
+ hidden: true,
567
+ type: "file",
568
+ accept: "image/*,video/*,audio/*",
569
+ onChange: (e) => upload(e.target.files)
570
+ }
571
+ )),
572
+ /* @__PURE__ */ React.createElement("div", { className: "reference-picker-grid" }, assets.filter(
573
+ (a) => a.versions?.length && roles.some(
574
+ (r) => kinds(r).includes(a.data.kind) && (!fixedRole || roleId(r) === fixedRole)
575
+ )
576
+ ).map((a) => /* @__PURE__ */ React.createElement(
577
+ "button",
578
+ {
579
+ key: a.objectId,
580
+ className: `reference-picker-card ${assetId === a.objectId ? "chosen" : ""}`,
581
+ onClick: () => pick(a)
582
+ },
583
+ /* @__PURE__ */ React.createElement("div", { className: "reference-thumb" }, /* @__PURE__ */ React.createElement(
584
+ Thumbnail,
585
+ {
586
+ bridge,
587
+ projectId,
588
+ studio,
589
+ asset: a,
590
+ version: a.versions.at(-1)
591
+ }
592
+ )),
593
+ /* @__PURE__ */ React.createElement("strong", null, a.data.name),
594
+ /* @__PURE__ */ React.createElement("small", null, a.data.kind, " \xB7 v", a.versions.at(-1).versionNumber)
595
+ ))),
596
+ !assets.some(
597
+ (a) => a.versions?.length && roles.some(
598
+ (r) => kinds(r).includes(a.data.kind) && (!fixedRole || roleId(r) === fixedRole)
599
+ )
600
+ ) && /* @__PURE__ */ React.createElement("p", { className: "reference-picker-empty" }, "No matching files. Upload a file from your computer."),
601
+ nextOffset !== null && /* @__PURE__ */ React.createElement("button", { onClick: () => void attempt(() => load(nextOffset)) }, "Load more files"),
602
+ asset && /* @__PURE__ */ React.createElement("div", { className: "reference-picker-selection" }, /* @__PURE__ */ React.createElement("label", null, "File version", /* @__PURE__ */ React.createElement(
603
+ Dropdown,
604
+ {
605
+ label: "File version",
606
+ value: versionId,
607
+ onValueChange: setVersionId,
608
+ options: asset.versions.map((v) => ({
609
+ value: v.versionId,
610
+ label: `v${v.versionNumber}${v.versionId === asset.versions.at(-1)?.versionId ? " \xB7 Latest" : ""}`
611
+ }))
612
+ }
613
+ )), compatibleRoles.length > 1 && /* @__PURE__ */ React.createElement("label", null, "Reference purpose", /* @__PURE__ */ React.createElement(
614
+ Dropdown,
615
+ {
616
+ label: "Reference purpose",
617
+ value: role,
618
+ onValueChange: setRole,
619
+ options: [
620
+ { value: "", label: "Choose purpose" },
621
+ ...compatibleRoles.map((r) => ({
622
+ value: roleId(r),
623
+ label: roleLabel(roleId(r))
624
+ }))
625
+ ]
626
+ }
627
+ ))),
628
+ atLimit && /* @__PURE__ */ React.createElement("p", { className: "reference-warning" }, "This purpose supports up to ", selectedRole.max_items, " files. Replace an existing reference."),
629
+ /* @__PURE__ */ React.createElement("div", { className: "reference-picker-footer" }, /* @__PURE__ */ React.createElement("span", { className: "sub" }, "This fixed version will be used as the reference."), /* @__PURE__ */ React.createElement(
630
+ Button,
631
+ {
632
+ label: editing === null ? "Use file" : "Use version",
633
+ variant: "text-filled-md",
634
+ width: "hug",
635
+ disabled: !asset || !versionId || !selectedRole || atLimit || Boolean(error),
636
+ onClick: apply
637
+ }
638
+ ))
639
+ )
640
+ ));
641
+ }
642
+ var roleId, kinds, frameRole;
643
+ var init_reference_picker = __esm({
644
+ "packages/project-studio/src/ui/reference-picker.tsx"() {
645
+ "use strict";
646
+ init_dist();
647
+ roleId = (role) => role.role || role.name;
648
+ kinds = (role) => role.kind ? [role.kind] : role.media_types || role.media_kinds || role.kinds || role.accepted_kinds || [];
649
+ frameRole = (role) => ["first-frame", "last-frame"].includes(roleId(role)?.replaceAll("_", "-"));
650
+ }
651
+ });
652
+
653
+ // packages/project-studio/src/ui/generation-inputs.tsx
654
+ import * as React2 from "react";
655
+ function TypedParameters({ capability, draft, changeDraft, inputKinds }) {
656
+ let parameters, inputs;
657
+ try {
658
+ parameters = JSON.parse(draft.parameters);
659
+ inputs = JSON.parse(draft.inputs);
660
+ } catch {
661
+ return /* @__PURE__ */ React2.createElement("p", { className: "reference-warning" }, "Draft settings could not be read. Choose the model again.");
662
+ }
663
+ if (!parameters || typeof parameters !== "object" || Array.isArray(parameters) || !Array.isArray(inputs))
664
+ return /* @__PURE__ */ React2.createElement("p", { className: "reference-warning" }, "Choose the model again to restore generation settings.");
665
+ const mode = inputKinds.includes("video") ? "video" : inputKinds.includes("image") ? "image" : inputKinds.includes("audio") ? "audio" : "text";
666
+ const operation = capability?.operations?.find((o) => o.name === draft.capabilityId);
667
+ const all = (operation?.params || []).filter(
668
+ (p) => names[p.name] && ["string", "number", "integer", "boolean"].includes(p.type) && (!p.input_modes || p.input_modes.includes(mode))
669
+ );
670
+ const definitions = all.filter(
671
+ (p) => p.name !== "size" || !all.some((v) => v.name === "aspect-ratio")
672
+ );
673
+ const common = definitions.filter(
674
+ (p) => ["aspect-ratio", "quality", "resolution", "duration"].includes(p.name) || p.required
675
+ );
676
+ const advanced = definitions.filter((p) => !common.includes(p));
677
+ const set = (name, value) => {
678
+ const next = { ...parameters };
679
+ if (value === void 0) delete next[name];
680
+ else next[name] = value;
681
+ changeDraft({ parameters: JSON.stringify(next, null, 2) });
682
+ };
683
+ const field = (p) => {
684
+ const value = parameters[p.name] ?? p.default;
685
+ const choices = p.enum || (p.type === "boolean" ? [true, false] : null);
686
+ const label = names[p.name];
687
+ return /* @__PURE__ */ React2.createElement("div", { className: "vx-field", key: p.name }, /* @__PURE__ */ React2.createElement("span", null, label, p.required ? " *" : ""), choices ? /* @__PURE__ */ React2.createElement(
688
+ Dropdown,
689
+ {
690
+ label,
691
+ value: value === void 0 ? null : JSON.stringify(coerce(p, value)),
692
+ placeholder: p.default !== void 0 ? "Default" : "Automatic",
693
+ options: [
694
+ { value: "", label: p.default !== void 0 ? "Default" : "Automatic" },
695
+ ...choices.map((v) => ({
696
+ value: JSON.stringify(v),
697
+ label: choiceLabels[String(v)] || String(v)
698
+ }))
699
+ ],
700
+ onValueChange: (value2) => set(p.name, value2 === "" ? void 0 : JSON.parse(value2))
701
+ }
702
+ ) : ["number", "integer"].includes(p.type) ? /* @__PURE__ */ React2.createElement(
703
+ "input",
704
+ {
705
+ "aria-label": label,
706
+ type: "number",
707
+ min: p.min,
708
+ max: p.max,
709
+ step: p.type === "integer" ? 1 : "any",
710
+ value: value ?? "",
711
+ onChange: (e) => set(p.name, e.target.value === "" ? void 0 : Number(e.target.value))
712
+ }
713
+ ) : /* @__PURE__ */ React2.createElement(
714
+ "input",
715
+ {
716
+ "aria-label": label,
717
+ value: value ?? "",
718
+ minLength: p.min_length,
719
+ maxLength: p.max_length,
720
+ onChange: (e) => set(p.name, e.target.value)
721
+ }
722
+ ));
723
+ };
724
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("div", { className: "generation-settings-grid" }, common.map(field)), advanced.length > 0 && /* @__PURE__ */ React2.createElement("details", { className: "generation-more" }, /* @__PURE__ */ React2.createElement("summary", null, "More settings"), /* @__PURE__ */ React2.createElement("div", { className: "generation-settings-grid" }, advanced.map(field))));
725
+ }
726
+ function coerce(p, value) {
727
+ if (p.type === "integer" || p.type === "number") return Number(value);
728
+ if (p.type === "boolean" && typeof value === "string") return value === "true";
729
+ return value;
730
+ }
731
+ var names, choiceLabels;
732
+ var init_generation_inputs = __esm({
733
+ "packages/project-studio/src/ui/generation-inputs.tsx"() {
734
+ "use strict";
735
+ init_dist();
736
+ init_reference_picker();
737
+ names = {
738
+ "aspect-ratio": "Aspect ratio",
739
+ quality: "Quality",
740
+ resolution: "Resolution",
741
+ duration: "Duration (seconds)",
742
+ size: "Size",
743
+ background: "Background",
744
+ "input-fidelity": "Reference fidelity",
745
+ "output-format": "Output format",
746
+ seed: "Seed",
747
+ "enhance-prompt": "Enhance prompt",
748
+ "camera-fixed": "Fixed camera",
749
+ "voice-id": "Voice ID",
750
+ language: "Language",
751
+ "speech-rate": "Speech rate",
752
+ "pitch-rate": "Pitch",
753
+ volume: "Volume",
754
+ "voice-name": "Voice name",
755
+ prefix: "Voice name",
756
+ "voice-description": "Voice description",
757
+ "preview-text": "Preview text",
758
+ gender: "Gender",
759
+ age: "Age",
760
+ emotion: "Emotion",
761
+ "sample-rate": "Sample rate",
762
+ "bit-rate": "Bit rate",
763
+ speed: "Speech rate",
764
+ "audio-format": "Audio format"
765
+ };
766
+ choiceLabels = {
767
+ low: "Low",
768
+ medium: "Standard",
769
+ high: "High",
770
+ auto: "Automatic",
771
+ transparent: "Transparent",
772
+ opaque: "Opaque",
773
+ standard: "Standard",
774
+ lite: "Fast",
775
+ true: "On",
776
+ false: "Off"
777
+ };
778
+ }
779
+ });
780
+
781
+ // packages/project-studio/src/ads-shared.ts
782
+ import { z } from "vivix-studio/sdk/shared";
783
+ var fixedRef, copy, clipScript, adBrief, adStoryboard;
784
+ var init_ads_shared = __esm({
785
+ "packages/project-studio/src/ads-shared.ts"() {
786
+ "use strict";
787
+ fixedRef = z.object({ storeId: z.uuid(), objectId: z.uuid(), versionId: z.uuid() }).strict();
788
+ copy = z.string().max(8e3).default("");
789
+ clipScript = z.object({
790
+ title: z.string().trim().min(1).max(200),
791
+ visual: copy,
792
+ voiceover: copy,
793
+ onScreenText: copy,
794
+ cta: copy,
795
+ plannedDurationMs: z.number().int().positive().nullable().default(null)
796
+ }).strict();
797
+ adBrief = z.object({
798
+ product: copy,
799
+ category: copy,
800
+ audience: copy,
801
+ sellingPoints: copy,
802
+ channel: copy,
803
+ aspectRatio: z.string().max(40).nullable().default(null),
804
+ targetDurationMs: z.number().int().positive().nullable().default(null),
805
+ body: copy,
806
+ productRefs: z.array(fixedRef).max(100).default([]),
807
+ modelRefs: z.array(fixedRef).max(100).default([])
808
+ }).strict();
809
+ adStoryboard = z.object({
810
+ brief: adBrief,
811
+ clips: z.array(
812
+ z.object({
813
+ assetId: z.uuid(),
814
+ script: clipScript,
815
+ edits: z.array(z.object({ sourceVersionId: z.uuid(), editRef: fixedRef }).strict()).max(200)
816
+ }).strict()
817
+ ).max(200)
818
+ }).strict().superRefine((value, context) => {
819
+ if (new Set(value.clips.map((c) => c.assetId)).size !== value.clips.length)
820
+ context.addIssue({ code: "custom", message: "A clip can appear only once in a storyboard." });
821
+ for (const clip of value.clips)
822
+ if (new Set(clip.edits.map((e) => e.sourceVersionId)).size !== clip.edits.length)
823
+ context.addIssue({
824
+ code: "custom",
825
+ message: "Each candidate has one current edit binding."
826
+ });
827
+ });
828
+ }
829
+ });
830
+
831
+ // packages/project-studio/src/shared.ts
832
+ import { z as z2, defineOperation } from "vivix-studio/sdk/shared";
833
+ function generationOperation(model, kind2, referenceCount, action = "") {
834
+ if (!model || model.modality !== kind2) return "";
835
+ const operations = model.operations || [];
836
+ if (kind2 === "image") {
837
+ const name = referenceCount ? "image.edit" : "image.generate";
838
+ return operations.some((operation) => operation.name === name) ? name : "";
839
+ }
840
+ if (operations.length === 1) return operations[0].name;
841
+ return operations.some((operation) => operation.name === action) ? action : "";
842
+ }
843
+ function generationParameters(model, operation, raw, kinds2) {
844
+ let parameters;
845
+ try {
846
+ parameters = JSON.parse(raw);
847
+ } catch {
848
+ return raw;
849
+ }
850
+ if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return raw;
851
+ const mode = kinds2.includes("video") ? "video" : kinds2.includes("image") ? "image" : kinds2.includes("audio") ? "audio" : "text";
852
+ const declared = model?.operations?.flatMap((item) => item.params || []) || [];
853
+ const active = (model?.operations?.find((item) => item.name === operation)?.params || []).filter((parameter) => !parameter.input_modes || parameter.input_modes.includes(mode));
854
+ return JSON.stringify(
855
+ Object.fromEntries(
856
+ Object.entries(parameters).filter(
857
+ ([name]) => active.some((parameter) => parameter.name === name) || !declared.some((parameter) => parameter.name === name)
858
+ )
859
+ )
860
+ );
861
+ }
862
+ var studioConfig, versionRef, tags, kind, projectData, assetData, anchor, generationInput, projectSettingsInput, generationSubmitOperation, favoriteData, sqlScalar, globalQueryInput;
863
+ var init_shared = __esm({
864
+ "packages/project-studio/src/shared.ts"() {
865
+ "use strict";
866
+ init_cost();
867
+ init_ads_shared();
868
+ studioConfig = z2.object({
869
+ role: z2.enum(["project", "coordinator"]).default("project"),
870
+ dataInstance: z2.string().min(1).default("user:data"),
871
+ apiInstance: z2.string().min(1).default("api"),
872
+ libraryInstance: z2.string().min(1).nullable().default(null)
873
+ }).strict();
874
+ versionRef = z2.object({ storeId: z2.uuid(), objectId: z2.uuid(), versionId: z2.uuid() }).strict();
875
+ tags = z2.array(z2.string().trim().min(1).max(64)).max(50).transform((x) => [...new Set(x)]).default([]);
876
+ kind = z2.enum(["text", "image", "video", "audio", "file"]);
877
+ projectData = z2.object({
878
+ name: z2.string().trim().min(1).max(120),
879
+ description: z2.string().max(4e3).default(""),
880
+ status: z2.enum(["draft", "active", "archived"]).default("draft"),
881
+ aspectRatio: z2.string().nullable().default(null),
882
+ coverRef: versionRef.nullable().default(null),
883
+ briefRef: z2.object({ storeId: z2.uuid(), objectId: z2.uuid() }).strict().nullable().default(null),
884
+ tags
885
+ }).strict();
886
+ assetData = z2.object({
887
+ projectId: z2.uuid(),
888
+ name: z2.string().trim().min(1).max(200),
889
+ kind,
890
+ purpose: z2.enum(["reference", "script", "shot", "music", "voice", "subtitle", "final", "other"]).default("other"),
891
+ origin: z2.enum(["upload", "library", "generated", "authored"]),
892
+ orderKey: z2.number().int().nullable().default(null),
893
+ tags,
894
+ lifecycle: z2.enum(["draft", "ready", "archived"]).default("draft"),
895
+ sourceRef: versionRef.nullable().default(null),
896
+ adoptedVersionId: z2.uuid().nullable().default(null)
897
+ }).strict();
898
+ anchor = z2.discriminatedUnion("kind", [
899
+ z2.object({ kind: z2.literal("whole") }).strict(),
900
+ z2.object({
901
+ kind: z2.literal("timeRange"),
902
+ startMs: z2.number().int().nonnegative(),
903
+ endMs: z2.number().int().nonnegative()
904
+ }).strict()
905
+ ]);
906
+ generationInput = z2.object({
907
+ targetAssetId: z2.uuid().optional(),
908
+ taskName: z2.string().trim().min(1).max(200).optional(),
909
+ context: z2.record(z2.string().min(1).max(64), z2.string().min(1).max(200)).optional(),
910
+ expectedRevision: z2.number().int().positive().optional(),
911
+ newAsset: z2.object({
912
+ name: z2.string().trim().min(1).max(200),
913
+ kind: z2.enum(["image", "video", "audio"])
914
+ }).strict().optional(),
915
+ modelId: z2.string().min(1),
916
+ capabilityId: z2.string().min(1),
917
+ prompt: z2.string().max(32e3),
918
+ parameters: z2.record(z2.string(), z2.unknown()).default({}),
919
+ outputCount: z2.number().int().min(1).max(100).default(1),
920
+ inputs: z2.array(
921
+ z2.object({
922
+ ref: versionRef,
923
+ role: z2.string().min(1),
924
+ order: z2.number().int().nonnegative(),
925
+ weight: z2.number().finite().nullable().default(null)
926
+ }).strict()
927
+ ).max(100).default([]),
928
+ baseVersionId: z2.uuid().optional(),
929
+ changeRequest: z2.string().trim().min(1).max(4e3).optional(),
930
+ preserveRequest: z2.string().max(4e3).default(""),
931
+ anchor: anchor.default({ kind: "whole" }),
932
+ retryOfTaskId: z2.uuid().optional(),
933
+ acknowledgeUnknown: z2.boolean().optional()
934
+ }).strict();
935
+ projectSettingsInput = z2.object({
936
+ projectId: z2.uuid(),
937
+ expectedRevision: z2.number().int().positive(),
938
+ changes: projectData.pick({ name: true, description: true, tags: true, aspectRatio: true, coverRef: true }).extend({
939
+ description: projectData.shape.description.unwrap(),
940
+ tags: projectData.shape.tags.unwrap(),
941
+ aspectRatio: projectData.shape.aspectRatio.unwrap(),
942
+ coverRef: projectData.shape.coverRef.unwrap()
943
+ }).partial().strict()
944
+ }).strict();
945
+ generationSubmitOperation = defineOperation({
946
+ id: "generation.submit",
947
+ title: "Generate",
948
+ description: "Submit one media generation without approval, retaining its paid request identity.",
949
+ input: generationInput,
950
+ output: z2.any(),
951
+ effects: ["write", "paid"],
952
+ confirm: "none"
953
+ });
954
+ favoriteData = z2.object({ ref: versionRef }).strict();
955
+ sqlScalar = z2.union([z2.string(), z2.number().finite(), z2.null()]);
956
+ globalQueryInput = z2.object({
957
+ sql: z2.string().trim().min(1),
958
+ params: z2.union([z2.array(sqlScalar), z2.record(z2.string(), sqlScalar)]).optional(),
959
+ timeoutMs: z2.number().int().positive().optional(),
960
+ maxRows: z2.number().int().positive().optional(),
961
+ limit: z2.number().int().min(1).max(200).optional(),
962
+ offset: z2.number().int().nonnegative().optional(),
963
+ source: z2.enum(["projects", "assets", "tasks"]),
964
+ catalog: z2.array(z2.object({ id: z2.string().min(1), name: z2.string(), available: z2.boolean() }).strict()).optional()
965
+ }).strict();
966
+ }
967
+ });
968
+
969
+ // packages/project-studio/src/ui/generation-operation.ts
970
+ var init_generation_operation = __esm({
971
+ "packages/project-studio/src/ui/generation-operation.ts"() {
972
+ "use strict";
973
+ init_shared();
974
+ }
975
+ });
976
+
977
+ // packages/project-studio/src/ui/panels.tsx
978
+ import * as React3 from "react";
979
+ function GenerationPanel(props) {
980
+ const {
981
+ bridge,
982
+ projectId,
983
+ api,
984
+ studio,
985
+ selected,
986
+ draft,
987
+ changeDraft,
988
+ models,
989
+ capability,
990
+ busy,
991
+ submitting = busy,
992
+ submitGeneration,
993
+ attempt,
994
+ setCapability,
995
+ shotGeneration = false,
996
+ resultDescription = "Generated results are saved to this project."
997
+ } = props;
998
+ const [inputKinds, setInputKinds] = React3.useState([]);
999
+ const [loadingModel, setLoadingModel] = React3.useState(false);
1000
+ const modelRequest = React3.useRef(0);
1001
+ const statusId = React3.useId();
1002
+ const kind2 = selected?.data.kind || draft.kind;
1003
+ const currentModel = capability?.model === draft.modelId && capability.modality === kind2 ? capability : null;
1004
+ let references = [];
1005
+ let validReferences = true;
1006
+ try {
1007
+ references = JSON.parse(draft.inputs);
1008
+ if (!Array.isArray(references)) throw Error("Invalid references.");
1009
+ } catch {
1010
+ validReferences = false;
1011
+ }
1012
+ const operation = generationOperation(
1013
+ currentModel,
1014
+ kind2,
1015
+ references?.length || 0,
1016
+ draft.capabilityId
1017
+ );
1018
+ const generationDraft = { ...draft, capabilityId: operation };
1019
+ const chooseAction = kind2 !== "image" && currentModel?.operations?.length > 1;
1020
+ const actionLabel = kind2 === "audio" ? "Audio action" : "Video action";
1021
+ const countParameter = currentModel?.operations?.find((o) => o.name === operation)?.params?.find((p) => ["n", "num-outputs", "output-count"].includes(p.name));
1022
+ const minOutputs = countParameter?.min ?? 1;
1023
+ const maxOutputs = countParameter?.max ?? (countParameter ? 100 : 1);
1024
+ const roles = referenceRoles(currentModel, operation);
1025
+ const requiredReference = roles.find((role) => {
1026
+ const id = role.role || role.name;
1027
+ return references.filter((reference) => reference?.role === id).length < (role.min_items || 0);
1028
+ });
1029
+ const modelListed = models.some(
1030
+ (model) => model.model === draft.modelId && model.modality === kind2
1031
+ );
1032
+ const promptLabel = shotGeneration ? kind2 === "video" ? "Video prompt" : "Image prompt" : "Prompt";
1033
+ const imageOperationMessage = references.length ? "This model does not support reference images. Remove them or select another model." : "This model needs a reference image. Add an image or select another model.";
1034
+ const validationMessage = () => {
1035
+ if (!draft.modelId) return "Select a model.";
1036
+ if (loadingModel) return "Loading model settings\u2026";
1037
+ if (!currentModel || !modelListed || currentModel.available === false)
1038
+ return "This model is unavailable. Select another model.";
1039
+ if (!operation)
1040
+ return kind2 === "image" ? imageOperationMessage : "Select an action supported by this model.";
1041
+ if (!draft.prompt?.trim() && kind2 !== "audio") return `Enter a ${kind2} prompt.`;
1042
+ if (!validReferences) return "Select the reference files again.";
1043
+ if (references.some(
1044
+ (reference) => !roles.some((role) => (role.role || role.name) === reference?.role)
1045
+ ))
1046
+ return "Remove or replace the references this model does not support.";
1047
+ if (requiredReference)
1048
+ return `Add the required reference: ${roleLabel(requiredReference.role || requiredReference.name)}.`;
1049
+ if (!Number.isInteger(draft.outputCount) || draft.outputCount < minOutputs || draft.outputCount > maxOutputs)
1050
+ return `Enter an output count from ${minOutputs} to ${maxOutputs}.`;
1051
+ return "";
1052
+ };
1053
+ const blocked = shotGeneration ? validationMessage() : "";
1054
+ const selectModel = (modelId) => {
1055
+ const request = ++modelRequest.current;
1056
+ changeDraft({
1057
+ modelId,
1058
+ capabilityId: "",
1059
+ parameters: "{}",
1060
+ ...shotGeneration && { outputCount: 1 }
1061
+ });
1062
+ setCapability(null);
1063
+ setLoadingModel(Boolean(modelId));
1064
+ if (!modelId) return;
1065
+ void attempt(async () => {
1066
+ try {
1067
+ const details = await bridge.project(projectId, api, "capabilities.get", { modelId });
1068
+ if (modelRequest.current === request) setCapability(details);
1069
+ } catch (error) {
1070
+ if (modelRequest.current === request) throw error;
1071
+ } finally {
1072
+ if (modelRequest.current === request) setLoadingModel(false);
1073
+ }
1074
+ });
1075
+ };
1076
+ return /* @__PURE__ */ React3.createElement("div", { className: "vx-generation-panel" }, /* @__PURE__ */ React3.createElement("p", { className: "vx-generation-hint" }, shotGeneration ? kind2 === "video" ? "Describe the movement, camera, and timing. Review the result before adding it as a candidate." : "Describe the composition, characters, and lighting. Review the result before using it on the storyboard." : "Describe the result you want to create."), !shotGeneration && /* @__PURE__ */ React3.createElement(
1077
+ TextInput,
1078
+ {
1079
+ label: "Task name",
1080
+ value: draft.taskName || "",
1081
+ onValueChange: (taskName) => changeDraft({ taskName })
1082
+ }
1083
+ ), !selected && !shotGeneration && /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
1084
+ TextInput,
1085
+ {
1086
+ label: "File name",
1087
+ value: draft.newName,
1088
+ onValueChange: (newName) => changeDraft({ newName })
1089
+ }
1090
+ ), /* @__PURE__ */ React3.createElement(
1091
+ SegmentedControl,
1092
+ {
1093
+ label: "Creation type",
1094
+ value: draft.kind,
1095
+ onValueChange: (value) => changeDraft({ kind: value }),
1096
+ items: ["image", "video", "audio"].map((value) => ({
1097
+ value,
1098
+ label: { image: "Image", video: "Video", audio: "Audio" }[value]
1099
+ }))
1100
+ }
1101
+ )), /* @__PURE__ */ React3.createElement("div", { className: "vx-field" }, /* @__PURE__ */ React3.createElement("span", null, "Model"), /* @__PURE__ */ React3.createElement(
1102
+ Dropdown,
1103
+ {
1104
+ label: "Model",
1105
+ value: draft.modelId || null,
1106
+ placeholder: "Choose a model",
1107
+ options: [
1108
+ { value: "", label: "Choose a model" },
1109
+ ...models.filter((m) => m.modality === (selected?.data.kind || draft.kind)).map((m) => ({ value: m.model, label: m.model }))
1110
+ ],
1111
+ onValueChange: selectModel
1112
+ }
1113
+ )), chooseAction && /* @__PURE__ */ React3.createElement("div", { className: "vx-field" }, /* @__PURE__ */ React3.createElement("span", null, actionLabel), /* @__PURE__ */ React3.createElement(
1114
+ Dropdown,
1115
+ {
1116
+ label: actionLabel,
1117
+ value: operation || null,
1118
+ placeholder: "Choose a purpose",
1119
+ options: [
1120
+ { value: "", label: "Choose an action" },
1121
+ ...currentModel.operations.map((item) => ({
1122
+ value: item.name,
1123
+ label: {
1124
+ "voice.generate": "Generate speech",
1125
+ "voice.clone": "Clone voice",
1126
+ "voice.design": "Design voice",
1127
+ "voice.create": "Create voice",
1128
+ "video.generate": "Generate video",
1129
+ "video.edit": "Edit video",
1130
+ "video.extend": "Extend video"
1131
+ }[item.name] || item.name
1132
+ }))
1133
+ ],
1134
+ onValueChange: (capabilityId) => changeDraft({ capabilityId, ...shotGeneration && { outputCount: 1 } })
1135
+ }
1136
+ )), !shotGeneration && currentModel && !operation && kind2 === "image" && /* @__PURE__ */ React3.createElement("p", { className: "reference-warning", role: "alert" }, imageOperationMessage), /* @__PURE__ */ React3.createElement("label", { className: "vx-field" }, /* @__PURE__ */ React3.createElement("span", null, promptLabel), /* @__PURE__ */ React3.createElement(
1137
+ "textarea",
1138
+ {
1139
+ "aria-label": promptLabel,
1140
+ rows: 6,
1141
+ value: draft.prompt,
1142
+ onChange: (e) => changeDraft({ prompt: e.target.value })
1143
+ }
1144
+ )), /* @__PURE__ */ React3.createElement(
1145
+ ReferenceInputs,
1146
+ {
1147
+ bridge,
1148
+ projectId,
1149
+ studio,
1150
+ capability: currentModel,
1151
+ draft: generationDraft,
1152
+ changeDraft,
1153
+ attempt,
1154
+ onInputKinds: setInputKinds
1155
+ }
1156
+ ), /* @__PURE__ */ React3.createElement(
1157
+ TypedParameters,
1158
+ {
1159
+ capability: currentModel,
1160
+ draft: generationDraft,
1161
+ changeDraft,
1162
+ inputKinds
1163
+ }
1164
+ ), (!shotGeneration || countParameter) && /* @__PURE__ */ React3.createElement("label", { className: "vx-field vx-generation-count" }, /* @__PURE__ */ React3.createElement("span", null, "Number of results"), /* @__PURE__ */ React3.createElement(
1165
+ "input",
1166
+ {
1167
+ "aria-label": "Number of results",
1168
+ type: "number",
1169
+ min: minOutputs,
1170
+ max: maxOutputs,
1171
+ disabled: !countParameter,
1172
+ value: draft.outputCount,
1173
+ onChange: (e) => changeDraft({ outputCount: Number(e.target.value) })
1174
+ }
1175
+ )), /* @__PURE__ */ React3.createElement("div", { className: "vx-generation-submit" }, /* @__PURE__ */ React3.createElement("p", { className: "vx-generation-hint", id: statusId, role: blocked ? "status" : void 0 }, blocked || resultDescription), /* @__PURE__ */ React3.createElement(
1176
+ Button,
1177
+ {
1178
+ label: submitting || shotGeneration && busy ? "Submitting\u2026" : "Generate",
1179
+ "aria-describedby": statusId,
1180
+ loading: submitting || shotGeneration && busy,
1181
+ width: "hug",
1182
+ variant: "text-filled-md",
1183
+ disabled: busy || loadingModel || Boolean(blocked) || !draft.modelId || !draft.prompt?.trim() && kind2 !== "audio" || !operation || !validReferences || !currentModel || !models.some(
1184
+ (m) => m.model === draft.modelId && m.modality === (selected?.data.kind || draft.kind)
1185
+ ),
1186
+ onClick: () => void submitGeneration({
1187
+ capabilityId: operation,
1188
+ parameters: generationParameters(
1189
+ currentModel,
1190
+ operation,
1191
+ draft.parameters,
1192
+ inputKinds
1193
+ )
1194
+ })
1195
+ }
1196
+ )));
1197
+ }
1198
+ function ReviewPanel(props) {
1199
+ const {
1200
+ selected,
1201
+ locations,
1202
+ Preview,
1203
+ tasks,
1204
+ attempt,
1205
+ setLocations,
1206
+ projectCall,
1207
+ generation,
1208
+ setSelected,
1209
+ openForm
1210
+ } = props;
1211
+ return selected ? /* @__PURE__ */ React3.createElement("div", { className: "split" }, /* @__PURE__ */ React3.createElement("div", { className: "compare" }, selected.versions.map((v) => /* @__PURE__ */ React3.createElement("section", { className: "candidate", key: v.versionId }, /* @__PURE__ */ React3.createElement("h3", null, "v", v.versionNumber, " ", selected.data.adoptedVersionId === v.versionId ? "\xB7 Adopted" : ""), /* @__PURE__ */ React3.createElement(Preview, { kind: selected.data.kind, location: locations[v.versionId] }), !locations[v.versionId] && /* @__PURE__ */ React3.createElement(
1212
+ "button",
1213
+ {
1214
+ onClick: () => void attempt(
1215
+ async () => setLocations({
1216
+ ...locations,
1217
+ [v.versionId]: await projectCall("files.resolveLocation", {
1218
+ ref: ref(selected, v)
1219
+ })
1220
+ })
1221
+ )
1222
+ },
1223
+ "Load version"
1224
+ ), /* @__PURE__ */ React3.createElement("div", { className: "sub" }, v.payload.durationMs !== null ? v.payload.durationMs + " ms" : "", " \xB7 ", v.origin), /* @__PURE__ */ React3.createElement("div", { className: "actions" }, /* @__PURE__ */ React3.createElement("button", { onClick: () => generation(v) }, "Iterate from v", v.versionNumber), /* @__PURE__ */ React3.createElement(
1225
+ "button",
1226
+ {
1227
+ onClick: () => void attempt(async () => {
1228
+ await projectCall(
1229
+ "choices.adopt",
1230
+ {
1231
+ assetId: selected.objectId,
1232
+ versionId: v.versionId,
1233
+ expectedRevision: selected.revision
1234
+ },
1235
+ { requestKey: freshKey() }
1236
+ );
1237
+ const current = await projectCall("assets.get", {
1238
+ assetId: selected.objectId
1239
+ });
1240
+ setSelected(current);
1241
+ })
1242
+ },
1243
+ "Adopt"
1244
+ ))))), /* @__PURE__ */ React3.createElement("section", null, /* @__PURE__ */ React3.createElement("div", { className: "eyebrow" }, "Comments and iteration"), /* @__PURE__ */ React3.createElement("p", { className: "sub" }, "Choose a fixed version. Enter changes and preservation requirements before submitting."), tasks.filter((t) => t.data.kind === "iterate").map((t) => /* @__PURE__ */ React3.createElement("article", { className: "history", key: t.objectId }, /* @__PURE__ */ React3.createElement("span", { className: "sub" }, t.data.state, " \xB7", " ", t.data.inputSnapshot.anchor.kind === "timeRange" ? `${t.data.inputSnapshot.anchor.startMs / 1e3}${t.data.inputSnapshot.anchor.endMs !== t.data.inputSnapshot.anchor.startMs ? "\u2013" + t.data.inputSnapshot.anchor.endMs / 1e3 : ""} s` : "Whole version"), /* @__PURE__ */ React3.createElement("p", null, t.data.inputSnapshot.changeRequest), /* @__PURE__ */ React3.createElement("p", { className: "sub" }, "Preserve: ", t.data.inputSnapshot.preserveRequest), /* @__PURE__ */ React3.createElement("button", { onClick: () => openForm("Task", t) }, "View task"))))) : /* @__PURE__ */ React3.createElement("div", { className: "empty" }, "Select an asset in the asset table to compare its versions.");
1245
+ }
1246
+ function TaskDetails({ form }) {
1247
+ const snapshot = form.data.inputSnapshot;
1248
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("h3", null, form.data.name || "Generation"), /* @__PURE__ */ React3.createElement("p", { className: "status" }, form.data.state), /* @__PURE__ */ React3.createElement("dl", null, /* @__PURE__ */ React3.createElement("dt", null, "Actual cost"), /* @__PURE__ */ React3.createElement("dd", null, taskCostLabel(form.data)), form.data.cost?.checkedAt && /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("dt", null, "Cost updated"), /* @__PURE__ */ React3.createElement("dd", null, new Date(form.data.cost.checkedAt).toLocaleString())), /* @__PURE__ */ React3.createElement("dt", null, "Model"), /* @__PURE__ */ React3.createElement("dd", null, snapshot.modelId), /* @__PURE__ */ React3.createElement("dt", null, "Prompt"), /* @__PURE__ */ React3.createElement("dd", null, snapshot.originalPrompt), form.projection && /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("dt", null, "Remote result"), /* @__PURE__ */ React3.createElement("dd", null, form.projection.remoteOutcome.status), /* @__PURE__ */ React3.createElement("dt", null, "Local saving"), /* @__PURE__ */ React3.createElement("dd", null, form.projection.localPersistence), /* @__PURE__ */ React3.createElement("dt", null, "Outputs returned"), /* @__PURE__ */ React3.createElement("dd", null, form.projection.remoteOutcome.resultCount ?? "Unknown", " /", " ", form.projection.remoteOutcome.requestedCount ?? "Unknown")), /* @__PURE__ */ React3.createElement("dt", null, "Outputs saved"), /* @__PURE__ */ React3.createElement("dd", null, form.data.receivedOutputs, " / ", form.data.expectedOutputs)), form.data.cost?.error && /* @__PURE__ */ React3.createElement("p", { role: "status" }, form.data.cost.error.message), form.data.error && /* @__PURE__ */ React3.createElement("p", { role: "alert" }, form.data.error.message));
1249
+ }
1250
+ function ref(object, version) {
1251
+ return { storeId: object.storeId, objectId: object.objectId, versionId: version.versionId };
1252
+ }
1253
+ var freshKey;
1254
+ var init_panels = __esm({
1255
+ "packages/project-studio/src/ui/panels.tsx"() {
1256
+ "use strict";
1257
+ init_cost();
1258
+ init_dist();
1259
+ init_generation_inputs();
1260
+ init_generation_operation();
1261
+ init_reference_picker();
1262
+ freshKey = () => crypto.randomUUID();
1263
+ }
1264
+ });
1265
+
1266
+ // packages/ui-system/dist/styles.css
1267
+ var init_styles = __esm({
1268
+ "packages/ui-system/dist/styles.css"() {
1269
+ }
1270
+ });
1271
+
1272
+ // packages/project-studio/src/ui/generation-form.css
1273
+ var init_generation_form = __esm({
1274
+ "packages/project-studio/src/ui/generation-form.css"() {
1275
+ }
1276
+ });
1277
+
1278
+ // packages/project-studio/src/ui/generation-ui.tsx
1279
+ var generation_ui_exports = {};
1280
+ __export(generation_ui_exports, {
1281
+ default: () => GenerationPanel
1282
+ });
1283
+ var init_generation_ui = __esm({
1284
+ "packages/project-studio/src/ui/generation-ui.tsx"() {
1285
+ "use strict";
1286
+ init_styles();
1287
+ init_generation_form();
1288
+ init_panels();
1289
+ }
1290
+ });
1291
+
1292
+ // packages/project-studio/src/client.tsx
1293
+ init_panels();
1294
+ import * as React4 from "react";
1295
+
1296
+ // packages/project-studio/src/property-metadata.ts
1297
+ function isInternalMediaProvenance(value) {
1298
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1299
+ const media = value;
1300
+ const uuid = (value2) => typeof value2 === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value2);
1301
+ if (media.kind !== "video" && media.kind !== "sequence" || !uuid(media.jobId)) return false;
1302
+ if (!Array.isArray(media.inputs) || !media.inputs.length) return false;
1303
+ return media.inputs.every((input) => {
1304
+ if (!input || typeof input !== "object") return false;
1305
+ const { file, recipe } = input;
1306
+ return uuid(file?.fileId) && uuid(file?.source?.storeId) && uuid(file?.source?.objectId) && uuid(file?.source?.versionId) && recipe && typeof recipe === "object" && recipe.trim && recipe.crop;
1307
+ }) && !!media.tools && typeof media.tools === "object" && typeof media.tools.ffmpegVersion === "string" && typeof media.tools.ffprobeVersion === "string";
1308
+ }
1309
+
1310
+ // packages/project-studio/src/client.tsx
1311
+ init_shared();
1312
+ var StyledGenerationPanel = React4.lazy(() => Promise.resolve().then(() => (init_generation_ui(), generation_ui_exports)));
1313
+ function GenerationPanel2(props) {
1314
+ return /* @__PURE__ */ React4.createElement(React4.Suspense, { fallback: /* @__PURE__ */ React4.createElement("p", null, "Loading generation settings\u2026") }, /* @__PURE__ */ React4.createElement(StyledGenerationPanel, { ...props }));
1315
+ }
1316
+ function AssetPreview({ location, kind: kind2 }) {
1317
+ if (!location) return /* @__PURE__ */ React4.createElement("div", { className: "empty-preview" }, "Select a readable version");
1318
+ const url = location.previewUrl;
1319
+ return kind2 === "video" ? /* @__PURE__ */ React4.createElement("video", { controls: true, src: url }) : kind2 === "audio" ? /* @__PURE__ */ React4.createElement("audio", { controls: true, src: url }) : kind2 === "image" ? /* @__PURE__ */ React4.createElement("img", { src: url, alt: "Selected version" }) : kind2 === "text" ? /* @__PURE__ */ React4.createElement(TextPreview, { url }) : /* @__PURE__ */ React4.createElement("a", { href: url }, "Open file");
1320
+ }
1321
+ function TextPreview({ url }) {
1322
+ const [text, setText] = React4.useState("");
1323
+ React4.useEffect(() => {
1324
+ let current = true;
1325
+ fetch(url).then((r) => {
1326
+ if (!r.ok) throw Error("Text is unavailable.");
1327
+ return r.text();
1328
+ }).then((t) => {
1329
+ if (current) setText(t);
1330
+ }).catch((e) => {
1331
+ if (current) setText(e.message);
1332
+ });
1333
+ return () => {
1334
+ current = false;
1335
+ };
1336
+ }, [url]);
1337
+ return /* @__PURE__ */ React4.createElement("pre", { className: "text-preview" }, text);
1338
+ }
1339
+ function AssetProperties({ record }) {
1340
+ return /* @__PURE__ */ React4.createElement("dl", null, /* @__PURE__ */ React4.createElement("dt", null, "Type"), /* @__PURE__ */ React4.createElement("dd", null, record.data.kind), /* @__PURE__ */ React4.createElement("dt", null, "Tags"), /* @__PURE__ */ React4.createElement("dd", null, record.data.tags.map((t) => /* @__PURE__ */ React4.createElement("span", { className: "tag", key: t }, t))), /* @__PURE__ */ React4.createElement("dt", null, "Latest"), /* @__PURE__ */ React4.createElement("dd", null, record.versions?.at(-1)?.versionNumber || "No content"), /* @__PURE__ */ React4.createElement("dt", null, "Adopted"), /* @__PURE__ */ React4.createElement("dd", null, record.versions?.find((v) => v.versionId === record.data.adoptedVersionId)?.versionNumber || "None"), /* @__PURE__ */ React4.createElement("dt", null, "Availability"), /* @__PURE__ */ React4.createElement("dd", null, record.availability), /* @__PURE__ */ React4.createElement("dt", null, "Revision"), /* @__PURE__ */ React4.createElement("dd", null, record.revision), Object.entries(record.metadata).filter(([key, value]) => key !== "media" || !isInternalMediaProvenance(value)).map(([key, value]) => /* @__PURE__ */ React4.createElement(React4.Fragment, { key }, /* @__PURE__ */ React4.createElement("dt", null, key), /* @__PURE__ */ React4.createElement("dd", null, /* @__PURE__ */ React4.createElement("details", null, /* @__PURE__ */ React4.createElement("summary", null, typeof value === "object" ? JSON.stringify(value).slice(0, 60) : String(value)), /* @__PURE__ */ React4.createElement("pre", null, JSON.stringify(value, null, 2)))))));
1341
+ }
1342
+ var dependencies = (config) => [
1343
+ config.apiInstance,
1344
+ config.dataInstance,
1345
+ ...config.libraryInstance ? [config.libraryInstance] : []
1346
+ ];
1347
+ function activate(client) {
1348
+ if (client.config.role === "coordinator") return;
1349
+ let active = true;
1350
+ for (const [id, component] of Object.entries({
1351
+ "studio.generation": GenerationPanel2,
1352
+ "studio.review": ReviewPanel,
1353
+ "studio.task": TaskDetails,
1354
+ "studio.preview": AssetPreview,
1355
+ "studio.properties": AssetProperties
1356
+ }))
1357
+ client.registerUI({
1358
+ id,
1359
+ title: id,
1360
+ kind: "component",
1361
+ component: (props) => active ? React4.createElement(component, props) : null
1362
+ });
1363
+ client.onDispose(() => {
1364
+ active = false;
1365
+ });
1366
+ }
1367
+ export {
1368
+ AssetPreview,
1369
+ AssetProperties,
1370
+ studioConfig as Config,
1371
+ activate as default,
1372
+ dependencies
1373
+ };