@tangle-network/agent-app 0.46.2 → 0.46.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/assistant/index.js +2 -1
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/chunk-KKBTPZIE.js +805 -0
  5. package/dist/chunk-KKBTPZIE.js.map +1 -0
  6. package/dist/{chunk-2RTYQU4W.js → chunk-LEGZX5MQ.js} +150 -4
  7. package/dist/chunk-LEGZX5MQ.js.map +1 -0
  8. package/dist/{chunk-MBKNKAN2.js → chunk-RZ6MYWMJ.js} +416 -1210
  9. package/dist/chunk-RZ6MYWMJ.js.map +1 -0
  10. package/dist/stories/studio/StudioProviders.d.ts +10 -0
  11. package/dist/stories/studio/fixtures.d.ts +10 -2
  12. package/dist/studio/audio-preview.d.ts +12 -0
  13. package/dist/studio/generation.d.ts +22 -1
  14. package/dist/studio/index.d.ts +2 -0
  15. package/dist/studio/index.js +29 -1
  16. package/dist/studio/ports.d.ts +54 -0
  17. package/dist/studio-react/composer-option-controls.d.ts +19 -8
  18. package/dist/studio-react/download-generations.d.ts +4 -0
  19. package/dist/studio-react/index.d.ts +38 -22
  20. package/dist/studio-react/index.js +2198 -545
  21. package/dist/studio-react/index.js.map +1 -1
  22. package/dist/studio-react/media-tile.d.ts +21 -0
  23. package/dist/studio-react/media-viewer.d.ts +12 -0
  24. package/dist/studio-react/studio-confirm.d.ts +8 -0
  25. package/dist/studio-react/studio-generation-screen.d.ts +42 -0
  26. package/dist/studio-react/studio-history-screen.d.ts +28 -0
  27. package/dist/studio-react/studio-home-screen.d.ts +20 -0
  28. package/dist/studio-react/studio-playback.d.ts +31 -0
  29. package/dist/studio-react/studio-toasts.d.ts +21 -0
  30. package/dist/studio-react/studio.css +142 -72
  31. package/dist/studio-react/use-batch-navigation.d.ts +20 -0
  32. package/dist/studio-react/use-deferred-delete.d.ts +19 -0
  33. package/dist/studio-react/use-generation-history.d.ts +22 -0
  34. package/dist/studio-react/vault-path-popover.d.ts +12 -0
  35. package/dist/web-react/index.js +8 -6
  36. package/package.json +2 -7
  37. package/dist/chunk-2RTYQU4W.js.map +0 -1
  38. package/dist/chunk-MBKNKAN2.js.map +0 -1
  39. package/dist/studio-react/generation-card.d.ts +0 -9
  40. package/dist/studio-react/generation-detail-modal.d.ts +0 -11
  41. package/dist/studio-react/generation-detail.d.ts +0 -6
  42. package/dist/studio-react/generation-grid.d.ts +0 -15
  43. package/dist/studio-react/library-drawer.d.ts +0 -12
  44. package/dist/studio-react/library-panel.d.ts +0 -15
  45. package/dist/studio-react/result-canvas.d.ts +0 -6
  46. package/dist/studio-react/studio-header.d.ts +0 -5
  47. package/dist/studio-react/studio-sheet.d.ts +0 -12
  48. package/dist/studio-react/studio-workspace.d.ts +0 -21
  49. package/dist/studio-react/type-config.d.ts +0 -11
@@ -1,21 +1,32 @@
1
1
  import {
2
+ MEDIA_TYPE_FILTERS,
3
+ WIDE_WAVEFORM_BARS,
4
+ aspectRatioFromOptions,
2
5
  buildGenerationRequestBody,
3
6
  curateComposerModels,
7
+ defaultVaultPathFor,
4
8
  failedOptimisticGeneration,
9
+ generationAspectRatio,
10
+ generationBatchKey,
5
11
  generationError,
12
+ generationSpecSegments,
6
13
  generationStatus,
7
14
  generationVaultPath,
15
+ generationsInBatch,
16
+ hashSeed,
8
17
  imageToVideoSibling,
9
- isGenerationType,
10
18
  isLocalGeneration,
11
19
  latestBatchOf,
20
+ mergeGenerationPages,
12
21
  mergeLiveGeneration,
13
22
  mergeLoaderAndLive,
14
23
  modelMessage,
15
24
  normalizeImageCount,
25
+ normalizeVaultPath,
16
26
  optimisticGeneration,
17
27
  optionChoices,
18
28
  preferredModelId,
29
+ previewWaveformBars,
19
30
  reconcileOptionValues,
20
31
  relativeTime,
21
32
  resolveComposerOptions,
@@ -23,22 +34,21 @@ import {
23
34
  textToVideoSibling,
24
35
  userSafeGenerationMessage,
25
36
  validateCustomImageSize
26
- } from "../chunk-2RTYQU4W.js";
37
+ } from "../chunk-LEGZX5MQ.js";
38
+ import {
39
+ useInfiniteScroll
40
+ } from "../chunk-KKBTPZIE.js";
27
41
  import {
28
42
  CheckGlyph,
29
43
  ChevronDown,
30
44
  OVERLAY_SHADOW,
31
45
  POPOVER_OPTION_FOCUS,
46
+ POPOVER_SURFACE_ATTR,
32
47
  PopoverSurface,
33
48
  ProviderLogo,
34
49
  usePopover
35
50
  } from "../chunk-YDOQE47G.js";
36
-
37
- // src/studio-react/studio-workspace.tsx
38
- import { useRef as useRef3, useState as useState4 } from "react";
39
- import { useSearchParams } from "react-router";
40
- import { Button as Button4 } from "@tangle-network/sandbox-ui/primitives";
41
- import { Images as Images2 } from "lucide-react";
51
+ import "../chunk-EDTWGSQT.js";
42
52
 
43
53
  // src/studio-react/use-studio-generations.ts
44
54
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -208,6 +218,82 @@ function useCloseWhenScrolledOut(open, setOpen, triggerRef, bandRef) {
208
218
  return () => band.removeEventListener("scroll", onScroll);
209
219
  }, [bandRef, open, setOpen, triggerRef]);
210
220
  }
221
+ function MenuRows({
222
+ value,
223
+ choices,
224
+ onSelect
225
+ }) {
226
+ return choices.map((choice) => /* @__PURE__ */ jsxs(
227
+ "button",
228
+ {
229
+ type: "button",
230
+ role: "menuitemradio",
231
+ "aria-checked": choice.value === value,
232
+ onClick: () => onSelect(choice.value),
233
+ className: menuRowClass(choice.value === value),
234
+ children: [
235
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: choice.label }),
236
+ choice.value === value && /* @__PURE__ */ jsx(CheckGlyph, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-primary" })
237
+ ]
238
+ },
239
+ String(choice.value)
240
+ ));
241
+ }
242
+ function MenuPill({
243
+ label,
244
+ value,
245
+ choices,
246
+ onSelect,
247
+ className
248
+ }) {
249
+ const [open, setOpen] = useState2(false);
250
+ const { containerRef, triggerRef, panelRef, triggerProps } = usePopover(open, setOpen);
251
+ const panelId = useId();
252
+ const selected = choices.find((choice) => choice.value === value);
253
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative inline-flex flex-none", children: [
254
+ /* @__PURE__ */ jsxs(
255
+ "button",
256
+ {
257
+ type: "button",
258
+ ...triggerProps,
259
+ "aria-controls": open ? panelId : void 0,
260
+ title: label,
261
+ "aria-label": label,
262
+ onClick: () => setOpen(!open),
263
+ className: `${PILL} ${className ?? ""}`,
264
+ children: [
265
+ /* @__PURE__ */ jsx("span", { children: selected?.label ?? "\u2014" }),
266
+ /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })
267
+ ]
268
+ }
269
+ ),
270
+ /* @__PURE__ */ jsxs(
271
+ PopoverSurface,
272
+ {
273
+ open,
274
+ id: panelId,
275
+ role: "menu",
276
+ triggerRef,
277
+ panelRef,
278
+ className: MENU_PANEL,
279
+ children: [
280
+ /* @__PURE__ */ jsx("div", { className: MENU_HEADER, children: label }),
281
+ /* @__PURE__ */ jsx(
282
+ MenuRows,
283
+ {
284
+ value,
285
+ choices,
286
+ onSelect: (next) => {
287
+ onSelect(next);
288
+ setOpen(false);
289
+ }
290
+ }
291
+ )
292
+ ]
293
+ }
294
+ )
295
+ ] });
296
+ }
211
297
  function OptionPill({
212
298
  label,
213
299
  value,
@@ -258,24 +344,17 @@ function OptionPill({
258
344
  children: [
259
345
  /* @__PURE__ */ jsx("div", { className: MENU_HEADER, children: label }),
260
346
  customOpen && custom ? custom.render({ close }) : /* @__PURE__ */ jsxs(Fragment, { children: [
261
- choices.map((choice) => /* @__PURE__ */ jsxs(
262
- "button",
347
+ /* @__PURE__ */ jsx(
348
+ MenuRows,
263
349
  {
264
- type: "button",
265
- role: "menuitemradio",
266
- "aria-checked": choice.value === value,
267
- onClick: () => {
268
- onSelect(choice.value);
350
+ value,
351
+ choices,
352
+ onSelect: (next) => {
353
+ onSelect(next);
269
354
  close();
270
- },
271
- className: menuRowClass(choice.value === value),
272
- children: [
273
- /* @__PURE__ */ jsx("span", { className: "truncate", children: choice.label }),
274
- choice.value === value && /* @__PURE__ */ jsx(CheckGlyph, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-primary" })
275
- ]
276
- },
277
- String(choice.value)
278
- )),
355
+ }
356
+ }
357
+ ),
279
358
  custom && /* @__PURE__ */ jsx(
280
359
  "button",
281
360
  {
@@ -734,14 +813,20 @@ function StudioComposer({
734
813
  setError(null);
735
814
  const clientRequestId = crypto.randomUUID();
736
815
  const imageCount = type === "image" ? normalizeImageCount(values.n ?? 1) : 1;
737
- const localGenerations = Array.from({ length: imageCount }, (_, outputIndex) => optimisticGeneration({
738
- type,
739
- prompt: promptText,
740
- model: modelId,
741
- clientRequestId,
742
- outputIndex: type === "image" ? outputIndex : void 0,
743
- outputCount: type === "image" ? imageCount : void 0
744
- }));
816
+ const localGenerations = Array.from({ length: imageCount }, (_, outputIndex) => optimisticGeneration(
817
+ {
818
+ type,
819
+ prompt: promptText,
820
+ model: modelId,
821
+ clientRequestId,
822
+ outputIndex: type === "image" ? outputIndex : void 0,
823
+ outputCount: type === "image" ? imageCount : void 0
824
+ },
825
+ aspectRatioFromOptions(type, {
826
+ size: asText(values.size),
827
+ aspectRatio: asText(values.aspect_ratio)
828
+ })
829
+ ));
745
830
  localGenerations.slice().reverse().forEach(onGenerated);
746
831
  setPrompt("");
747
832
  let receivedServerGeneration = false;
@@ -909,577 +994,2145 @@ function choicesForPill(param, meta, value) {
909
994
  return [...choices, { value, label: `${optionValueLabel(param, value)} \xB7 custom` }];
910
995
  }
911
996
 
912
- // src/studio-react/studio-header.tsx
913
- import { Button } from "@tangle-network/sandbox-ui/primitives";
914
- import { Images } from "lucide-react";
997
+ // src/studio-react/generation-notice.tsx
998
+ import { Info } from "lucide-react";
915
999
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
916
- function StudioHeader({
917
- count,
918
- onOpenLibrary,
919
- canGenerate
920
- }) {
921
- return /* @__PURE__ */ jsx3("header", { className: "sticky top-0 z-20 border-b border-border bg-card", children: /* @__PURE__ */ jsxs3("div", { className: "mx-auto flex w-full max-w-3xl items-center justify-between gap-3 px-4 py-3 sm:px-6", children: [
922
- /* @__PURE__ */ jsxs3("div", { className: "min-w-0", children: [
923
- /* @__PURE__ */ jsx3("h1", { className: "text-lg font-semibold tracking-tight text-foreground", children: "Studio" }),
924
- /* @__PURE__ */ jsx3("p", { className: "truncate text-xs text-muted-foreground", children: canGenerate ? "Generate images, video, voice, avatars, and transcripts." : "Browse your team's generations." })
925
- ] }),
926
- /* @__PURE__ */ jsxs3(Button, { size: "sm", variant: "outline", onClick: onOpenLibrary, className: "shrink-0", children: [
927
- /* @__PURE__ */ jsx3(Images, { className: "mr-1.5 h-4 w-4" }),
928
- "Library",
929
- count > 0 && /* @__PURE__ */ jsx3("span", { className: "ml-2 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary/15 px-1.5 text-xs font-semibold text-primary", children: count })
930
- ] })
931
- ] }) });
1000
+ function GenerationNoticeChip({ className }) {
1001
+ return /* @__PURE__ */ jsxs3(
1002
+ "p",
1003
+ {
1004
+ className: `inline-block rounded-full border px-3.5 py-1.5 text-center text-[12.5px] font-medium leading-relaxed text-foreground shadow-sm ${className ?? ""}`,
1005
+ style: { background: "var(--studio-notice-bg)", borderColor: "var(--studio-notice-border)" },
1006
+ children: [
1007
+ /* @__PURE__ */ jsx3(Info, { className: "mr-1.5 inline-block h-[14px] w-[14px] align-[-2px] text-primary", strokeWidth: 1.5, "aria-hidden": true }),
1008
+ "Each prompt starts a new generation \u2014 this chat does not remember the last one."
1009
+ ]
1010
+ }
1011
+ );
932
1012
  }
933
1013
 
934
- // src/studio-react/result-canvas.tsx
935
- import { Badge } from "@tangle-network/sandbox-ui/primitives";
936
- import { ArrowRight, Sparkles } from "lucide-react";
937
-
938
- // src/studio-react/type-config.ts
939
- import { FileText, Image, Mic, User, Video as Video2 } from "lucide-react";
940
- var MEDIA_SCRIM = "bg-black/60 text-white border-white/10";
941
- var IMAGE = { label: "Image", icon: Image, color: MEDIA_SCRIM };
942
- var TYPE_CONFIG = {
943
- image: IMAGE,
944
- video: { label: "Video", icon: Video2, color: MEDIA_SCRIM },
945
- avatar: { label: "Avatar", icon: User, color: MEDIA_SCRIM },
946
- speech: { label: "Audio", icon: Mic, color: MEDIA_SCRIM },
947
- transcription: { label: "Transcript", icon: FileText, color: MEDIA_SCRIM }
948
- };
949
- function typeConfigFor(type) {
950
- return TYPE_CONFIG[type] ?? IMAGE;
951
- }
1014
+ // src/studio-react/media-tile.tsx
1015
+ import {
1016
+ Check,
1017
+ Download,
1018
+ FileText,
1019
+ FolderOpen,
1020
+ FolderPlus,
1021
+ Pause,
1022
+ Play,
1023
+ Trash2
1024
+ } from "lucide-react";
1025
+ import {
1026
+ useEffect as useEffect6,
1027
+ useState as useState6
1028
+ } from "react";
952
1029
 
953
- // src/studio-react/result-canvas.tsx
1030
+ // src/studio-react/vault-path-popover.tsx
1031
+ import { useEffect as useEffect4, useId as useId2, useRef as useRef3, useState as useState4 } from "react";
954
1032
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
955
- function ResultCanvas({
956
- batch,
957
- onOpenLibrary,
958
- onSelect
1033
+ function VaultPathPopover({
1034
+ open,
1035
+ triggerRef,
1036
+ panelRef,
1037
+ generations,
1038
+ onSubmit,
1039
+ onCancel,
1040
+ pending = false
959
1041
  }) {
960
- const cardClass = "rounded-xl border border-border bg-card shadow-sm";
961
- if (batch.length === 0) {
962
- return /* @__PURE__ */ jsxs4("section", { className: `${cardClass} flex min-h-[180px] flex-col items-center justify-center gap-2 p-8 text-center`, children: [
963
- /* @__PURE__ */ jsx4("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx4(Sparkles, { className: "h-6 w-6 text-muted-foreground/40" }) }),
964
- /* @__PURE__ */ jsx4("p", { className: "text-sm font-medium text-foreground", children: "Your creations appear here" }),
965
- /* @__PURE__ */ jsx4("p", { className: "max-w-xs text-xs text-muted-foreground", children: "Generate above \u2014 results appear here and are saved to your library." })
966
- ] });
1042
+ const inputRef = useRef3(null);
1043
+ const inputId = useId2();
1044
+ const [error, setError] = useState4(null);
1045
+ useEffect4(() => {
1046
+ if (!open) return;
1047
+ setError(null);
1048
+ inputRef.current?.focus();
1049
+ inputRef.current?.select();
1050
+ }, [open]);
1051
+ if (!open) return null;
1052
+ async function submit(event) {
1053
+ event.preventDefault();
1054
+ const normalized = normalizeVaultPath(inputRef.current?.value ?? "");
1055
+ if (!normalized) {
1056
+ setError("Enter a folder path inside the vault.");
1057
+ return;
1058
+ }
1059
+ setError(null);
1060
+ try {
1061
+ await onSubmit(normalized);
1062
+ } catch {
1063
+ setError("Could not save to vault. Try again.");
1064
+ }
967
1065
  }
968
- const cfg = typeConfigFor(batch[0]?.type ?? "image");
969
- const Icon = cfg.icon;
970
- const statuses = batch.map(generationStatus);
971
- const runLabel = statuses.some((s) => s === "pending" || s === "running") ? "Generating\u2026" : statuses.every((s) => s === "failed") ? "Last run failed" : "Latest creation";
972
- const isWorking = statuses.some((s) => s === "pending" || s === "running");
973
- return /* @__PURE__ */ jsxs4("section", { className: `${cardClass} p-5`, children: [
974
- /* @__PURE__ */ jsxs4("div", { className: "mb-3 flex items-center justify-between gap-3", children: [
975
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
976
- /* @__PURE__ */ jsxs4(Badge, { variant: "outline", className: `${cfg.color} gap-1 text-xs`, children: [
977
- /* @__PURE__ */ jsx4(Icon, { className: "h-3 w-3" }),
978
- cfg.label
1066
+ const count = generations.length;
1067
+ const defaultPath = defaultVaultPathFor(generations);
1068
+ return /* @__PURE__ */ jsx4(
1069
+ PopoverSurface,
1070
+ {
1071
+ open,
1072
+ triggerRef,
1073
+ panelRef,
1074
+ className: `flex w-[262px] flex-col overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground ${OVERLAY_SHADOW}`,
1075
+ children: /* @__PURE__ */ jsxs4("form", { onSubmit: submit, children: [
1076
+ /* @__PURE__ */ jsxs4("label", { htmlFor: inputId, className: "block px-0.5 pb-[7px] pt-0.5 text-[12px] text-muted-foreground", children: [
1077
+ "Save ",
1078
+ count,
1079
+ " item",
1080
+ count > 1 ? "s" : "",
1081
+ " to"
979
1082
  ] }),
980
- /* @__PURE__ */ jsx4("span", { className: "text-sm font-medium text-foreground", children: runLabel }),
981
- isWorking && /* @__PURE__ */ jsx4("span", { className: "h-2 w-2 animate-pulse rounded-full bg-primary" })
982
- ] }),
983
- /* @__PURE__ */ jsxs4(
984
- "button",
985
- {
986
- type: "button",
987
- onClick: onOpenLibrary,
988
- className: "flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:text-primary/80",
989
- children: [
990
- "View in Library",
991
- /* @__PURE__ */ jsx4(ArrowRight, { className: "h-3.5 w-3.5" })
992
- ]
993
- }
994
- )
995
- ] }),
996
- /* @__PURE__ */ jsx4("div", { className: `grid gap-3 ${batch.length === 1 ? "grid-cols-1" : "grid-cols-2"}`, children: batch.map((generation) => /* @__PURE__ */ jsx4(
997
- "button",
998
- {
999
- type: "button",
1000
- onClick: () => onSelect(generation),
1001
- className: "studio-bloom group relative aspect-video overflow-hidden rounded-lg border border-border bg-secondary transition-colors hover:border-primary/40",
1002
- children: /* @__PURE__ */ jsx4(CanvasTile, { generation })
1003
- },
1004
- generation.id
1005
- )) }),
1006
- batch.length > 1 && !isWorking && /* @__PURE__ */ jsxs4("p", { className: "mt-2 text-xs text-muted-foreground", children: [
1007
- batch.length,
1008
- " results from this run"
1009
- ] })
1010
- ] });
1011
- }
1012
- function CanvasTile({ generation }) {
1013
- const cfg = typeConfigFor(generation.type);
1014
- const Icon = cfg.icon;
1015
- const status = generationStatus(generation);
1016
- if (status === "pending" || status === "running") {
1017
- return /* @__PURE__ */ jsx4("div", { className: "studio-shimmer absolute inset-0 h-full w-full" });
1018
- }
1019
- if (status === "failed") {
1020
- return /* @__PURE__ */ jsxs4("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2 p-4 text-center", children: [
1021
- /* @__PURE__ */ jsx4(Icon, { className: "h-6 w-6 text-destructive/50" }),
1022
- /* @__PURE__ */ jsx4("p", { className: "line-clamp-3 text-xs text-destructive", children: generationError(generation) ?? "Generation failed" })
1023
- ] });
1024
- }
1025
- if (generation.type === "image" && generation.result) {
1026
- return /* @__PURE__ */ jsx4("img", { src: generation.result, alt: generation.prompt, className: "absolute inset-0 h-full w-full object-contain" });
1027
- }
1028
- if ((generation.type === "video" || generation.type === "avatar") && generation.result) {
1029
- return /* @__PURE__ */ jsx4("video", { src: generation.result, controls: true, className: "absolute inset-0 h-full w-full object-contain" });
1030
- }
1031
- if (generation.type === "speech" && generation.result) {
1032
- return /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 flex items-center justify-center p-4", children: /* @__PURE__ */ jsx4("audio", { src: generation.result, controls: true, className: "w-full" }) });
1033
- }
1034
- if (generation.type === "transcription" && generation.result) {
1035
- return /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 overflow-y-auto p-4", children: /* @__PURE__ */ jsx4("p", { className: "whitespace-pre-wrap text-xs text-foreground", children: generation.result }) });
1036
- }
1037
- return /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx4(Icon, { className: "h-8 w-8 text-muted-foreground/20" }) });
1038
- }
1039
-
1040
- // src/studio-react/library-drawer.tsx
1041
- import { useMemo as useMemo3 } from "react";
1042
- import { Link as Link2 } from "react-router";
1043
- import {
1044
- Button as Button3,
1045
- Tabs,
1046
- TabsList,
1047
- TabsTrigger
1048
- } from "@tangle-network/sandbox-ui/primitives";
1049
- import { ArrowLeft, DollarSign, FolderOpen as FolderOpen2, X as X2 } from "lucide-react";
1050
-
1051
- // src/studio-react/studio-sheet.tsx
1052
- import * as Dialog from "@radix-ui/react-dialog";
1053
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1054
- function StudioSheet({
1055
- open,
1056
- onOpenChange,
1057
- title,
1058
- children
1059
- }) {
1060
- return /* @__PURE__ */ jsx5(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs5(Dialog.Portal, { children: [
1061
- /* @__PURE__ */ jsx5(Dialog.Overlay, { className: "studio-sheet-overlay fixed inset-0 z-50 bg-black/50" }),
1062
- /* @__PURE__ */ jsxs5(
1063
- Dialog.Content,
1064
- {
1065
- className: "studio-sheet-content fixed inset-y-0 right-0 z-50 flex w-[min(92vw,30rem)] flex-col border-l border-card-edge bg-popover shadow-[var(--shadow-overlay)] focus:outline-none",
1066
- "aria-describedby": void 0,
1067
- children: [
1068
- /* @__PURE__ */ jsx5(Dialog.Title, { className: "sr-only", children: title }),
1069
- children
1070
- ]
1071
- }
1072
- )
1073
- ] }) });
1074
- }
1075
-
1076
- // src/studio-react/generation-grid.tsx
1077
- import { EmptyState } from "@tangle-network/sandbox-ui/primitives";
1078
- import { Film, Sparkles as Sparkles2 } from "lucide-react";
1079
-
1080
- // src/studio-react/generation-card.tsx
1081
- import { Card, CardContent, Badge as Badge2 } from "@tangle-network/sandbox-ui/primitives";
1082
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1083
- function GenerationCard({
1084
- generation,
1085
- onSelect
1086
- }) {
1087
- const cfg = typeConfigFor(generation.type);
1088
- const Icon = cfg.icon;
1089
- const status = generationStatus(generation);
1090
- return /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => onSelect(generation), className: "group text-left animate-row-in", children: /* @__PURE__ */ jsxs6(Card, { className: "overflow-hidden transition-all group-hover:border-primary/50 group-hover:shadow-md", children: [
1091
- /* @__PURE__ */ jsxs6("div", { className: "relative aspect-video bg-secondary", children: [
1092
- generation.type === "image" && generation.result ? /* @__PURE__ */ jsx6("img", { src: generation.result, alt: generation.prompt, className: "h-full w-full object-cover" }) : (generation.type === "video" || generation.type === "avatar") && generation.result ? /* @__PURE__ */ jsx6("video", { src: generation.result, className: "h-full w-full object-cover", muted: true }) : generation.type === "speech" && generation.result ? /* @__PURE__ */ jsx6("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx6(Icon, { className: "h-6 w-6 text-muted-foreground/40" }) }) : generation.type === "transcription" && generation.result ? /* @__PURE__ */ jsx6("div", { className: "flex h-full items-center justify-center p-4", children: /* @__PURE__ */ jsx6("p", { className: "line-clamp-5 text-xs text-muted-foreground", children: generation.result }) }) : status === "pending" || status === "running" ? /* @__PURE__ */ jsx6("div", { className: "shimmer h-full w-full" }) : /* @__PURE__ */ jsx6("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx6(Icon, { className: "h-8 w-8 text-muted-foreground/20" }) }),
1093
- /* @__PURE__ */ jsx6("div", { className: "absolute left-2 top-2", children: /* @__PURE__ */ jsxs6(Badge2, { variant: "outline", className: `${cfg.color} gap-1 text-xs`, children: [
1094
- /* @__PURE__ */ jsx6(Icon, { className: "h-3 w-3" }),
1095
- cfg.label
1096
- ] }) }),
1097
- /* @__PURE__ */ jsx6(GenerationStatusBadge, { generation })
1098
- ] }),
1099
- /* @__PURE__ */ jsxs6(CardContent, { className: "p-3", children: [
1100
- /* @__PURE__ */ jsx6("p", { className: "mb-2 line-clamp-2 text-sm font-medium text-foreground", children: generation.prompt }),
1101
- status === "failed" && /* @__PURE__ */ jsx6("p", { className: "mb-2 line-clamp-2 text-xs text-destructive", children: generationError(generation) }),
1102
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
1103
- generation.model && /* @__PURE__ */ jsx6("span", { className: "truncate text-xs text-muted-foreground", children: generation.model }),
1104
- /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 items-center gap-2", children: [
1105
- generation.cost != null && /* @__PURE__ */ jsxs6("span", { className: "text-xs text-muted-foreground", children: [
1106
- "$",
1107
- generation.cost.toFixed(3)
1108
- ] }),
1109
- /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: relativeTime(generation.createdAt) })
1083
+ /* @__PURE__ */ jsx4(
1084
+ "input",
1085
+ {
1086
+ ref: inputRef,
1087
+ id: inputId,
1088
+ defaultValue: defaultPath,
1089
+ spellCheck: false,
1090
+ autoComplete: "off",
1091
+ className: "h-8 w-full rounded-lg border border-input bg-muted px-2.5 text-[13px] outline-none focus:border-primary focus:ring-[3px] focus:ring-ring/30"
1092
+ },
1093
+ defaultPath
1094
+ ),
1095
+ error && /* @__PURE__ */ jsx4("p", { className: "pt-1.5 text-[12px] text-destructive", children: error }),
1096
+ /* @__PURE__ */ jsx4("p", { className: "pt-2 text-[11.5px] text-muted-foreground", children: "Media stays in Studio until you save it into the vault." }),
1097
+ /* @__PURE__ */ jsxs4("div", { className: "flex justify-end gap-2 pt-2.5", children: [
1098
+ /* @__PURE__ */ jsx4(
1099
+ "button",
1100
+ {
1101
+ type: "button",
1102
+ onClick: onCancel,
1103
+ className: "h-[30px] rounded-full border border-border px-3.5 text-[13px] hover:bg-accent",
1104
+ children: "Cancel"
1105
+ }
1106
+ ),
1107
+ /* @__PURE__ */ jsx4(
1108
+ "button",
1109
+ {
1110
+ type: "submit",
1111
+ disabled: pending,
1112
+ className: "h-[30px] rounded-full bg-primary px-3.5 text-[13px] font-medium text-primary-foreground hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50",
1113
+ children: "Save"
1114
+ }
1115
+ )
1110
1116
  ] })
1111
1117
  ] })
1112
- ] })
1113
- ] }) });
1114
- }
1115
- function GenerationStatusBadge({
1116
- generation,
1117
- inline = false
1118
- }) {
1119
- const status = generationStatus(generation);
1120
- if (status === "succeeded") return null;
1121
- const label = status === "failed" ? "Failed" : status === "running" ? "Running" : "Pending";
1122
- const className = status === "failed" ? "border-destructive/30 bg-[color-mix(in_srgb,hsl(var(--destructive))_10%,hsl(var(--card)))] text-destructive" : "border-warning/30 bg-[color-mix(in_srgb,hsl(var(--warning))_10%,hsl(var(--card)))] text-warning";
1123
- return /* @__PURE__ */ jsx6("div", { className: inline ? "" : "absolute bottom-2 left-2", children: /* @__PURE__ */ jsx6(Badge2, { variant: "outline", className: `${className} text-xs`, children: label }) });
1118
+ }
1119
+ );
1124
1120
  }
1125
1121
 
1126
- // src/studio-react/generation-grid.tsx
1127
- import { jsx as jsx7 } from "react/jsx-runtime";
1128
- function filterGenerations(generations, typeFilter) {
1129
- if (!typeFilter || !isGenerationType(typeFilter)) return generations;
1130
- return generations.filter((generation) => generation.type === typeFilter);
1122
+ // src/studio-react/download-generations.ts
1123
+ var downloadGenerationsViaAnchor = (generations) => {
1124
+ if (typeof document === "undefined") return;
1125
+ const downloadable = generations.filter((generation) => generation.result !== null);
1126
+ downloadable.forEach((generation, index) => {
1127
+ setTimeout(() => {
1128
+ if (generation.result === null) return;
1129
+ const vaultPath = generation.metadata?.vaultPath;
1130
+ const filename = typeof vaultPath === "string" && vaultPath.trim() ? vaultPath.trim().split("/").filter(Boolean).at(-1) ?? `${generation.type}-${generation.id}` : `${generation.type}-${generation.id}`;
1131
+ const anchor = document.createElement("a");
1132
+ anchor.href = generation.result;
1133
+ anchor.download = filename;
1134
+ document.body.appendChild(anchor);
1135
+ anchor.click();
1136
+ anchor.remove();
1137
+ }, index * 150);
1138
+ });
1139
+ };
1140
+
1141
+ // src/studio-react/studio-playback.tsx
1142
+ import {
1143
+ createContext,
1144
+ useCallback as useCallback3,
1145
+ useContext,
1146
+ useEffect as useEffect5,
1147
+ useMemo as useMemo3,
1148
+ useRef as useRef4,
1149
+ useState as useState5
1150
+ } from "react";
1151
+ import { jsx as jsx5 } from "react/jsx-runtime";
1152
+ var StudioPlaybackContext = createContext(null);
1153
+ var createBrowserAudioElement = () => new Audio();
1154
+ function formatClock(seconds) {
1155
+ const wholeSeconds = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
1156
+ return `${Math.floor(wholeSeconds / 60)}:${String(wholeSeconds % 60).padStart(2, "0")}`;
1131
1157
  }
1132
- function GenerationGrid({
1133
- generations,
1134
- typeFilter,
1135
- onSelect
1136
- }) {
1137
- if (generations.length === 0) {
1138
- return /* @__PURE__ */ jsx7(
1139
- EmptyState,
1140
- {
1141
- icon: typeFilter ? /* @__PURE__ */ jsx7(Film, { className: "h-8 w-8 text-muted-foreground/30" }) : /* @__PURE__ */ jsx7(Sparkles2, { className: "h-10 w-10 text-muted-foreground/30" }),
1142
- title: typeFilter ? `No ${TYPE_CONFIG[typeFilter]?.label ?? typeFilter} generations` : "No generations yet",
1143
- description: typeFilter ? "Try a different filter or change the type in the composer." : "Everything you and the agent create lives here.",
1144
- className: "py-16"
1145
- }
1146
- );
1147
- }
1148
- return /* @__PURE__ */ jsx7("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: generations.map((generation) => /* @__PURE__ */ jsx7(GenerationCard, { generation, onSelect }, generation.id)) });
1158
+ function metadataDuration(generation) {
1159
+ const duration = generation.metadata?.durationSeconds;
1160
+ return typeof duration === "number" && Number.isFinite(duration) ? Math.max(0, duration) : 0;
1149
1161
  }
1150
-
1151
- // src/studio-react/generation-detail.tsx
1152
- import { Link } from "react-router";
1153
- import { Badge as Badge3, Button as Button2 } from "@tangle-network/sandbox-ui/primitives";
1154
- import { FolderOpen } from "lucide-react";
1155
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1156
- function GenerationDetail({
1157
- generation,
1158
- vaultHref,
1159
- onNavigate
1160
- }) {
1161
- const cfg = typeConfigFor(generation.type);
1162
- const Icon = cfg.icon;
1163
- const vaultPath = generationVaultPath(generation);
1164
- const href = vaultHref && vaultPath ? vaultHref(vaultPath) : null;
1165
- return /* @__PURE__ */ jsxs7("div", { className: "space-y-4", children: [
1166
- /* @__PURE__ */ jsx8("div", { className: "overflow-hidden rounded-lg bg-secondary", children: generation.type === "image" && generation.result ? /* @__PURE__ */ jsx8("img", { src: generation.result, alt: generation.prompt, className: "max-h-[420px] w-full object-contain" }) : (generation.type === "video" || generation.type === "avatar") && generation.result ? /* @__PURE__ */ jsx8("video", { src: generation.result, controls: true, className: "max-h-[420px] w-full" }) : generation.type === "speech" && generation.result ? /* @__PURE__ */ jsx8("div", { className: "p-6", children: /* @__PURE__ */ jsx8("audio", { src: generation.result, controls: true, className: "w-full" }) }) : /* @__PURE__ */ jsx8("div", { className: "flex h-32 items-center justify-center", children: /* @__PURE__ */ jsx8(Icon, { className: "h-8 w-8 text-muted-foreground/20" }) }) }),
1167
- /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-center gap-2", children: [
1168
- /* @__PURE__ */ jsxs7(Badge3, { variant: "outline", className: `${cfg.color} gap-1`, children: [
1169
- /* @__PURE__ */ jsx8(Icon, { className: "h-3 w-3" }),
1170
- cfg.label
1171
- ] }),
1172
- /* @__PURE__ */ jsx8(GenerationStatusBadge, { generation, inline: true }),
1173
- generationError(generation) && /* @__PURE__ */ jsx8("span", { className: "text-xs text-destructive", children: generationError(generation) })
1174
- ] }),
1175
- /* @__PURE__ */ jsxs7("div", { children: [
1176
- /* @__PURE__ */ jsx8("span", { className: "mb-1 block text-xs font-semibold uppercase tracking-[0.05em] text-muted-foreground", children: "Prompt" }),
1177
- /* @__PURE__ */ jsx8("p", { className: "text-sm text-foreground", children: generation.prompt })
1178
- ] }),
1179
- /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-center gap-4", children: [
1180
- generation.model && /* @__PURE__ */ jsxs7("div", { children: [
1181
- /* @__PURE__ */ jsx8("span", { className: "block text-xs font-semibold uppercase tracking-[0.05em] text-muted-foreground", children: "Model" }),
1182
- /* @__PURE__ */ jsx8("span", { className: "text-xs text-foreground", children: generation.model })
1183
- ] }),
1184
- generation.cost != null && /* @__PURE__ */ jsxs7("div", { children: [
1185
- /* @__PURE__ */ jsx8("span", { className: "block text-xs font-semibold uppercase tracking-[0.05em] text-muted-foreground", children: "Cost" }),
1186
- /* @__PURE__ */ jsxs7("span", { className: "text-xs text-foreground", children: [
1187
- "$",
1188
- generation.cost.toFixed(3)
1189
- ] })
1190
- ] }),
1191
- generation.createdAt && /* @__PURE__ */ jsxs7("div", { children: [
1192
- /* @__PURE__ */ jsx8("span", { className: "block text-xs font-semibold uppercase tracking-[0.05em] text-muted-foreground", children: "Created" }),
1193
- /* @__PURE__ */ jsx8("span", { className: "text-xs text-foreground", children: new Date(generation.createdAt).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" }) })
1194
- ] })
1195
- ] }),
1196
- generation.type === "transcription" && generation.result && /* @__PURE__ */ jsxs7("div", { children: [
1197
- /* @__PURE__ */ jsx8("span", { className: "mb-1 block text-xs font-semibold uppercase tracking-[0.05em] text-muted-foreground", children: "Transcription" }),
1198
- /* @__PURE__ */ jsx8("pre", { className: "max-h-64 overflow-y-auto whitespace-pre-wrap rounded-lg bg-secondary p-4 font-mono text-[13px] tabular-nums text-foreground", children: generation.result })
1199
- ] }),
1200
- href && /* @__PURE__ */ jsx8("div", { className: "border-t border-border pt-2", children: /* @__PURE__ */ jsx8(Link, { to: href, prefetch: "intent", onClick: onNavigate, children: /* @__PURE__ */ jsxs7(Button2, { size: "sm", variant: "outline", children: [
1201
- /* @__PURE__ */ jsx8(FolderOpen, { className: "mr-1.5 h-3.5 w-3.5" }),
1202
- "Open in Vault"
1203
- ] }) }) })
1204
- ] });
1162
+ function clampPosition(seconds, duration) {
1163
+ const finiteSeconds = Number.isFinite(seconds) ? seconds : 0;
1164
+ return Math.max(0, duration > 0 ? Math.min(finiteSeconds, duration) : finiteSeconds);
1205
1165
  }
1206
-
1207
- // src/studio-react/library-drawer.tsx
1208
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1209
- function LibraryDrawer({
1210
- open,
1211
- onOpenChange,
1212
- generations,
1213
- totalCost,
1214
- typeFilter,
1215
- onFilterChange,
1216
- vaultHref,
1217
- selected,
1218
- onSelect
1219
- }) {
1220
- const visible = useMemo3(() => filterGenerations(generations, typeFilter), [generations, typeFilter]);
1221
- return /* @__PURE__ */ jsxs8(StudioSheet, { open, onOpenChange, title: "Asset library", children: [
1222
- /* @__PURE__ */ jsxs8("header", { className: "flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3", children: [
1223
- selected ? /* @__PURE__ */ jsxs8(
1224
- "button",
1225
- {
1226
- type: "button",
1227
- onClick: () => onSelect(null),
1228
- className: "flex items-center gap-1.5 text-sm font-medium text-foreground transition-colors hover:text-primary",
1229
- children: [
1230
- /* @__PURE__ */ jsx9(ArrowLeft, { className: "h-4 w-4" }),
1231
- "Back to library"
1232
- ]
1233
- }
1234
- ) : /* @__PURE__ */ jsx9("h2", { className: "text-sm font-semibold text-foreground", children: "Asset library" }),
1235
- /* @__PURE__ */ jsx9(
1236
- "button",
1237
- {
1238
- type: "button",
1239
- "aria-label": "Close library",
1240
- onClick: () => onOpenChange(false),
1241
- className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
1242
- children: /* @__PURE__ */ jsx9(X2, { className: "h-4 w-4" })
1166
+ function StudioPlaybackProvider(props) {
1167
+ const [activeId, setActiveId] = useState5(null);
1168
+ const [playing, setPlaying] = useState5(false);
1169
+ const [durationSeconds, setDurationSeconds] = useState5(0);
1170
+ const activeIdRef = useRef4(null);
1171
+ const playingRef = useRef4(false);
1172
+ const durationRef = useRef4(0);
1173
+ const createAudioElementRef = useRef4(props.createAudioElement ?? createBrowserAudioElement);
1174
+ createAudioElementRef.current = props.createAudioElement ?? createBrowserAudioElement;
1175
+ const audioRef = useRef4(null);
1176
+ const audioListenersRef = useRef4(null);
1177
+ const positionNodesRef = useRef4(/* @__PURE__ */ new Map());
1178
+ const timeNodeRef = useRef4(null);
1179
+ const animationFrameRef = useRef4(null);
1180
+ const playAttemptRef = useRef4(0);
1181
+ const setActive = useCallback3((id) => {
1182
+ activeIdRef.current = id;
1183
+ setActiveId(id);
1184
+ }, []);
1185
+ const setIsPlaying = useCallback3((value2) => {
1186
+ playingRef.current = value2;
1187
+ setPlaying(value2);
1188
+ }, []);
1189
+ const setDuration = useCallback3((value2) => {
1190
+ const normalized = Number.isFinite(value2) ? Math.max(0, value2) : 0;
1191
+ durationRef.current = normalized;
1192
+ setDurationSeconds(normalized);
1193
+ }, []);
1194
+ const paintAll = useCallback3(() => {
1195
+ const currentId = activeIdRef.current;
1196
+ const position = currentId ? Math.max(0, audioRef.current?.currentTime ?? 0) : 0;
1197
+ const duration = durationRef.current;
1198
+ const fraction = duration > 0 ? Math.min(1, position / duration) : 0;
1199
+ for (const [id, nodes] of positionNodesRef.current) {
1200
+ const isActive = id === currentId && (playingRef.current || position > 0);
1201
+ for (const node of nodes) {
1202
+ node.style.setProperty("--pos", isActive ? fraction.toFixed(4) : "0");
1203
+ if (isActive) node.dataset.active = "true";
1204
+ else delete node.dataset.active;
1205
+ }
1206
+ }
1207
+ if (timeNodeRef.current) timeNodeRef.current.textContent = formatClock(position);
1208
+ }, []);
1209
+ const cancelAnimation = useCallback3(() => {
1210
+ if (animationFrameRef.current !== null && typeof cancelAnimationFrame === "function") {
1211
+ cancelAnimationFrame(animationFrameRef.current);
1212
+ }
1213
+ animationFrameRef.current = null;
1214
+ }, []);
1215
+ const startAnimation = useCallback3(() => {
1216
+ if (animationFrameRef.current !== null || typeof requestAnimationFrame !== "function") return;
1217
+ const frame = () => {
1218
+ animationFrameRef.current = null;
1219
+ paintAll();
1220
+ if (playingRef.current) animationFrameRef.current = requestAnimationFrame(frame);
1221
+ };
1222
+ animationFrameRef.current = requestAnimationFrame(frame);
1223
+ }, [paintAll]);
1224
+ const ensureAudio = useCallback3(() => {
1225
+ if (audioRef.current) return audioRef.current;
1226
+ const audio = createAudioElementRef.current();
1227
+ const onLoadedMetadata = () => {
1228
+ if (Number.isFinite(audio.duration)) setDuration(audio.duration);
1229
+ paintAll();
1230
+ };
1231
+ const onEnded = () => {
1232
+ playAttemptRef.current += 1;
1233
+ audio.currentTime = 0;
1234
+ setIsPlaying(false);
1235
+ cancelAnimation();
1236
+ paintAll();
1237
+ };
1238
+ audio.addEventListener("loadedmetadata", onLoadedMetadata);
1239
+ audio.addEventListener("ended", onEnded);
1240
+ audioListenersRef.current = { loadedmetadata: onLoadedMetadata, ended: onEnded };
1241
+ audioRef.current = audio;
1242
+ return audio;
1243
+ }, [cancelAnimation, paintAll, setDuration, setIsPlaying]);
1244
+ const adopt = useCallback3((generation) => {
1245
+ const audio = ensureAudio();
1246
+ if (activeIdRef.current !== generation.id) {
1247
+ audio.src = generation.result ?? "";
1248
+ audio.currentTime = 0;
1249
+ setDuration(metadataDuration(generation));
1250
+ setActive(generation.id);
1251
+ }
1252
+ return audio;
1253
+ }, [ensureAudio, setActive, setDuration]);
1254
+ const play = useCallback3((generation) => {
1255
+ if (!generation.result) return;
1256
+ const audio = adopt(generation);
1257
+ const attempt = ++playAttemptRef.current;
1258
+ setIsPlaying(false);
1259
+ cancelAnimation();
1260
+ paintAll();
1261
+ try {
1262
+ const result = audio.play();
1263
+ if (result && typeof result.then === "function") {
1264
+ void result.then(() => {
1265
+ if (playAttemptRef.current !== attempt || activeIdRef.current !== generation.id) return;
1266
+ setIsPlaying(true);
1267
+ paintAll();
1268
+ startAnimation();
1269
+ }, () => {
1270
+ if (playAttemptRef.current !== attempt) return;
1271
+ setIsPlaying(false);
1272
+ cancelAnimation();
1273
+ paintAll();
1274
+ });
1275
+ } else {
1276
+ setIsPlaying(true);
1277
+ paintAll();
1278
+ startAnimation();
1279
+ }
1280
+ } catch {
1281
+ if (playAttemptRef.current === attempt) {
1282
+ setIsPlaying(false);
1283
+ cancelAnimation();
1284
+ paintAll();
1285
+ }
1286
+ }
1287
+ }, [adopt, cancelAnimation, paintAll, setIsPlaying, startAnimation]);
1288
+ const pause = useCallback3(() => {
1289
+ playAttemptRef.current += 1;
1290
+ audioRef.current?.pause();
1291
+ setIsPlaying(false);
1292
+ cancelAnimation();
1293
+ paintAll();
1294
+ }, [cancelAnimation, paintAll, setIsPlaying]);
1295
+ const stop2 = useCallback3(() => {
1296
+ playAttemptRef.current += 1;
1297
+ audioRef.current?.pause();
1298
+ if (audioRef.current) audioRef.current.currentTime = 0;
1299
+ setIsPlaying(false);
1300
+ setActive(null);
1301
+ cancelAnimation();
1302
+ paintAll();
1303
+ }, [cancelAnimation, paintAll, setActive, setIsPlaying]);
1304
+ const toggle = useCallback3((generation) => {
1305
+ if (activeIdRef.current === generation.id && playingRef.current) pause();
1306
+ else play(generation);
1307
+ }, [pause, play]);
1308
+ const seekTo = useCallback3((generation, seconds) => {
1309
+ if (!generation.result) return;
1310
+ const changedTrack = activeIdRef.current !== generation.id;
1311
+ const audio = adopt(generation);
1312
+ if (changedTrack) {
1313
+ playAttemptRef.current += 1;
1314
+ setIsPlaying(false);
1315
+ cancelAnimation();
1316
+ }
1317
+ audio.currentTime = clampPosition(seconds, durationRef.current);
1318
+ paintAll();
1319
+ }, [adopt, cancelAnimation, paintAll, setIsPlaying]);
1320
+ const seekBy = useCallback3((seconds) => {
1321
+ if (!activeIdRef.current || !audioRef.current) return;
1322
+ audioRef.current.currentTime = clampPosition(audioRef.current.currentTime + seconds, durationRef.current);
1323
+ paintAll();
1324
+ }, [paintAll]);
1325
+ const getPositionSeconds = useCallback3(() => audioRef.current?.currentTime ?? 0, []);
1326
+ const registerPositionNode = useCallback3((id, node) => {
1327
+ if (!node) {
1328
+ const nodes2 = positionNodesRef.current.get(id);
1329
+ if (nodes2) {
1330
+ for (const registered of nodes2) {
1331
+ registered.style.setProperty("--pos", "0");
1332
+ delete registered.dataset.active;
1243
1333
  }
1244
- )
1245
- ] }),
1246
- selected ? /* @__PURE__ */ jsx9("div", { className: "flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsx9(
1247
- GenerationDetail,
1248
- {
1249
- generation: selected,
1250
- vaultHref,
1251
- onNavigate: () => onOpenChange(false)
1252
1334
  }
1253
- ) }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
1254
- /* @__PURE__ */ jsxs8("div", { className: "shrink-0 border-b border-border px-4 pt-3", children: [
1255
- /* @__PURE__ */ jsxs8("div", { className: "mb-2 flex items-center justify-between gap-3", children: [
1256
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
1257
- /* @__PURE__ */ jsxs8("span", { children: [
1258
- visible.length,
1259
- " generation",
1260
- visible.length !== 1 ? "s" : ""
1261
- ] }),
1262
- /* @__PURE__ */ jsxs8("span", { className: "flex items-center gap-0.5", children: [
1263
- /* @__PURE__ */ jsx9(DollarSign, { className: "h-3 w-3" }),
1264
- totalCost.toFixed(2),
1265
- " spent"
1266
- ] })
1267
- ] }),
1268
- vaultHref && /* @__PURE__ */ jsx9(Link2, { to: vaultHref(), prefetch: "intent", onClick: () => onOpenChange(false), children: /* @__PURE__ */ jsxs8(Button3, { size: "sm", variant: "outline", children: [
1269
- /* @__PURE__ */ jsx9(FolderOpen2, { className: "mr-1.5 h-3.5 w-3.5" }),
1270
- "Vault"
1271
- ] }) })
1272
- ] }),
1273
- /* @__PURE__ */ jsx9(Tabs, { value: typeFilter ?? "all", onValueChange: (v) => onFilterChange(v === "all" ? null : v), children: /* @__PURE__ */ jsxs8(TabsList, { className: "h-9 w-full justify-start gap-1 overflow-x-auto bg-transparent", children: [
1274
- /* @__PURE__ */ jsx9(TabsTrigger, { value: "all", className: "text-xs", children: "All" }),
1275
- Object.entries(TYPE_CONFIG).map(([key, cfg]) => /* @__PURE__ */ jsx9(TabsTrigger, { value: key, className: "text-xs", children: cfg.label }, key))
1276
- ] }) })
1277
- ] }),
1278
- /* @__PURE__ */ jsx9("div", { className: "flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsx9(GenerationGrid, { generations: visible, typeFilter, onSelect }) })
1279
- ] })
1280
- ] });
1335
+ positionNodesRef.current.delete(id);
1336
+ return () => {
1337
+ };
1338
+ }
1339
+ const nodes = positionNodesRef.current.get(id) ?? /* @__PURE__ */ new Set();
1340
+ nodes.add(node);
1341
+ positionNodesRef.current.set(id, nodes);
1342
+ paintAll();
1343
+ return () => {
1344
+ nodes.delete(node);
1345
+ if (nodes.size === 0) positionNodesRef.current.delete(id);
1346
+ };
1347
+ }, [paintAll]);
1348
+ const registerTimeNode = useCallback3((node) => {
1349
+ timeNodeRef.current = node;
1350
+ paintAll();
1351
+ return () => {
1352
+ if (timeNodeRef.current === node) timeNodeRef.current = null;
1353
+ };
1354
+ }, [paintAll]);
1355
+ useEffect5(() => {
1356
+ paintAll();
1357
+ }, [activeId, durationSeconds, paintAll, playing]);
1358
+ useEffect5(() => () => {
1359
+ playAttemptRef.current += 1;
1360
+ cancelAnimation();
1361
+ const audio = audioRef.current;
1362
+ if (!audio) return;
1363
+ audio.pause();
1364
+ const listeners = audioListenersRef.current;
1365
+ if (listeners) {
1366
+ audio.removeEventListener("loadedmetadata", listeners.loadedmetadata);
1367
+ audio.removeEventListener("ended", listeners.ended);
1368
+ }
1369
+ }, [cancelAnimation]);
1370
+ const value = useMemo3(() => ({
1371
+ activeId,
1372
+ playing,
1373
+ durationSeconds,
1374
+ play,
1375
+ pause,
1376
+ toggle,
1377
+ stop: stop2,
1378
+ seekTo,
1379
+ seekBy,
1380
+ getPositionSeconds,
1381
+ registerPositionNode,
1382
+ registerTimeNode
1383
+ }), [
1384
+ activeId,
1385
+ durationSeconds,
1386
+ getPositionSeconds,
1387
+ pause,
1388
+ play,
1389
+ playing,
1390
+ registerPositionNode,
1391
+ registerTimeNode,
1392
+ seekBy,
1393
+ seekTo,
1394
+ stop2,
1395
+ toggle
1396
+ ]);
1397
+ return /* @__PURE__ */ jsx5(StudioPlaybackContext.Provider, { value, children: props.children });
1398
+ }
1399
+ function useStudioPlayback() {
1400
+ const playback = useContext(StudioPlaybackContext);
1401
+ if (!playback) throw new Error("useStudioPlayback must be used within a StudioPlaybackProvider");
1402
+ return playback;
1281
1403
  }
1282
1404
 
1283
- // src/studio-react/studio-workspace.tsx
1284
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1285
- function StudioWorkspace({
1286
- generations,
1287
- totalCost,
1288
- workspaceId,
1289
- role,
1290
- generationsEndpoint,
1291
- vaultHref
1405
+ // src/studio-react/media-tile.tsx
1406
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1407
+ function stop(event) {
1408
+ event.stopPropagation();
1409
+ }
1410
+ function MediaTile({
1411
+ generation,
1412
+ context,
1413
+ onOpen,
1414
+ actions,
1415
+ aspectRatio,
1416
+ waveformBars = 26,
1417
+ selectMode = false,
1418
+ selected = false,
1419
+ onToggleSelect,
1420
+ onRequestDelete,
1421
+ onSaved,
1422
+ className,
1423
+ style
1292
1424
  }) {
1293
- const [searchParams, setSearchParams] = useSearchParams();
1294
- const typeFilter = searchParams.get("type");
1295
- const [drawerOpen, setDrawerOpen] = useState4(false);
1296
- const [selected, setSelected] = useState4(null);
1297
- const canvasRef = useRef3(null);
1298
- const canGenerate = role !== "viewer";
1299
- const resolvedVaultHref = vaultHref ?? (workspaceId ? (filePath) => filePath ? `/app/${workspaceId}/vault?file=${encodeURIComponent(filePath)}` : `/app/${workspaceId}/vault` : void 0);
1300
- const { mergedGenerations, latestBatch, onGenerated } = useStudioGenerations(generations, {
1301
- workspaceId,
1302
- generationsEndpoint
1303
- });
1304
- function setFilter(type) {
1305
- if (type) setSearchParams({ type });
1306
- else setSearchParams({});
1307
- }
1308
- function openLibrary() {
1309
- setSelected(null);
1310
- setDrawerOpen(true);
1425
+ const playback = useStudioPlayback();
1426
+ const [waveNode, setWaveNode] = useState6(null);
1427
+ const [saveOpen, setSaveOpen] = useState6(false);
1428
+ const [saving, setSaving] = useState6(false);
1429
+ const { containerRef, triggerRef, panelRef, triggerProps } = usePopover(saveOpen, setSaveOpen);
1430
+ const vaultPath = generationVaultPath(generation);
1431
+ const status = generationStatus(generation);
1432
+ const isSpeech = generation.type === "speech";
1433
+ const isPlaying = isSpeech && playback.activeId === generation.id && playback.playing;
1434
+ useEffect6(() => playback.registerPositionNode(generation.id, waveNode), [
1435
+ generation.id,
1436
+ playback.registerPositionNode,
1437
+ waveNode
1438
+ ]);
1439
+ const classes = [
1440
+ "studio-tile relative isolate m-0 cursor-pointer overflow-hidden rounded-none border-0 bg-accent",
1441
+ aspectRatio === void 0 ? "aspect-square" : "",
1442
+ "focus-visible:[outline-offset:-2px]",
1443
+ isSpeech ? "studio-has-player" : "",
1444
+ selectMode ? "studio-selectmode" : "",
1445
+ selected ? "outline outline-2 -outline-offset-2 outline-primary" : "",
1446
+ className ?? ""
1447
+ ].filter(Boolean).join(" ");
1448
+ const tileStyle = aspectRatio === void 0 ? { ...style } : { ...style, aspectRatio, "--r": aspectRatio };
1449
+ function activate() {
1450
+ if (selectMode) onToggleSelect?.(generation.id);
1451
+ else onOpen(generation);
1311
1452
  }
1312
- function openDetail(generation) {
1313
- setSelected(generation);
1314
- setDrawerOpen(true);
1453
+ function onKeyDown(event) {
1454
+ if (event.target !== event.currentTarget || event.key !== "Enter" && event.key !== " ") return;
1455
+ event.preventDefault();
1456
+ activate();
1315
1457
  }
1316
- return /* @__PURE__ */ jsxs9("div", { className: "min-h-0 flex-1 overflow-y-auto bg-background", children: [
1317
- /* @__PURE__ */ jsx10(
1318
- StudioHeader,
1458
+ async function save(path) {
1459
+ if (!actions?.save) return;
1460
+ setSaving(true);
1461
+ try {
1462
+ const results = await actions.save({ generations: [generation], path });
1463
+ setSaveOpen(false);
1464
+ onSaved?.(results);
1465
+ } finally {
1466
+ setSaving(false);
1467
+ }
1468
+ }
1469
+ const bars = isSpeech ? previewWaveformBars(generation.id, waveformBars) : [];
1470
+ const hue = hashSeed(generation.id) % 360;
1471
+ const secondHue = (hue + 34) % 360;
1472
+ return /* @__PURE__ */ jsxs5(
1473
+ "figure",
1474
+ {
1475
+ role: "button",
1476
+ tabIndex: 0,
1477
+ "aria-label": `${generation.prompt} \u2014 open`,
1478
+ className: classes,
1479
+ style: tileStyle,
1480
+ "data-selectable": context === "history",
1481
+ "data-selected": selected,
1482
+ onClick: activate,
1483
+ onKeyDown,
1484
+ children: [
1485
+ /* @__PURE__ */ jsx6("div", { className: "absolute inset-0", children: status === "pending" || status === "running" ? /* @__PURE__ */ jsx6("div", { className: "studio-skeleton absolute inset-0" }) : status === "failed" ? /* @__PURE__ */ jsx6("div", { className: "grid h-full w-full place-items-center bg-accent p-3 text-center text-[12px] text-destructive", children: generationError(generation) ?? "Generation failed" }) : generation.result === null ? /* @__PURE__ */ jsx6("div", { className: "studio-skeleton absolute inset-0" }) : generation.type === "image" ? /* @__PURE__ */ jsx6(
1486
+ "img",
1487
+ {
1488
+ src: generation.result,
1489
+ alt: "",
1490
+ className: "h-full w-full object-cover",
1491
+ loading: "lazy",
1492
+ draggable: false
1493
+ }
1494
+ ) : generation.type === "video" || generation.type === "avatar" ? /* @__PURE__ */ jsx6(
1495
+ "video",
1496
+ {
1497
+ src: generation.result,
1498
+ muted: true,
1499
+ playsInline: true,
1500
+ preload: "metadata",
1501
+ className: "h-full w-full object-cover"
1502
+ }
1503
+ ) : generation.type === "transcription" ? /* @__PURE__ */ jsx6("div", { className: "grid h-full w-full place-items-center bg-muted text-muted-foreground", children: /* @__PURE__ */ jsx6(FileText, { size: 20, strokeWidth: 1.5 }) }) : isSpeech ? /* @__PURE__ */ jsx6(
1504
+ "div",
1505
+ {
1506
+ className: "absolute inset-0",
1507
+ style: { background: `linear-gradient(158deg, hsl(${hue} 38% 24%), hsl(${secondHue} 46% 13%))` },
1508
+ children: /* @__PURE__ */ jsxs5(
1509
+ "div",
1510
+ {
1511
+ className: "studio-wave-layers",
1512
+ ref: setWaveNode,
1513
+ style: { color: `hsl(${hue} 92% 74%)` },
1514
+ children: [
1515
+ /* @__PURE__ */ jsx6("div", { className: "studio-wave studio-wave-base", children: bars.map((bar, index) => /* @__PURE__ */ jsx6("i", { style: { height: `${bar.heightPct}%`, opacity: bar.opacity } }, index)) }),
1516
+ /* @__PURE__ */ jsx6("div", { className: "studio-wave studio-wave-play", children: bars.map((bar, index) => /* @__PURE__ */ jsx6("i", { style: { height: `${bar.heightPct}%`, opacity: bar.opacity } }, index)) })
1517
+ ]
1518
+ }
1519
+ )
1520
+ }
1521
+ ) : null }),
1522
+ /* @__PURE__ */ jsx6("div", { className: "studio-tile-scrim absolute inset-0" }),
1523
+ /* @__PURE__ */ jsxs5("div", { className: "studio-tile-actions absolute right-2 top-2 flex gap-[5px]", children: [
1524
+ /* @__PURE__ */ jsx6(
1525
+ "button",
1526
+ {
1527
+ type: "button",
1528
+ className: "studio-ibtn relative grid h-[30px] w-[30px] place-items-center rounded-full",
1529
+ "data-tip": "Download",
1530
+ "aria-label": "Download",
1531
+ onClick: (event) => {
1532
+ stop(event);
1533
+ void (actions?.download ?? downloadGenerationsViaAnchor)([generation]);
1534
+ },
1535
+ children: /* @__PURE__ */ jsx6(Download, { size: 15, strokeWidth: 1.5 })
1536
+ }
1537
+ ),
1538
+ !vaultPath && actions?.save && /* @__PURE__ */ jsxs5("div", { ref: containerRef, children: [
1539
+ /* @__PURE__ */ jsx6(
1540
+ "button",
1541
+ {
1542
+ ...triggerProps,
1543
+ type: "button",
1544
+ className: "studio-ibtn relative grid h-[30px] w-[30px] place-items-center rounded-full",
1545
+ "data-tip": "Save to vault",
1546
+ "aria-label": "Save to vault",
1547
+ onClick: (event) => {
1548
+ stop(event);
1549
+ setSaveOpen((open) => !open);
1550
+ },
1551
+ children: /* @__PURE__ */ jsx6(FolderPlus, { size: 15, strokeWidth: 1.5 })
1552
+ }
1553
+ ),
1554
+ /* @__PURE__ */ jsx6(
1555
+ VaultPathPopover,
1556
+ {
1557
+ open: saveOpen,
1558
+ triggerRef,
1559
+ panelRef,
1560
+ generations: [generation],
1561
+ onSubmit: save,
1562
+ onCancel: () => setSaveOpen(false),
1563
+ pending: saving
1564
+ }
1565
+ )
1566
+ ] }),
1567
+ vaultPath && actions?.vaultHref && /* @__PURE__ */ jsx6(
1568
+ "a",
1569
+ {
1570
+ href: actions.vaultHref(vaultPath),
1571
+ className: "studio-ibtn relative grid h-[30px] w-[30px] place-items-center rounded-full",
1572
+ "data-tip": "View in vault",
1573
+ "aria-label": "View in vault",
1574
+ onClick: (event) => {
1575
+ stop(event);
1576
+ if (actions.onOpenVault) {
1577
+ event.preventDefault();
1578
+ actions.onOpenVault(generation);
1579
+ }
1580
+ },
1581
+ children: /* @__PURE__ */ jsx6(FolderOpen, { size: 15, strokeWidth: 1.5 })
1582
+ }
1583
+ ),
1584
+ vaultPath && !actions?.vaultHref && actions?.onOpenVault && /* @__PURE__ */ jsx6(
1585
+ "button",
1586
+ {
1587
+ type: "button",
1588
+ className: "studio-ibtn relative grid h-[30px] w-[30px] place-items-center rounded-full",
1589
+ "data-tip": "View in vault",
1590
+ "aria-label": "View in vault",
1591
+ onClick: (event) => {
1592
+ stop(event);
1593
+ actions.onOpenVault?.(generation);
1594
+ },
1595
+ children: /* @__PURE__ */ jsx6(FolderOpen, { size: 15, strokeWidth: 1.5 })
1596
+ }
1597
+ ),
1598
+ actions?.remove && onRequestDelete && /* @__PURE__ */ jsx6(
1599
+ "button",
1600
+ {
1601
+ type: "button",
1602
+ className: "studio-ibtn studio-ibtn-danger relative grid h-[30px] w-[30px] place-items-center rounded-full",
1603
+ "data-tip": "Delete",
1604
+ "aria-label": "Delete",
1605
+ onClick: (event) => {
1606
+ stop(event);
1607
+ onRequestDelete(generation);
1608
+ },
1609
+ children: /* @__PURE__ */ jsx6(Trash2, { size: 15, strokeWidth: 1.5 })
1610
+ }
1611
+ )
1612
+ ] }),
1613
+ /* @__PURE__ */ jsxs5("div", { className: "studio-tile-badges absolute bottom-2 left-2 flex items-center gap-1.5", children: [
1614
+ (generation.type === "video" || generation.type === "avatar") && /* @__PURE__ */ jsx6("span", { className: "studio-chip-dark grid h-[22px] w-[22px] place-items-center rounded-full", "aria-hidden": "true", children: /* @__PURE__ */ jsx6(Play, { size: 11, strokeWidth: 1.5, fill: "currentColor" }) }),
1615
+ isSpeech && /* @__PURE__ */ jsx6(
1616
+ "button",
1617
+ {
1618
+ type: "button",
1619
+ className: "studio-play-btn studio-chip-dark grid h-[22px] w-[22px] place-items-center rounded-full",
1620
+ "aria-label": isPlaying ? `Pause ${generation.prompt}` : `Play ${generation.prompt}`,
1621
+ onClick: (event) => {
1622
+ stop(event);
1623
+ playback.toggle(generation);
1624
+ },
1625
+ children: isPlaying ? /* @__PURE__ */ jsx6(Pause, { size: 11, strokeWidth: 1.5, fill: "currentColor" }) : /* @__PURE__ */ jsx6(Play, { size: 11, strokeWidth: 1.5, fill: "currentColor" })
1626
+ }
1627
+ ),
1628
+ vaultPath && /* @__PURE__ */ jsxs5("span", { className: "studio-chip-dark inline-flex h-5 items-center gap-1 rounded-full pl-1.5 pr-2 text-[11px]", children: [
1629
+ /* @__PURE__ */ jsx6(FolderOpen, { size: 12, strokeWidth: 1.5 }),
1630
+ "In vault"
1631
+ ] })
1632
+ ] }),
1633
+ context === "history" && /* @__PURE__ */ jsx6(
1634
+ "button",
1635
+ {
1636
+ type: "button",
1637
+ className: `studio-tile-sel studio-chip-dark absolute left-2 top-2 h-6 w-6 place-items-center rounded-full ${selected ? "!bg-primary border-primary" : ""}`,
1638
+ "aria-pressed": selected,
1639
+ "aria-label": "Select this item",
1640
+ onClick: (event) => {
1641
+ stop(event);
1642
+ onToggleSelect?.(generation.id);
1643
+ },
1644
+ children: /* @__PURE__ */ jsx6(
1645
+ Check,
1646
+ {
1647
+ size: 13,
1648
+ strokeWidth: 1.5,
1649
+ className: selected ? "opacity-100" : "opacity-0"
1650
+ }
1651
+ )
1652
+ }
1653
+ ),
1654
+ /* @__PURE__ */ jsx6("figcaption", { className: "studio-tile-prompt absolute inset-x-[11px] bottom-[9px] m-0 truncate text-[12px] text-white", children: generation.prompt })
1655
+ ]
1656
+ }
1657
+ );
1658
+ }
1659
+
1660
+ // src/studio-react/media-viewer.tsx
1661
+ import {
1662
+ useCallback as useCallback4,
1663
+ useEffect as useEffect7,
1664
+ useMemo as useMemo4,
1665
+ useRef as useRef5,
1666
+ useState as useState7
1667
+ } from "react";
1668
+ import { createPortal } from "react-dom";
1669
+ import { Download as Download2, FolderOpen as FolderOpen2, FolderPlus as FolderPlus2, Pause as Pause2, Play as Play2, Trash2 as Trash22, X as X2 } from "lucide-react";
1670
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1671
+ var TYPE_LABELS = {
1672
+ image: "Image",
1673
+ video: "Video",
1674
+ speech: "Audio",
1675
+ avatar: "Avatar",
1676
+ transcription: "Transcript"
1677
+ };
1678
+ var FOCUSABLE_SELECTOR = [
1679
+ "a[href]",
1680
+ "button:not([disabled])",
1681
+ "input:not([disabled])",
1682
+ "textarea:not([disabled])",
1683
+ "select:not([disabled])",
1684
+ '[tabindex]:not([tabindex="-1"])'
1685
+ ].join(", ");
1686
+ var footerButtonClass = "inline-flex h-8 items-center gap-1.5 rounded-full border border-border px-3 text-[12.5px] font-medium hover:bg-accent";
1687
+ function hasOpenPopover() {
1688
+ return typeof document !== "undefined" && document.querySelector(`[${POPOVER_SURFACE_ATTR}]`) !== null;
1689
+ }
1690
+ function Waveform({ generation }) {
1691
+ const { registerPositionNode } = useStudioPlayback();
1692
+ const layersRef = useRef5(null);
1693
+ const bars = useMemo4(
1694
+ () => previewWaveformBars(generation.id, WIDE_WAVEFORM_BARS),
1695
+ [generation.id]
1696
+ );
1697
+ const hue = useMemo4(() => hashSeed(generation.id) % 360, [generation.id]);
1698
+ useEffect7(() => registerPositionNode(generation.id, layersRef.current), [generation.id, registerPositionNode]);
1699
+ const renderBars = () => bars.map((bar, index) => /* @__PURE__ */ jsx7("i", { style: { height: `${bar.heightPct}%`, opacity: bar.opacity } }, index));
1700
+ return /* @__PURE__ */ jsx7(
1701
+ "div",
1702
+ {
1703
+ className: "absolute inset-0",
1704
+ style: { background: `linear-gradient(158deg, hsl(${hue} 38% 24%), hsl(${(hue + 34) % 360} 46% 13%))` },
1705
+ children: /* @__PURE__ */ jsxs6("div", { ref: layersRef, className: "studio-wave-layers", style: { color: `hsl(${hue} 92% 74%)` }, children: [
1706
+ /* @__PURE__ */ jsx7("div", { className: "studio-wave studio-wave-base", children: renderBars() }),
1707
+ /* @__PURE__ */ jsx7("div", { className: "studio-wave studio-wave-play", children: renderBars() })
1708
+ ] })
1709
+ }
1710
+ );
1711
+ }
1712
+ function AudioTransport({ generation }) {
1713
+ const playback = useStudioPlayback();
1714
+ const elapsedRef = useRef5(null);
1715
+ const seekRef = useRef5(null);
1716
+ const trackRef = useRef5(null);
1717
+ const pointerIdRef = useRef5(null);
1718
+ const [ariaPosition, setAriaPosition] = useState7(0);
1719
+ const isPlaying = playback.activeId === generation.id && playback.playing;
1720
+ const duration = playback.activeId === generation.id ? playback.durationSeconds : 0;
1721
+ useEffect7(() => playback.registerTimeNode(elapsedRef.current), [playback.registerTimeNode]);
1722
+ useEffect7(
1723
+ () => playback.registerPositionNode(generation.id, seekRef.current),
1724
+ [generation.id, playback.registerPositionNode]
1725
+ );
1726
+ useEffect7(() => {
1727
+ setAriaPosition(playback.activeId === generation.id ? playback.getPositionSeconds() : 0);
1728
+ }, [generation.id, playback.activeId, playback.durationSeconds, playback.getPositionSeconds, playback.playing]);
1729
+ const syncAriaPosition = useCallback4(() => {
1730
+ setAriaPosition(playback.activeId === generation.id ? playback.getPositionSeconds() : 0);
1731
+ }, [generation.id, playback]);
1732
+ function toggle() {
1733
+ playback.toggle(generation);
1734
+ syncAriaPosition();
1735
+ }
1736
+ function seekFromClientX(clientX) {
1737
+ const rect = trackRef.current?.getBoundingClientRect();
1738
+ if (!rect || rect.width <= 0 || duration <= 0) return;
1739
+ const fraction = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1740
+ playback.seekTo(generation, fraction * duration);
1741
+ setAriaPosition(fraction * duration);
1742
+ }
1743
+ function onPointerDown(event) {
1744
+ event.currentTarget.focus();
1745
+ pointerIdRef.current = event.pointerId;
1746
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1747
+ seekFromClientX(event.clientX);
1748
+ }
1749
+ function onPointerMove(event) {
1750
+ if (pointerIdRef.current !== event.pointerId) return;
1751
+ seekFromClientX(event.clientX);
1752
+ }
1753
+ function endPointer(event) {
1754
+ if (pointerIdRef.current !== event.pointerId) return;
1755
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
1756
+ pointerIdRef.current = null;
1757
+ syncAriaPosition();
1758
+ }
1759
+ function onKeyDown(event) {
1760
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
1761
+ event.preventDefault();
1762
+ playback.seekBy(event.key === "ArrowLeft" ? -5 : 5);
1763
+ syncAriaPosition();
1764
+ } else if (event.key === " " || event.code === "Space") {
1765
+ event.preventDefault();
1766
+ toggle();
1767
+ }
1768
+ }
1769
+ const position = Math.max(0, duration > 0 ? Math.min(ariaPosition, duration) : ariaPosition);
1770
+ return /* @__PURE__ */ jsxs6("div", { className: "mt-3.5 flex items-center gap-3 px-0.5", children: [
1771
+ /* @__PURE__ */ jsx7(
1772
+ "button",
1773
+ {
1774
+ type: "button",
1775
+ "aria-label": isPlaying ? "Pause" : "Play",
1776
+ onClick: toggle,
1777
+ className: "grid h-[38px] w-[38px] shrink-0 place-items-center rounded-full bg-primary text-primary-foreground shadow-raised hover:brightness-110",
1778
+ children: isPlaying ? /* @__PURE__ */ jsx7(Pause2, { size: 15, strokeWidth: 1.5, fill: "currentColor" }) : /* @__PURE__ */ jsx7(Play2, { size: 15, strokeWidth: 1.5, fill: "currentColor" })
1779
+ }
1780
+ ),
1781
+ /* @__PURE__ */ jsx7("span", { ref: elapsedRef, className: "min-w-8 text-[12px] text-muted-foreground tabular-nums", children: duration > 0 ? "0:00" : "--:--" }),
1782
+ /* @__PURE__ */ jsxs6(
1783
+ "div",
1319
1784
  {
1320
- count: mergedGenerations.length,
1321
- canGenerate,
1322
- onOpenLibrary: openLibrary
1785
+ ref: seekRef,
1786
+ className: "studio-seek relative flex h-5 min-w-0 flex-1 cursor-pointer items-center",
1787
+ role: "slider",
1788
+ tabIndex: 0,
1789
+ "aria-label": "Seek",
1790
+ "aria-valuemin": 0,
1791
+ "aria-valuemax": duration,
1792
+ "aria-valuenow": position,
1793
+ "aria-valuetext": duration > 0 ? formatClock(position) : "--:--",
1794
+ onPointerDown,
1795
+ onPointerMove,
1796
+ onPointerUp: endPointer,
1797
+ onPointerCancel: endPointer,
1798
+ onKeyDown,
1799
+ children: [
1800
+ /* @__PURE__ */ jsx7("div", { ref: trackRef, className: "relative h-1 w-full overflow-hidden rounded-full bg-accent", children: /* @__PURE__ */ jsx7("div", { className: "studio-seek-fill absolute inset-y-0 left-0 bg-primary" }) }),
1801
+ /* @__PURE__ */ jsx7("div", { className: "studio-seek-thumb absolute h-[13px] w-[13px] -translate-x-1/2 rounded-full bg-primary ring-2 ring-card" })
1802
+ ]
1323
1803
  }
1324
1804
  ),
1325
- /* @__PURE__ */ jsx10("div", { className: "mx-auto w-full max-w-3xl space-y-4 px-4 py-6 sm:px-6", children: canGenerate ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
1805
+ /* @__PURE__ */ jsx7("span", { className: "text-[12px] text-muted-foreground tabular-nums", children: duration > 0 ? formatClock(duration) : "--:--" })
1806
+ ] });
1807
+ }
1808
+ function MediaViewerModal({
1809
+ generation,
1810
+ onClose,
1811
+ actions,
1812
+ onRequestDelete,
1813
+ onSaved
1814
+ }) {
1815
+ const playback = useStudioPlayback();
1816
+ const stopPlayback = playback.stop;
1817
+ const panelRef = useRef5(null);
1818
+ const closeRef = useRef5(null);
1819
+ const priorFocusRef = useRef5(null);
1820
+ const openSessionRef = useRef5(false);
1821
+ const [saveOpen, setSaveOpen] = useState7(false);
1822
+ const [savePending, setSavePending] = useState7(false);
1823
+ const popover = usePopover(saveOpen, setSaveOpen);
1824
+ const open = generation !== null;
1825
+ const stopAndRestore = useCallback4(() => {
1826
+ if (!openSessionRef.current) return;
1827
+ openSessionRef.current = false;
1828
+ stopPlayback();
1829
+ const priorFocus = priorFocusRef.current;
1830
+ if (priorFocus?.isConnected) priorFocus.focus();
1831
+ }, [stopPlayback]);
1832
+ const close = useCallback4(() => {
1833
+ setSaveOpen(false);
1834
+ stopAndRestore();
1835
+ onClose();
1836
+ }, [onClose, stopAndRestore]);
1837
+ useEffect7(() => {
1838
+ if (!open) return;
1839
+ priorFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1840
+ openSessionRef.current = true;
1841
+ closeRef.current?.focus();
1842
+ return stopAndRestore;
1843
+ }, [open, stopAndRestore]);
1844
+ useEffect7(() => {
1845
+ if (!open) return;
1846
+ function onDocumentKeyDown(event) {
1847
+ if (event.key !== "Escape" || event.defaultPrevented || hasOpenPopover()) return;
1848
+ event.preventDefault();
1849
+ close();
1850
+ }
1851
+ document.addEventListener("keydown", onDocumentKeyDown);
1852
+ return () => document.removeEventListener("keydown", onDocumentKeyDown);
1853
+ }, [close, open]);
1854
+ if (!generation || typeof document === "undefined") return null;
1855
+ const ratio = generationAspectRatio(generation);
1856
+ const status = generationStatus(generation);
1857
+ const vaultPath = generationVaultPath(generation);
1858
+ const metaSegments = [
1859
+ TYPE_LABELS[generation.type] ?? generation.type,
1860
+ generation.model,
1861
+ ...generationSpecSegments(generation),
1862
+ generation.createdAt ? relativeTime(generation.createdAt) : null,
1863
+ vaultPath
1864
+ ].filter((segment) => typeof segment === "string" && segment.length > 0);
1865
+ function onPanelKeyDown(event) {
1866
+ if (event.key !== "Tab" || hasOpenPopover()) return;
1867
+ const focusable = panelRef.current?.querySelectorAll(FOCUSABLE_SELECTOR);
1868
+ if (!focusable || focusable.length === 0) return;
1869
+ const first = focusable[0];
1870
+ const last = focusable[focusable.length - 1];
1871
+ const active = document.activeElement;
1872
+ if (event.shiftKey && active === first) {
1873
+ event.preventDefault();
1874
+ last?.focus();
1875
+ } else if (!event.shiftKey && active === last) {
1876
+ event.preventDefault();
1877
+ first?.focus();
1878
+ }
1879
+ }
1880
+ async function save(path) {
1881
+ if (!actions?.save || !generation) return;
1882
+ setSavePending(true);
1883
+ try {
1884
+ const results = await actions.save({ generations: [generation], path });
1885
+ setSaveOpen(false);
1886
+ onSaved?.(results);
1887
+ } finally {
1888
+ setSavePending(false);
1889
+ }
1890
+ }
1891
+ function viewVault(event) {
1892
+ if (!actions?.onOpenVault || !generation) return;
1893
+ event.preventDefault();
1894
+ actions.onOpenVault(generation);
1895
+ }
1896
+ const mediaStyle = generation.type === "transcription" ? { width: "100%" } : {
1897
+ width: `min(100%, calc(70vh * ${ratio}))`,
1898
+ aspectRatio: ratio
1899
+ };
1900
+ const vaultHref = vaultPath && actions?.vaultHref ? actions.vaultHref(vaultPath) : null;
1901
+ const viewControl = vaultPath && (vaultHref || actions?.onOpenVault);
1902
+ const modal = /* @__PURE__ */ jsx7(
1903
+ "div",
1904
+ {
1905
+ className: "studio-layer-viewer studio-backdrop fixed inset-0 grid place-items-center p-6",
1906
+ onMouseDown: (event) => {
1907
+ if (event.target === event.currentTarget) close();
1908
+ },
1909
+ children: /* @__PURE__ */ jsxs6(
1910
+ "div",
1911
+ {
1912
+ ref: panelRef,
1913
+ role: "dialog",
1914
+ "aria-modal": "true",
1915
+ "aria-label": "Media detail",
1916
+ onKeyDown: onPanelKeyDown,
1917
+ className: `studio-rise relative max-h-[92vh] w-[min(780px,100%)] overflow-auto rounded-[14px] border border-border bg-card p-3.5 ${OVERLAY_SHADOW}`,
1918
+ children: [
1919
+ /* @__PURE__ */ jsx7(
1920
+ "button",
1921
+ {
1922
+ ref: closeRef,
1923
+ type: "button",
1924
+ "aria-label": "Close",
1925
+ onClick: close,
1926
+ className: "absolute right-3.5 top-3.5 z-10 grid h-8 w-8 place-items-center rounded-full border border-border bg-card hover:bg-accent",
1927
+ children: /* @__PURE__ */ jsx7(X2, { size: 15, strokeWidth: 1.5 })
1928
+ }
1929
+ ),
1930
+ /* @__PURE__ */ jsx7("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx7("div", { className: "relative overflow-hidden rounded-xl border border-border bg-accent", style: mediaStyle, children: status === "pending" || status === "running" ? /* @__PURE__ */ jsx7("div", { className: "studio-skeleton absolute inset-0" }) : status === "failed" ? /* @__PURE__ */ jsx7("div", { className: "grid h-full w-full place-items-center bg-accent p-3 text-center text-[12px] text-destructive", children: generationError(generation) ?? "Generation failed" }) : generation.result === null ? /* @__PURE__ */ jsx7("div", { className: "studio-skeleton absolute inset-0" }) : generation.type === "image" ? /* @__PURE__ */ jsx7("img", { src: generation.result, className: "h-full w-full object-cover", alt: generation.prompt }) : generation.type === "video" || generation.type === "avatar" ? /* @__PURE__ */ jsx7("video", { src: generation.result, controls: true, playsInline: true, className: "h-full w-full object-cover" }) : generation.type === "transcription" ? /* @__PURE__ */ jsx7("div", { className: "max-h-[60vh] overflow-auto whitespace-pre-wrap p-4 text-[13.5px] leading-relaxed", children: generation.result ?? "" }) : generation.type === "speech" ? /* @__PURE__ */ jsx7(Waveform, { generation }) : null }) }),
1931
+ status !== "failed" && generation.type === "speech" && /* @__PURE__ */ jsx7(AudioTransport, { generation }),
1932
+ /* @__PURE__ */ jsx7("p", { className: "mx-0.5 mb-1.5 mt-4 max-w-[62ch] text-[15px] leading-normal", children: generation.prompt }),
1933
+ /* @__PURE__ */ jsx7("div", { className: "mx-0.5 flex flex-wrap items-center gap-1.5 text-[12.5px] text-muted-foreground", children: metaSegments.join(" \xB7 ") }),
1934
+ /* @__PURE__ */ jsxs6("div", { className: "mt-3.5 flex flex-wrap gap-2 border-t border-border pt-4", children: [
1935
+ /* @__PURE__ */ jsxs6(
1936
+ "button",
1937
+ {
1938
+ type: "button",
1939
+ onClick: () => {
1940
+ void (actions?.download ?? downloadGenerationsViaAnchor)([generation]);
1941
+ },
1942
+ className: footerButtonClass,
1943
+ children: [
1944
+ /* @__PURE__ */ jsx7(Download2, { size: 15, strokeWidth: 1.5 }),
1945
+ " Download"
1946
+ ]
1947
+ }
1948
+ ),
1949
+ !vaultPath && actions?.save && /* @__PURE__ */ jsxs6("div", { ref: popover.containerRef, children: [
1950
+ /* @__PURE__ */ jsxs6(
1951
+ "button",
1952
+ {
1953
+ ...popover.triggerProps,
1954
+ type: "button",
1955
+ onClick: () => setSaveOpen((value) => !value),
1956
+ className: footerButtonClass,
1957
+ children: [
1958
+ /* @__PURE__ */ jsx7(FolderPlus2, { size: 15, strokeWidth: 1.5 }),
1959
+ " Save to vault"
1960
+ ]
1961
+ }
1962
+ ),
1963
+ /* @__PURE__ */ jsx7(
1964
+ VaultPathPopover,
1965
+ {
1966
+ open: saveOpen,
1967
+ triggerRef: popover.triggerRef,
1968
+ panelRef: popover.panelRef,
1969
+ generations: [generation],
1970
+ onSubmit: save,
1971
+ onCancel: () => setSaveOpen(false),
1972
+ pending: savePending
1973
+ }
1974
+ )
1975
+ ] }),
1976
+ viewControl && (vaultHref ? /* @__PURE__ */ jsxs6("a", { href: vaultHref, onClick: viewVault, className: footerButtonClass, children: [
1977
+ /* @__PURE__ */ jsx7(FolderOpen2, { size: 15, strokeWidth: 1.5 }),
1978
+ " View in vault"
1979
+ ] }) : /* @__PURE__ */ jsxs6("button", { type: "button", onClick: viewVault, className: footerButtonClass, children: [
1980
+ /* @__PURE__ */ jsx7(FolderOpen2, { size: 15, strokeWidth: 1.5 }),
1981
+ " View in vault"
1982
+ ] })),
1983
+ onRequestDelete && /* @__PURE__ */ jsxs6(
1984
+ "button",
1985
+ {
1986
+ type: "button",
1987
+ onClick: () => onRequestDelete(generation),
1988
+ className: `${footerButtonClass} ml-auto text-destructive hover:border-destructive hover:bg-destructive/10`,
1989
+ children: [
1990
+ /* @__PURE__ */ jsx7(Trash22, { size: 15, strokeWidth: 1.5 }),
1991
+ " Delete"
1992
+ ]
1993
+ }
1994
+ )
1995
+ ] })
1996
+ ]
1997
+ }
1998
+ )
1999
+ }
2000
+ );
2001
+ return createPortal(modal, document.body);
2002
+ }
2003
+
2004
+ // src/studio-react/studio-home-screen.tsx
2005
+ import {
2006
+ useCallback as useCallback8,
2007
+ useEffect as useEffect11,
2008
+ useMemo as useMemo6,
2009
+ useState as useState10
2010
+ } from "react";
2011
+
2012
+ // src/studio-react/studio-confirm.tsx
2013
+ import { useEffect as useEffect8, useId as useId3, useRef as useRef6 } from "react";
2014
+ import { createPortal as createPortal2 } from "react-dom";
2015
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2016
+ function StudioConfirmDialog({
2017
+ open,
2018
+ count,
2019
+ onConfirm,
2020
+ onCancel
2021
+ }) {
2022
+ const panelRef = useRef6(null);
2023
+ const cancelRef = useRef6(null);
2024
+ const previousFocusRef = useRef6(null);
2025
+ const titleId = useId3();
2026
+ const descriptionId = useId3();
2027
+ useEffect8(() => {
2028
+ if (!open) return;
2029
+ previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2030
+ cancelRef.current?.focus();
2031
+ return () => {
2032
+ const previous = previousFocusRef.current;
2033
+ if (previous && document.contains(previous)) previous.focus();
2034
+ previousFocusRef.current = null;
2035
+ };
2036
+ }, [open]);
2037
+ if (!open || typeof document === "undefined") return null;
2038
+ function onKeyDown(event) {
2039
+ if (event.key === "Escape") {
2040
+ event.preventDefault();
2041
+ onCancel();
2042
+ return;
2043
+ }
2044
+ if (event.key !== "Tab") return;
2045
+ const focusable = panelRef.current?.querySelectorAll(
2046
+ 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
2047
+ );
2048
+ if (!focusable || focusable.length === 0) return;
2049
+ const first = focusable[0];
2050
+ const last = focusable[focusable.length - 1];
2051
+ const active = document.activeElement;
2052
+ if (event.shiftKey && active === first) {
2053
+ event.preventDefault();
2054
+ last?.focus();
2055
+ } else if (!event.shiftKey && active === last) {
2056
+ event.preventDefault();
2057
+ first?.focus();
2058
+ }
2059
+ }
2060
+ return createPortal2(
2061
+ /* @__PURE__ */ jsx8(
2062
+ "div",
2063
+ {
2064
+ className: "studio-layer-confirm studio-backdrop fixed inset-0 grid place-items-center p-6",
2065
+ onMouseDown: (event) => {
2066
+ if (event.target === event.currentTarget) onCancel();
2067
+ },
2068
+ children: /* @__PURE__ */ jsxs7(
2069
+ "div",
2070
+ {
2071
+ ref: panelRef,
2072
+ role: "alertdialog",
2073
+ "aria-modal": "true",
2074
+ "aria-labelledby": titleId,
2075
+ "aria-describedby": descriptionId,
2076
+ onKeyDown,
2077
+ className: `w-[min(360px,100%)] rounded-[14px] border border-border bg-card px-[18px] pb-4 pt-[18px] ${OVERLAY_SHADOW}`,
2078
+ children: [
2079
+ /* @__PURE__ */ jsxs7("h2", { id: titleId, className: "text-[15.5px] font-semibold tracking-[-0.01em]", children: [
2080
+ "Delete ",
2081
+ count,
2082
+ " item",
2083
+ count > 1 ? "s" : "",
2084
+ "?"
2085
+ ] }),
2086
+ /* @__PURE__ */ jsx8("p", { id: descriptionId, className: "mt-[7px] text-[13px] text-muted-foreground", children: "This cannot be undone." }),
2087
+ /* @__PURE__ */ jsxs7("div", { className: "flex justify-end gap-2 pt-[18px]", children: [
2088
+ /* @__PURE__ */ jsx8(
2089
+ "button",
2090
+ {
2091
+ ref: cancelRef,
2092
+ type: "button",
2093
+ onClick: onCancel,
2094
+ className: "h-[30px] rounded-full border border-border px-3.5 text-[13px] hover:bg-accent",
2095
+ children: "Cancel"
2096
+ }
2097
+ ),
2098
+ /* @__PURE__ */ jsx8(
2099
+ "button",
2100
+ {
2101
+ type: "button",
2102
+ onClick: onConfirm,
2103
+ className: "h-[30px] rounded-full bg-destructive px-3.5 text-[13px] font-medium text-destructive-foreground hover:brightness-110",
2104
+ children: "Delete"
2105
+ }
2106
+ )
2107
+ ] })
2108
+ ]
2109
+ }
2110
+ )
2111
+ }
2112
+ ),
2113
+ document.body
2114
+ );
2115
+ }
2116
+
2117
+ // src/studio-react/studio-toasts.tsx
2118
+ import {
2119
+ createContext as createContext2,
2120
+ useCallback as useCallback5,
2121
+ useContext as useContext2,
2122
+ useEffect as useEffect9,
2123
+ useMemo as useMemo5,
2124
+ useRef as useRef7,
2125
+ useState as useState8
2126
+ } from "react";
2127
+ import { createPortal as createPortal3 } from "react-dom";
2128
+ import { X as X3 } from "lucide-react";
2129
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2130
+ var StudioToastContext = createContext2(null);
2131
+ function StudioToast({ record, leave }) {
2132
+ useEffect9(() => {
2133
+ if (record.leaving) return;
2134
+ const duration = record.durationMs ?? 3500;
2135
+ if (duration === 0) return;
2136
+ const timer = window.setTimeout(() => leave(record.id, "timeout"), duration);
2137
+ return () => window.clearTimeout(timer);
2138
+ }, [leave, record.durationMs, record.id, record.leaving]);
2139
+ return /* @__PURE__ */ jsxs8(
2140
+ "div",
2141
+ {
2142
+ role: "status",
2143
+ className: `pointer-events-auto flex max-w-[min(94vw,480px)] items-center gap-2.5 rounded-[10px] border border-border bg-card py-2 pl-[13px] pr-2 text-[13px] ${OVERLAY_SHADOW} ${record.leaving ? "studio-toast-out" : "studio-toast-in"}`,
2144
+ children: [
2145
+ /* @__PURE__ */ jsx9("span", { className: "min-w-0 truncate", children: record.message }),
2146
+ record.action && /* @__PURE__ */ jsx9(
2147
+ "button",
2148
+ {
2149
+ type: "button",
2150
+ onClick: () => {
2151
+ record.action?.run();
2152
+ leave(record.id, "action");
2153
+ },
2154
+ className: "rounded-lg border-0 px-2 py-1 text-[13px] font-semibold text-primary hover:bg-primary/10",
2155
+ children: record.action.label
2156
+ }
2157
+ ),
2158
+ /* @__PURE__ */ jsx9(
2159
+ "button",
2160
+ {
2161
+ type: "button",
2162
+ "aria-label": "Dismiss",
2163
+ onClick: () => leave(record.id, "dismissed"),
2164
+ className: "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground",
2165
+ children: /* @__PURE__ */ jsx9(X3, { className: "h-3.5 w-3.5", "aria-hidden": true })
2166
+ }
2167
+ )
2168
+ ]
2169
+ }
2170
+ );
2171
+ }
2172
+ function StudioToastProvider({ children }) {
2173
+ const [toasts, setToasts] = useState8([]);
2174
+ const [dockLift, setDockLift] = useState8(null);
2175
+ const sequence = useRef7(0);
2176
+ const dismissed = useRef7(/* @__PURE__ */ new Set());
2177
+ const records = useRef7(/* @__PURE__ */ new Map());
2178
+ const leave = useCallback5((id, reason) => {
2179
+ if (dismissed.current.has(id)) return;
2180
+ const record = records.current.get(id);
2181
+ if (!record) return;
2182
+ dismissed.current.add(id);
2183
+ record.onDismiss?.(reason);
2184
+ setToasts((current) => current.map((toast2) => toast2.id === id ? { ...toast2, leaving: true } : toast2));
2185
+ window.setTimeout(() => {
2186
+ records.current.delete(id);
2187
+ setToasts((current) => current.filter((toast2) => toast2.id !== id));
2188
+ }, 180);
2189
+ }, []);
2190
+ const toast = useCallback5((input) => {
2191
+ const id = `studio-toast-${++sequence.current}`;
2192
+ const record = { ...input, id, leaving: false };
2193
+ records.current.set(id, record);
2194
+ setToasts((current) => [...current, record]);
2195
+ return id;
2196
+ }, []);
2197
+ const dismiss = useCallback5((id) => leave(id, "dismissed"), [leave]);
2198
+ const value = useMemo5(() => ({ toast, dismiss, setDockLift }), [dismiss, toast]);
2199
+ return /* @__PURE__ */ jsxs8(StudioToastContext.Provider, { value, children: [
2200
+ children,
2201
+ typeof document !== "undefined" && createPortal3(
2202
+ /* @__PURE__ */ jsx9(
2203
+ "div",
2204
+ {
2205
+ role: "region",
2206
+ "aria-label": "Notifications",
2207
+ style: { bottom: dockLift ? dockLift + 10 : 22 },
2208
+ className: "studio-layer-toasts pointer-events-none fixed left-1/2 flex -translate-x-1/2 flex-col items-center gap-2",
2209
+ children: toasts.map((record) => /* @__PURE__ */ jsx9(StudioToast, { record, leave }, record.id))
2210
+ }
2211
+ ),
2212
+ document.body
2213
+ )
2214
+ ] });
2215
+ }
2216
+ function useStudioToast() {
2217
+ const context = useContext2(StudioToastContext);
2218
+ if (!context) throw new Error("useStudioToast must be used within a StudioToastProvider");
2219
+ return context;
2220
+ }
2221
+
2222
+ // src/studio-react/use-batch-navigation.ts
2223
+ import { useCallback as useCallback6, useRef as useRef8 } from "react";
2224
+ function useBatchNavigation({
2225
+ seed,
2226
+ currentBatchKey,
2227
+ onOpenGeneration
2228
+ }) {
2229
+ const seenKeys = useRef8(null);
2230
+ seenKeys.current ??= new Set(seed.map(generationBatchKey));
2231
+ const currentRef = useRef8(currentBatchKey);
2232
+ currentRef.current = currentBatchKey;
2233
+ return useCallback6((generation) => {
2234
+ const key = generationBatchKey(generation);
2235
+ const seen = seenKeys.current;
2236
+ if (key === currentRef.current || !seen || seen.has(key)) return;
2237
+ seen.add(key);
2238
+ onOpenGeneration(key, generation);
2239
+ }, [onOpenGeneration]);
2240
+ }
2241
+
2242
+ // src/studio-react/use-deferred-delete.ts
2243
+ import { useCallback as useCallback7, useEffect as useEffect10, useRef as useRef9, useState as useState9 } from "react";
2244
+ var itemLabel = (count) => `item${count === 1 ? "" : "s"}`;
2245
+ function useDeferredDelete(options) {
2246
+ const { activeId, stop: stop2 } = useStudioPlayback();
2247
+ const { dismiss, toast } = useStudioToast();
2248
+ const [pendingIds, setPendingIds] = useState9(() => /* @__PURE__ */ new Set());
2249
+ const batchesRef = useRef9(/* @__PURE__ */ new Map());
2250
+ const committedIdsRef = useRef9(/* @__PURE__ */ new Set());
2251
+ const sequenceRef = useRef9(0);
2252
+ const optionsRef = useRef9(options);
2253
+ const dismissRef = useRef9(dismiss);
2254
+ optionsRef.current = options;
2255
+ dismissRef.current = dismiss;
2256
+ const visiblePendingIds = useCallback7(() => {
2257
+ const ids = new Set(committedIdsRef.current);
2258
+ for (const batch of batchesRef.current.values()) {
2259
+ for (const id of batch.ids) ids.add(id);
2260
+ }
2261
+ return ids;
2262
+ }, []);
2263
+ const commit = useCallback7((batchKey) => {
2264
+ const batch = batchesRef.current.get(batchKey);
2265
+ if (!batch || batch.status !== "pending") return;
2266
+ batch.status = "committing";
2267
+ void optionsRef.current.remove(batch.ids).then(() => {
2268
+ if (batchesRef.current.get(batchKey) !== batch) return;
2269
+ batchesRef.current.delete(batchKey);
2270
+ for (const id of batch.ids) committedIdsRef.current.add(id);
2271
+ optionsRef.current.onCommitted?.(batch.ids);
2272
+ }, () => {
2273
+ if (batchesRef.current.get(batchKey) !== batch) return;
2274
+ batchesRef.current.delete(batchKey);
2275
+ setPendingIds(visiblePendingIds());
2276
+ toast({ message: `Could not delete ${batch.ids.length} ${itemLabel(batch.ids.length)}` });
2277
+ optionsRef.current.onRestoreFailed?.(batch.ids);
2278
+ });
2279
+ }, [toast, visiblePendingIds]);
2280
+ const request = useCallback7((generations) => {
2281
+ if (generations.length === 0) return;
2282
+ const ids = [...new Set(generations.map((generation) => generation.id))];
2283
+ const batchKey = `deferred-delete-${++sequenceRef.current}`;
2284
+ setPendingIds((current) => {
2285
+ const next = new Set(current);
2286
+ for (const id of ids) next.add(id);
2287
+ return next;
2288
+ });
2289
+ if (activeId && ids.includes(activeId)) stop2();
2290
+ const undo = () => {
2291
+ const batch = batchesRef.current.get(batchKey);
2292
+ if (!batch || batch.status !== "pending") return;
2293
+ batchesRef.current.delete(batchKey);
2294
+ setPendingIds(visiblePendingIds());
2295
+ toast({ message: `Restored ${ids.length} ${itemLabel(ids.length)}` });
2296
+ };
2297
+ const toastId = toast({
2298
+ message: `Deleted ${ids.length} ${itemLabel(ids.length)}`,
2299
+ action: { label: "Undo", run: undo },
2300
+ durationMs: options.undoWindowMs ?? 3500,
2301
+ onDismiss: (reason) => {
2302
+ if (reason !== "action") commit(batchKey);
2303
+ }
2304
+ });
2305
+ batchesRef.current.set(batchKey, { ids, toastId, status: "pending" });
2306
+ }, [activeId, commit, options.undoWindowMs, stop2, toast, visiblePendingIds]);
2307
+ const flush = useCallback7(() => {
2308
+ for (const [batchKey, batch] of batchesRef.current) {
2309
+ if (batch.status !== "pending") continue;
2310
+ batch.status = "committing";
2311
+ batchesRef.current.delete(batchKey);
2312
+ dismissRef.current(batch.toastId);
2313
+ void optionsRef.current.remove(batch.ids).catch(() => {
2314
+ });
2315
+ }
2316
+ }, []);
2317
+ useEffect10(() => {
2318
+ window.addEventListener("pagehide", flush);
2319
+ return () => {
2320
+ window.removeEventListener("pagehide", flush);
2321
+ flush();
2322
+ };
2323
+ }, [flush]);
2324
+ return { pendingIds, request, flush };
2325
+ }
2326
+
2327
+ // src/studio-react/studio-home-screen.tsx
2328
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2329
+ function StudioHomeScreen({
2330
+ generations,
2331
+ onGenerated,
2332
+ onOpenGeneration,
2333
+ onOpenHistory,
2334
+ workspaceId,
2335
+ pickReferenceImage,
2336
+ actions,
2337
+ recentLimit = 20,
2338
+ className
2339
+ }) {
2340
+ const [viewer, setViewer] = useState10(null);
2341
+ const [confirmTargets, setConfirmTargets] = useState10(null);
2342
+ const playback = useStudioPlayback();
2343
+ const { toast } = useStudioToast();
2344
+ const deferredDelete = useDeferredDelete({
2345
+ remove: actions?.remove ?? (async () => {
2346
+ })
2347
+ });
2348
+ const navigateNewBatch = useBatchNavigation({ seed: generations, onOpenGeneration });
2349
+ const wrappedOnGenerated = useCallback8((generation) => {
2350
+ onGenerated(generation);
2351
+ navigateNewBatch(generation);
2352
+ }, [navigateNewBatch, onGenerated]);
2353
+ const visible = useMemo6(
2354
+ () => generations.filter((generation) => !deferredDelete.pendingIds.has(generation.id)),
2355
+ [deferredDelete.pendingIds, generations]
2356
+ );
2357
+ const renderedViewer = viewer ? generations.find((generation) => generation.id === viewer.id) ?? viewer : null;
2358
+ const requestDelete = actions?.remove ? (generation) => setConfirmTargets([generation]) : void 0;
2359
+ const onSaved = useCallback8((results) => {
2360
+ const first = results[0];
2361
+ if (first) toast({ message: `Saved to vault \xB7 ${first.vaultPath}` });
2362
+ }, [toast]);
2363
+ useEffect11(() => playback.stop, [playback.stop]);
2364
+ function confirmDelete() {
2365
+ const targets = confirmTargets;
2366
+ setConfirmTargets(null);
2367
+ if (!targets?.length) return;
2368
+ if (viewer && targets.some((target) => target.id === viewer.id)) setViewer(null);
2369
+ deferredDelete.request(targets);
2370
+ }
2371
+ return /* @__PURE__ */ jsxs9("main", { className, children: [
2372
+ /* @__PURE__ */ jsxs9("div", { className: "studio-home-top mx-auto w-full max-w-[820px] px-6 pt-[clamp(24px,12vh,124px)] max-[900px]:px-4", children: [
2373
+ /* @__PURE__ */ jsx10("h1", { className: "mb-5 text-center text-[30px] font-semibold leading-[1.2] tracking-[-0.02em] [text-wrap:balance] max-[640px]:text-[25px]", children: "What do you want to create?" }),
1326
2374
  /* @__PURE__ */ jsx10(
1327
2375
  StudioComposer,
1328
2376
  {
2377
+ variant: "home",
1329
2378
  workspaceId,
1330
- onGenerated: (generation) => {
1331
- onGenerated(generation);
1332
- requestAnimationFrame(() => {
1333
- canvasRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
1334
- });
2379
+ pickReferenceImage,
2380
+ onGenerated: wrappedOnGenerated
2381
+ }
2382
+ )
2383
+ ] }),
2384
+ /* @__PURE__ */ jsxs9("section", { className: "studio-home-recent pb-[72px]", children: [
2385
+ /* @__PURE__ */ jsxs9("div", { className: "mb-3 mt-[34px] flex items-center justify-between gap-3 px-6 max-[900px]:px-4", children: [
2386
+ /* @__PURE__ */ jsx10("h2", { className: "text-[13px] font-medium tracking-[0.02em] text-muted-foreground", children: "Recent media" }),
2387
+ /* @__PURE__ */ jsx10(
2388
+ "button",
2389
+ {
2390
+ type: "button",
2391
+ onClick: onOpenHistory,
2392
+ className: "h-[30px] whitespace-nowrap rounded-full border border-border bg-card px-3 text-[13px] shadow-sm hover:bg-accent",
2393
+ children: "View history"
1335
2394
  }
2395
+ )
2396
+ ] }),
2397
+ /* @__PURE__ */ jsx10("div", { className: "studio-grid studio-grid-library", children: visible.length === 0 ? /* @__PURE__ */ jsxs9("div", { className: "col-span-full flex flex-col items-center py-16 text-center", children: [
2398
+ /* @__PURE__ */ jsx10("p", { className: "text-[14px] text-foreground", children: "Nothing generated yet." }),
2399
+ /* @__PURE__ */ jsx10("p", { className: "max-w-[380px] text-[13px] text-muted-foreground", children: "Whatever you make lands here first \u2014 it only reaches the vault when you save it." })
2400
+ ] }) : visible.slice(0, recentLimit).map((generation) => /* @__PURE__ */ jsx10(
2401
+ MediaTile,
2402
+ {
2403
+ generation,
2404
+ context: "home",
2405
+ onOpen: setViewer,
2406
+ actions,
2407
+ onRequestDelete: requestDelete,
2408
+ onSaved
2409
+ },
2410
+ generation.id
2411
+ )) })
2412
+ ] }),
2413
+ /* @__PURE__ */ jsx10(
2414
+ MediaViewerModal,
2415
+ {
2416
+ generation: renderedViewer,
2417
+ onClose: () => setViewer(null),
2418
+ actions,
2419
+ onRequestDelete: requestDelete,
2420
+ onSaved
2421
+ }
2422
+ ),
2423
+ /* @__PURE__ */ jsx10(
2424
+ StudioConfirmDialog,
2425
+ {
2426
+ open: confirmTargets !== null,
2427
+ count: confirmTargets?.length ?? 0,
2428
+ onConfirm: confirmDelete,
2429
+ onCancel: () => setConfirmTargets(null)
2430
+ }
2431
+ )
2432
+ ] });
2433
+ }
2434
+
2435
+ // src/studio-react/studio-generation-screen.tsx
2436
+ import { ArrowLeft } from "lucide-react";
2437
+ import {
2438
+ useCallback as useCallback9,
2439
+ useEffect as useEffect12,
2440
+ useMemo as useMemo7,
2441
+ useRef as useRef10,
2442
+ useState as useState11
2443
+ } from "react";
2444
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2445
+ var FALLBACK_DOCK_HEIGHT = 190;
2446
+ var noRemove = async () => {
2447
+ };
2448
+ function isRunning(generation) {
2449
+ const status = generationStatus(generation);
2450
+ return status === "pending" || status === "running";
2451
+ }
2452
+ function StudioGenerationScreen({
2453
+ generations,
2454
+ batchKey,
2455
+ onGenerated,
2456
+ onOpenGeneration,
2457
+ onBack,
2458
+ workspaceId,
2459
+ pickReferenceImage,
2460
+ actions,
2461
+ className
2462
+ }) {
2463
+ const rootRef = useRef10(null);
2464
+ const dockRef = useRef10(null);
2465
+ const { setDockLift, toast } = useStudioToast();
2466
+ const { stop: stop2 } = useStudioPlayback();
2467
+ const stopRef = useRef10(stop2);
2468
+ stopRef.current = stop2;
2469
+ const [dockHeight, setDockHeight] = useState11(FALLBACK_DOCK_HEIGHT);
2470
+ const [viewerId, setViewerId] = useState11(null);
2471
+ const [deleteTarget, setDeleteTarget] = useState11(null);
2472
+ const remove = actions?.remove;
2473
+ const { pendingIds, request } = useDeferredDelete({ remove: remove ?? noRemove });
2474
+ const rows = useMemo7(
2475
+ () => generationsInBatch(generations, batchKey).filter((row) => !pendingIds.has(row.id)),
2476
+ [batchKey, generations, pendingIds]
2477
+ );
2478
+ const first = rows[0];
2479
+ const prompt = first?.prompt ?? "";
2480
+ const ratio = first ? generationAspectRatio(first) : 1.5;
2481
+ const runningCount = rows.filter(isRunning).length;
2482
+ const settledOrder = /* @__PURE__ */ new Map();
2483
+ for (const row of rows) if (!isRunning(row)) settledOrder.set(row.id, settledOrder.size);
2484
+ const columns = rows.length === 4 ? 2 : Math.min(rows.length, 4) || 1;
2485
+ const maxWidth = rows.length > 1 || first?.type === "speech" ? 820 : ratio < 1 ? 380 : 640;
2486
+ const viewerGeneration = viewerId ? rows.find((row) => row.id === viewerId) ?? null : null;
2487
+ const onSaved = useCallback9((results) => {
2488
+ const saved = results[0];
2489
+ if (saved) toast({ message: `Saved to vault \xB7 ${saved.vaultPath}` });
2490
+ }, [toast]);
2491
+ const navigateNewBatch = useBatchNavigation({ seed: [], currentBatchKey: batchKey, onOpenGeneration });
2492
+ const handleGenerated = useCallback9((generation) => {
2493
+ onGenerated(generation);
2494
+ navigateNewBatch(generation);
2495
+ }, [navigateNewBatch, onGenerated]);
2496
+ useEffect12(() => {
2497
+ const dock = dockRef.current;
2498
+ if (!dock) return;
2499
+ const apply = (height) => {
2500
+ if (height <= 0) return;
2501
+ rootRef.current?.style.setProperty("--studio-dock-h", `${height}px`);
2502
+ setDockHeight(height);
2503
+ setDockLift(height);
2504
+ };
2505
+ const measure = () => apply(dock.getBoundingClientRect().height);
2506
+ measure();
2507
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver((entries) => apply(entries[0]?.contentRect.height ?? dock.getBoundingClientRect().height));
2508
+ observer?.observe(dock);
2509
+ window.addEventListener("resize", measure);
2510
+ return () => {
2511
+ observer?.disconnect();
2512
+ window.removeEventListener("resize", measure);
2513
+ setDockLift(null);
2514
+ };
2515
+ }, [setDockLift]);
2516
+ useEffect12(() => () => stopRef.current(), []);
2517
+ function confirmDelete() {
2518
+ if (!deleteTarget) return;
2519
+ request([deleteTarget]);
2520
+ if (viewerId === deleteTarget.id) setViewerId(null);
2521
+ setDeleteTarget(null);
2522
+ }
2523
+ return /* @__PURE__ */ jsxs10("div", { ref: rootRef, className: `flex min-h-full flex-col ${className ?? ""}`, children: [
2524
+ /* @__PURE__ */ jsxs10("header", { className: "studio-gen-head mx-auto flex w-full max-w-[868px] items-center gap-2.5 px-6 pb-4 pt-1 max-[900px]:px-4", children: [
2525
+ /* @__PURE__ */ jsx11(
2526
+ "button",
2527
+ {
2528
+ type: "button",
2529
+ "aria-label": "Back to Studio",
2530
+ onClick: onBack,
2531
+ className: "grid h-8 w-8 flex-none place-items-center rounded-full border border-border bg-card shadow-sm hover:bg-accent",
2532
+ children: /* @__PURE__ */ jsx11(ArrowLeft, { size: 16, strokeWidth: 1.5 })
1336
2533
  }
1337
2534
  ),
1338
- /* @__PURE__ */ jsx10("div", { ref: canvasRef, children: /* @__PURE__ */ jsx10(
1339
- ResultCanvas,
2535
+ /* @__PURE__ */ jsx11(
2536
+ "p",
1340
2537
  {
1341
- batch: latestBatch,
1342
- onOpenLibrary: openLibrary,
1343
- onSelect: openDetail
2538
+ title: prompt,
2539
+ className: "min-w-0 max-w-full truncate rounded-full border border-border bg-card px-4 py-2 text-[14px] shadow-sm",
2540
+ children: prompt
1344
2541
  }
1345
- ) })
1346
- ] }) : /* @__PURE__ */ jsxs9("section", { className: "flex flex-col items-center justify-center gap-3 rounded-xl border border-border bg-card p-10 text-center shadow-sm", children: [
1347
- /* @__PURE__ */ jsx10("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx10(Images2, { className: "h-6 w-6 text-muted-foreground/40" }) }),
1348
- /* @__PURE__ */ jsx10("p", { className: "text-sm font-medium text-foreground", children: mergedGenerations.length > 0 ? `${mergedGenerations.length} asset${mergedGenerations.length !== 1 ? "s" : ""} in this workspace` : "No generations yet" }),
1349
- /* @__PURE__ */ jsx10("p", { className: "max-w-sm text-xs text-muted-foreground", children: "Team generations appear in the library." }),
1350
- /* @__PURE__ */ jsxs9(Button4, { size: "sm", variant: "outline", onClick: openLibrary, children: [
1351
- /* @__PURE__ */ jsx10(Images2, { className: "mr-1.5 h-4 w-4" }),
1352
- "Open library"
1353
- ] })
1354
- ] }) }),
1355
- /* @__PURE__ */ jsx10(
1356
- LibraryDrawer,
2542
+ )
2543
+ ] }),
2544
+ /* @__PURE__ */ jsx11(
2545
+ "div",
1357
2546
  {
1358
- open: drawerOpen,
1359
- onOpenChange: (open) => {
1360
- setDrawerOpen(open);
1361
- if (!open) setSelected(null);
1362
- },
1363
- generations: mergedGenerations,
1364
- totalCost,
1365
- typeFilter,
1366
- onFilterChange: setFilter,
1367
- vaultHref: resolvedVaultHref,
1368
- selected,
1369
- onSelect: (generation) => {
1370
- setSelected(generation);
1371
- if (generation) setDrawerOpen(true);
1372
- }
2547
+ className: "studio-gen-body flex-1 px-6 max-[900px]:px-4",
2548
+ style: { paddingBottom: `${dockHeight + 16}px` },
2549
+ children: rows.length === 0 ? /* @__PURE__ */ jsxs10("div", { className: "mx-auto flex max-w-[520px] flex-col items-center gap-1 py-16 text-center", children: [
2550
+ /* @__PURE__ */ jsx11("p", { className: "text-[14px]", children: "Nothing left from this generation." }),
2551
+ /* @__PURE__ */ jsx11("p", { className: "text-[13px] text-muted-foreground", children: "Send another prompt below to start a new one." })
2552
+ ] }) : /* @__PURE__ */ jsxs10(Fragment2, { children: [
2553
+ /* @__PURE__ */ jsx11(
2554
+ "div",
2555
+ {
2556
+ className: "mx-auto grid gap-[3px]",
2557
+ "aria-busy": runningCount > 0,
2558
+ style: { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, maxWidth },
2559
+ children: rows.map((row) => {
2560
+ if (isRunning(row)) {
2561
+ return /* @__PURE__ */ jsx11("div", { className: "studio-skeleton", style: { "--r": ratio } }, row.id);
2562
+ }
2563
+ if (generationStatus(row) === "failed") {
2564
+ return /* @__PURE__ */ jsx11(
2565
+ "div",
2566
+ {
2567
+ className: "grid place-items-center bg-accent p-3 text-center text-[12px] text-destructive",
2568
+ style: { aspectRatio: ratio },
2569
+ children: generationError(row) ?? "Generation failed"
2570
+ },
2571
+ row.id
2572
+ );
2573
+ }
2574
+ return /* @__PURE__ */ jsx11(
2575
+ "div",
2576
+ {
2577
+ className: "studio-rise",
2578
+ style: { animationDelay: `${(settledOrder.get(row.id) ?? 0) * 60}ms` },
2579
+ children: /* @__PURE__ */ jsx11(
2580
+ MediaTile,
2581
+ {
2582
+ generation: row,
2583
+ context: "generation",
2584
+ aspectRatio: ratio,
2585
+ waveformBars: WIDE_WAVEFORM_BARS,
2586
+ actions,
2587
+ onOpen: (generation) => setViewerId(generation.id),
2588
+ onRequestDelete: remove ? setDeleteTarget : void 0,
2589
+ onSaved
2590
+ }
2591
+ )
2592
+ },
2593
+ row.id
2594
+ );
2595
+ })
2596
+ }
2597
+ ),
2598
+ runningCount > 0 && /* @__PURE__ */ jsxs10("p", { className: "sr-only", "aria-live": "polite", children: [
2599
+ "Generating ",
2600
+ runningCount,
2601
+ " result",
2602
+ runningCount > 1 ? "s" : "",
2603
+ "\u2026"
2604
+ ] })
2605
+ ] })
2606
+ }
2607
+ ),
2608
+ /* @__PURE__ */ jsx11(
2609
+ "div",
2610
+ {
2611
+ ref: dockRef,
2612
+ className: "studio-dock sticky bottom-0 z-[5] bg-background px-6 pb-[18px] pt-4 max-[900px]:px-4",
2613
+ children: /* @__PURE__ */ jsxs10("div", { className: "mx-auto w-full max-w-[820px]", children: [
2614
+ /* @__PURE__ */ jsx11("div", { className: "mb-2.5 flex justify-center", children: /* @__PURE__ */ jsx11(GenerationNoticeChip, {}) }),
2615
+ /* @__PURE__ */ jsx11(
2616
+ StudioComposer,
2617
+ {
2618
+ variant: "docked",
2619
+ workspaceId,
2620
+ pickReferenceImage,
2621
+ onGenerated: handleGenerated
2622
+ }
2623
+ )
2624
+ ] })
2625
+ }
2626
+ ),
2627
+ /* @__PURE__ */ jsx11(
2628
+ MediaViewerModal,
2629
+ {
2630
+ generation: viewerGeneration,
2631
+ onClose: () => setViewerId(null),
2632
+ actions,
2633
+ onRequestDelete: remove ? setDeleteTarget : void 0,
2634
+ onSaved
2635
+ }
2636
+ ),
2637
+ /* @__PURE__ */ jsx11(
2638
+ StudioConfirmDialog,
2639
+ {
2640
+ open: deleteTarget !== null,
2641
+ count: 1,
2642
+ onConfirm: confirmDelete,
2643
+ onCancel: () => setDeleteTarget(null)
1373
2644
  }
1374
2645
  )
1375
2646
  ] });
1376
2647
  }
1377
2648
 
1378
- // src/studio-react/generation-notice.tsx
1379
- import { Info } from "lucide-react";
1380
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1381
- function GenerationNoticeChip({ className }) {
1382
- return /* @__PURE__ */ jsxs10(
1383
- "p",
1384
- {
1385
- className: `inline-block rounded-full border px-3.5 py-1.5 text-center text-[12.5px] font-medium leading-relaxed text-foreground shadow-sm ${className ?? ""}`,
1386
- style: { background: "var(--studio-notice-bg)", borderColor: "var(--studio-notice-border)" },
1387
- children: [
1388
- /* @__PURE__ */ jsx11(Info, { className: "mr-1.5 inline-block h-[14px] w-[14px] align-[-2px] text-primary", strokeWidth: 1.5, "aria-hidden": true }),
1389
- "Each prompt starts a new generation \u2014 this chat does not remember the last one."
1390
- ]
2649
+ // src/studio-react/studio-history-screen.tsx
2650
+ import {
2651
+ useCallback as useCallback11,
2652
+ useEffect as useEffect14,
2653
+ useMemo as useMemo9,
2654
+ useState as useState13
2655
+ } from "react";
2656
+ import { ArrowLeft as ArrowLeft2, Download as Download3, FolderPlus as FolderPlus3, Search, Trash2 as Trash23, X as X4 } from "lucide-react";
2657
+
2658
+ // src/studio-react/use-generation-history.ts
2659
+ import { useCallback as useCallback10, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef11, useState as useState12 } from "react";
2660
+ function seedSignature(page) {
2661
+ if (!page) return "";
2662
+ return `${page.nextCursor ?? ""}|${page.items.map((item) => item.id).join(",")}`;
2663
+ }
2664
+ function useGenerationHistory({
2665
+ fetchPage,
2666
+ q,
2667
+ type,
2668
+ initialPage
2669
+ }) {
2670
+ const startsFromSeed = q === "" && type === "all" && initialPage !== void 0;
2671
+ const [items, setItems] = useState12(startsFromSeed ? initialPage.items : []);
2672
+ const [nextCursor, setNextCursor] = useState12(
2673
+ startsFromSeed ? initialPage.nextCursor ?? null : null
2674
+ );
2675
+ const [phase, setPhase] = useState12(
2676
+ startsFromSeed ? "idle" : "loadingFirst"
2677
+ );
2678
+ const [reloadKey, setReloadKey] = useState12(0);
2679
+ const seqRef = useRef11(0);
2680
+ const resetAbortRef = useRef11(null);
2681
+ const loadMoreAbortRef = useRef11(null);
2682
+ const loadingFirstRef = useRef11(!startsFromSeed);
2683
+ const loadingMoreRef = useRef11(false);
2684
+ const lastOpRef = useRef11("first");
2685
+ const seedEligibleRef = useRef11(true);
2686
+ const nextCursorRef = useRef11(nextCursor);
2687
+ nextCursorRef.current = nextCursor;
2688
+ const viewRef = useRef11({ q, type, fetchPage });
2689
+ viewRef.current = { q, type, fetchPage };
2690
+ const seedRef = useRef11(initialPage);
2691
+ seedRef.current = initialPage;
2692
+ const isDefaultView = q === "" && type === "all";
2693
+ const seedKey = useMemo8(() => seedSignature(initialPage), [initialPage]);
2694
+ useEffect13(() => {
2695
+ resetAbortRef.current?.abort();
2696
+ loadMoreAbortRef.current?.abort();
2697
+ loadingFirstRef.current = false;
2698
+ loadingMoreRef.current = false;
2699
+ const seq = ++seqRef.current;
2700
+ if (!isDefaultView) seedEligibleRef.current = false;
2701
+ const seed = seedRef.current;
2702
+ if (isDefaultView && seedEligibleRef.current && reloadKey === 0 && seed) {
2703
+ setItems(seed.items);
2704
+ setNextCursor(seed.nextCursor ?? null);
2705
+ setPhase("idle");
2706
+ return;
1391
2707
  }
2708
+ const controller = new AbortController();
2709
+ resetAbortRef.current = controller;
2710
+ loadingFirstRef.current = true;
2711
+ lastOpRef.current = "first";
2712
+ setItems([]);
2713
+ setNextCursor(null);
2714
+ setPhase("loadingFirst");
2715
+ void (async () => {
2716
+ try {
2717
+ const current = viewRef.current;
2718
+ const page = await current.fetchPage({
2719
+ q: current.q.trim(),
2720
+ type: current.type,
2721
+ cursor: null,
2722
+ signal: controller.signal
2723
+ });
2724
+ if (seq !== seqRef.current) return;
2725
+ setItems(page.items);
2726
+ setNextCursor(page.nextCursor ?? null);
2727
+ setPhase("idle");
2728
+ } catch {
2729
+ if (controller.signal.aborted || seq !== seqRef.current) return;
2730
+ setPhase("error");
2731
+ } finally {
2732
+ if (seq === seqRef.current) loadingFirstRef.current = false;
2733
+ }
2734
+ })();
2735
+ return () => controller.abort();
2736
+ }, [q, type, seedKey, isDefaultView, reloadKey]);
2737
+ const loadMore = useCallback10(() => {
2738
+ const cursor = nextCursorRef.current;
2739
+ if (!cursor || loadingFirstRef.current || loadingMoreRef.current) return;
2740
+ const current = viewRef.current;
2741
+ const seq = seqRef.current;
2742
+ loadMoreAbortRef.current?.abort();
2743
+ const controller = new AbortController();
2744
+ loadMoreAbortRef.current = controller;
2745
+ loadingMoreRef.current = true;
2746
+ lastOpRef.current = "more";
2747
+ setPhase("loadingMore");
2748
+ void (async () => {
2749
+ try {
2750
+ const page = await current.fetchPage({
2751
+ q: current.q.trim(),
2752
+ type: current.type,
2753
+ cursor,
2754
+ signal: controller.signal
2755
+ });
2756
+ if (seq !== seqRef.current) return;
2757
+ setItems((previous) => mergeGenerationPages(previous, page.items));
2758
+ setNextCursor(page.nextCursor ?? null);
2759
+ setPhase("idle");
2760
+ } catch {
2761
+ if (controller.signal.aborted || seq !== seqRef.current) return;
2762
+ setPhase("error");
2763
+ } finally {
2764
+ if (seq === seqRef.current) loadingMoreRef.current = false;
2765
+ }
2766
+ })();
2767
+ }, []);
2768
+ const retry = useCallback10(() => {
2769
+ if (lastOpRef.current === "more") loadMore();
2770
+ else setReloadKey((key) => key + 1);
2771
+ }, [loadMore]);
2772
+ const reload = useCallback10(() => {
2773
+ setReloadKey((key) => key + 1);
2774
+ }, []);
2775
+ useEffect13(
2776
+ () => () => {
2777
+ resetAbortRef.current?.abort();
2778
+ loadMoreAbortRef.current?.abort();
2779
+ },
2780
+ []
1392
2781
  );
2782
+ return {
2783
+ items,
2784
+ hasMore: nextCursor !== null,
2785
+ isLoadingFirst: phase === "loadingFirst",
2786
+ isLoadingMore: phase === "loadingMore",
2787
+ isError: phase === "error",
2788
+ loadMore,
2789
+ retry,
2790
+ reload
2791
+ };
1393
2792
  }
1394
2793
 
1395
- // src/studio-react/library-panel.tsx
1396
- import { useMemo as useMemo4 } from "react";
1397
- import {
1398
- Tabs as Tabs2,
1399
- TabsList as TabsList2,
1400
- TabsTrigger as TabsTrigger2
1401
- } from "@tangle-network/sandbox-ui/primitives";
1402
- import { DollarSign as DollarSign2 } from "lucide-react";
2794
+ // src/studio-react/studio-history-screen.tsx
1403
2795
  import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1404
- function LibraryPanel({
1405
- generations,
1406
- totalCost,
1407
- typeFilter,
1408
- onFilterChange,
1409
- onSelect
2796
+ var FIRST_LOAD_SKELETONS = 8;
2797
+ var MORE_SKELETONS = 4;
2798
+ var BAR = "flex min-h-[44px] flex-wrap items-center justify-between gap-3 px-6 pb-[18px] pt-0.5 max-[900px]:px-4";
2799
+ var SEARCH_INPUT = "h-8 w-[260px] max-w-[52vw] rounded-full border border-border bg-card pl-8 pr-3 text-[13px] text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-[3px] focus:ring-ring/30 max-[640px]:w-full max-[640px]:max-w-none";
2800
+ var OUTLINE_PILL = "inline-flex h-8 items-center gap-1.5 rounded-full border border-border px-3 text-[12.5px] font-medium hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40";
2801
+ var TYPE_NOUNS = {
2802
+ image: "images",
2803
+ video: "videos",
2804
+ speech: "audio"
2805
+ };
2806
+ var SQUARE_CELL = { "--r": 1 };
2807
+ function skeletonCells(count, keyPrefix) {
2808
+ return Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx12("div", { className: "studio-skeleton", style: SQUARE_CELL, "aria-hidden": true }, `${keyPrefix}-${index}`));
2809
+ }
2810
+ function hasOpenPopover2() {
2811
+ return typeof document !== "undefined" && document.querySelector(`[${POPOVER_SURFACE_ATTR}]`) !== null;
2812
+ }
2813
+ function StudioHistoryScreen({
2814
+ fetchPage,
2815
+ initialPage,
2816
+ onBack,
2817
+ actions,
2818
+ searchDebounceMs = 250,
2819
+ className
1410
2820
  }) {
1411
- const visible = useMemo4(() => filterGenerations(generations, typeFilter), [generations, typeFilter]);
1412
- return /* @__PURE__ */ jsxs11("div", { className: "flex min-w-0 flex-col", children: [
1413
- /* @__PURE__ */ jsxs11("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3", children: [
1414
- /* @__PURE__ */ jsx12(Tabs2, { value: typeFilter ?? "all", onValueChange: (v) => onFilterChange(v === "all" ? null : v), children: /* @__PURE__ */ jsxs11(TabsList2, { className: "h-9 justify-start gap-1 overflow-x-auto overflow-y-hidden rounded-lg border border-border bg-background p-1", children: [
2821
+ const playback = useStudioPlayback();
2822
+ const { stop: stop2 } = playback;
2823
+ const { toast } = useStudioToast();
2824
+ const [query, setQuery] = useState13("");
2825
+ const [debouncedQuery, setDebouncedQuery] = useState13("");
2826
+ const [type, setType] = useState13("all");
2827
+ const [selectMode, setSelectMode] = useState13(false);
2828
+ const [selected, setSelected] = useState13(() => /* @__PURE__ */ new Set());
2829
+ const [viewer, setViewer] = useState13(null);
2830
+ const [confirmTargets, setConfirmTargets] = useState13(null);
2831
+ const [saveOpen, setSaveOpen] = useState13(false);
2832
+ const [savePending, setSavePending] = useState13(false);
2833
+ const savePopover = usePopover(saveOpen, setSaveOpen);
2834
+ useEffect14(() => {
2835
+ if (query === debouncedQuery) return;
2836
+ const timer = window.setTimeout(() => setDebouncedQuery(query), searchDebounceMs);
2837
+ return () => window.clearTimeout(timer);
2838
+ }, [debouncedQuery, query, searchDebounceMs]);
2839
+ const history = useGenerationHistory({ fetchPage, q: debouncedQuery, type, initialPage });
2840
+ const deferredDelete = useDeferredDelete({
2841
+ remove: actions?.remove ?? (async () => {
2842
+ })
2843
+ // Nothing to reload: `pendingIds` already hides the rows and the server has
2844
+ // dropped them. A refetch here would only re-request the page we can see.
2845
+ });
2846
+ const { pendingIds } = deferredDelete;
2847
+ const rows = useMemo9(
2848
+ () => history.items.filter((item) => !pendingIds.has(item.id)),
2849
+ [history.items, pendingIds]
2850
+ );
2851
+ const selectedRows = useMemo9(() => rows.filter((row) => selected.has(row.id)), [rows, selected]);
2852
+ useEffect14(() => {
2853
+ if (history.isLoadingFirst || history.isLoadingMore || !playback.activeId) return;
2854
+ if (rows.some((row) => row.id === playback.activeId) || viewer?.id === playback.activeId) return;
2855
+ playback.stop();
2856
+ }, [history.isLoadingFirst, history.isLoadingMore, playback, rows, viewer]);
2857
+ const sentinelRef = useInfiniteScroll(history.loadMore, {
2858
+ enabled: history.hasMore && !history.isLoadingMore && !history.isLoadingFirst && !history.isError
2859
+ });
2860
+ const exitSelectMode = useCallback11(() => {
2861
+ setSelectMode(false);
2862
+ setSelected(/* @__PURE__ */ new Set());
2863
+ setSaveOpen(false);
2864
+ }, []);
2865
+ const toggleSelect = useCallback11((id) => {
2866
+ if (!selectMode) {
2867
+ stop2();
2868
+ setSelectMode(true);
2869
+ setSelected(/* @__PURE__ */ new Set([id]));
2870
+ return;
2871
+ }
2872
+ setSelected((current) => {
2873
+ const next = new Set(current);
2874
+ if (next.has(id)) next.delete(id);
2875
+ else next.add(id);
2876
+ return next;
2877
+ });
2878
+ }, [selectMode, stop2]);
2879
+ useEffect14(() => {
2880
+ if (!selectMode) return;
2881
+ function onKeyDown(event) {
2882
+ if (event.key !== "Escape") return;
2883
+ if (confirmTargets || viewer || hasOpenPopover2()) return;
2884
+ event.preventDefault();
2885
+ exitSelectMode();
2886
+ }
2887
+ document.addEventListener("keydown", onKeyDown);
2888
+ return () => document.removeEventListener("keydown", onKeyDown);
2889
+ }, [confirmTargets, exitSelectMode, selectMode, viewer]);
2890
+ useEffect14(() => () => stop2(), [stop2]);
2891
+ const clearFilters = useCallback11(() => {
2892
+ setQuery("");
2893
+ setDebouncedQuery("");
2894
+ setType("all");
2895
+ }, []);
2896
+ const onSaved = useCallback11((results) => {
2897
+ const path = results[0]?.vaultPath;
2898
+ if (!path) return;
2899
+ toast({
2900
+ message: results.length > 1 ? `Saved ${results.length} items to vault \xB7 ${path}` : `Saved to vault \xB7 ${path}`
2901
+ });
2902
+ }, [toast]);
2903
+ function batchDownload() {
2904
+ void (actions?.download ?? downloadGenerationsViaAnchor)(selectedRows);
2905
+ exitSelectMode();
2906
+ }
2907
+ async function batchSave(path) {
2908
+ if (!actions?.save) return;
2909
+ setSavePending(true);
2910
+ try {
2911
+ const results = await actions.save({ generations: selectedRows, path });
2912
+ setSaveOpen(false);
2913
+ onSaved(results);
2914
+ exitSelectMode();
2915
+ } finally {
2916
+ setSavePending(false);
2917
+ }
2918
+ }
2919
+ function confirmDelete() {
2920
+ const targets = confirmTargets ?? [];
2921
+ setConfirmTargets(null);
2922
+ if (targets.length === 0) return;
2923
+ if (viewer && targets.some((target) => target.id === viewer.id)) setViewer(null);
2924
+ deferredDelete.request(targets);
2925
+ exitSelectMode();
2926
+ }
2927
+ const viewerRow = viewer ? rows.find((row) => row.id === viewer.id) ?? viewer : null;
2928
+ const searchTerm = debouncedQuery.trim();
2929
+ const isFiltered = query !== "" || debouncedQuery !== "" || type !== "all";
2930
+ const emptyCopy = searchTerm ? {
2931
+ title: `No media matches \u201C${searchTerm}\u201D.`,
2932
+ body: "Try a shorter word, or drop the type filter."
2933
+ } : type !== "all" ? {
2934
+ title: `No ${TYPE_NOUNS[type]} yet.`,
2935
+ body: "Generate one from the Studio composer and it will show up here."
2936
+ } : {
2937
+ title: "Your history is empty.",
2938
+ body: "Everything you generate is kept here until you delete it."
2939
+ };
2940
+ return /* @__PURE__ */ jsxs11("div", { className: `studio-hist-wrap pb-[72px] ${className ?? ""}`, children: [
2941
+ selectMode ? /* @__PURE__ */ jsxs11("div", { className: BAR, children: [
2942
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-[13.5px] font-medium", children: [
1415
2943
  /* @__PURE__ */ jsx12(
1416
- TabsTrigger2,
2944
+ "button",
1417
2945
  {
1418
- value: "all",
1419
- className: "rounded-md px-3 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[state=active]:bg-primary/10 data-[state=active]:font-semibold data-[state=active]:text-primary data-[state=active]:shadow-none",
1420
- children: "All"
2946
+ type: "button",
2947
+ "aria-label": "Exit select mode",
2948
+ onClick: exitSelectMode,
2949
+ className: "grid h-[26px] w-[26px] place-items-center rounded-full hover:bg-accent",
2950
+ children: /* @__PURE__ */ jsx12(X4, { size: 15, strokeWidth: 1.5 })
1421
2951
  }
1422
2952
  ),
1423
- Object.entries(TYPE_CONFIG).map(([key, cfg]) => /* @__PURE__ */ jsx12(
1424
- TabsTrigger2,
2953
+ selected.size,
2954
+ " selected"
2955
+ ] }),
2956
+ /* @__PURE__ */ jsxs11("div", { role: "toolbar", "aria-label": "Selection actions", className: "ml-auto flex flex-wrap items-center gap-2", children: [
2957
+ /* @__PURE__ */ jsxs11(
2958
+ "button",
1425
2959
  {
1426
- value: key,
1427
- className: "rounded-md px-3 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[state=active]:bg-primary/10 data-[state=active]:font-semibold data-[state=active]:text-primary data-[state=active]:shadow-none",
1428
- children: cfg.label
1429
- },
1430
- key
1431
- ))
1432
- ] }) }),
1433
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
1434
- /* @__PURE__ */ jsxs11("span", { children: [
1435
- visible.length,
1436
- " generation",
1437
- visible.length !== 1 ? "s" : ""
2960
+ type: "button",
2961
+ disabled: selected.size === 0,
2962
+ onClick: batchDownload,
2963
+ className: OUTLINE_PILL,
2964
+ children: [
2965
+ /* @__PURE__ */ jsx12(Download3, { size: 16, strokeWidth: 1.5 }),
2966
+ " Download"
2967
+ ]
2968
+ }
2969
+ ),
2970
+ actions?.save && /* @__PURE__ */ jsxs11("div", { ref: savePopover.containerRef, className: "inline-flex", children: [
2971
+ /* @__PURE__ */ jsxs11(
2972
+ "button",
2973
+ {
2974
+ ...savePopover.triggerProps,
2975
+ type: "button",
2976
+ disabled: selected.size === 0,
2977
+ onClick: () => setSaveOpen(!saveOpen),
2978
+ className: OUTLINE_PILL,
2979
+ children: [
2980
+ /* @__PURE__ */ jsx12(FolderPlus3, { size: 16, strokeWidth: 1.5 }),
2981
+ " Save to vault"
2982
+ ]
2983
+ }
2984
+ ),
2985
+ /* @__PURE__ */ jsx12(
2986
+ VaultPathPopover,
2987
+ {
2988
+ open: saveOpen,
2989
+ triggerRef: savePopover.triggerRef,
2990
+ panelRef: savePopover.panelRef,
2991
+ generations: selectedRows,
2992
+ onSubmit: batchSave,
2993
+ onCancel: () => setSaveOpen(false),
2994
+ pending: savePending
2995
+ }
2996
+ )
1438
2997
  ] }),
1439
- /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-0.5", children: [
1440
- /* @__PURE__ */ jsx12(DollarSign2, { className: "h-3 w-3" }),
1441
- totalCost.toFixed(2),
1442
- " spent"
1443
- ] })
2998
+ actions?.remove && /* @__PURE__ */ jsxs11(
2999
+ "button",
3000
+ {
3001
+ type: "button",
3002
+ disabled: selected.size === 0,
3003
+ onClick: () => setConfirmTargets(selectedRows),
3004
+ className: `${OUTLINE_PILL} text-destructive hover:border-destructive hover:bg-destructive/10`,
3005
+ children: [
3006
+ /* @__PURE__ */ jsx12(Trash23, { size: 16, strokeWidth: 1.5 }),
3007
+ " Delete"
3008
+ ]
3009
+ }
3010
+ )
3011
+ ] })
3012
+ ] }) : /* @__PURE__ */ jsxs11("div", { className: BAR, children: [
3013
+ /* @__PURE__ */ jsx12(
3014
+ "button",
3015
+ {
3016
+ type: "button",
3017
+ "aria-label": "Back to Studio",
3018
+ onClick: onBack,
3019
+ className: "grid h-8 w-8 flex-none place-items-center rounded-full border border-border bg-card shadow-sm hover:bg-accent",
3020
+ children: /* @__PURE__ */ jsx12(ArrowLeft2, { size: 16, strokeWidth: 1.5 })
3021
+ }
3022
+ ),
3023
+ /* @__PURE__ */ jsxs11("div", { className: "ml-auto flex flex-wrap items-center gap-2 max-[640px]:w-full", children: [
3024
+ /* @__PURE__ */ jsxs11("div", { className: "relative inline-flex items-center max-[640px]:flex-1 max-[640px]:basis-[160px]", children: [
3025
+ /* @__PURE__ */ jsx12(
3026
+ Search,
3027
+ {
3028
+ size: 15,
3029
+ strokeWidth: 1.5,
3030
+ "aria-hidden": true,
3031
+ className: "pointer-events-none absolute left-[11px] text-muted-foreground"
3032
+ }
3033
+ ),
3034
+ /* @__PURE__ */ jsx12(
3035
+ "input",
3036
+ {
3037
+ type: "text",
3038
+ value: query,
3039
+ placeholder: "Search",
3040
+ "aria-label": "Search prompts",
3041
+ onChange: (event) => setQuery(event.target.value),
3042
+ className: SEARCH_INPUT
3043
+ }
3044
+ )
3045
+ ] }),
3046
+ /* @__PURE__ */ jsx12(
3047
+ MenuPill,
3048
+ {
3049
+ label: "Filter by media type",
3050
+ value: type,
3051
+ choices: MEDIA_TYPE_FILTERS,
3052
+ onSelect: setType
3053
+ }
3054
+ )
1444
3055
  ] })
1445
3056
  ] }),
1446
- /* @__PURE__ */ jsx12(GenerationGrid, { generations: visible, typeFilter, onSelect })
3057
+ history.isError && rows.length === 0 ? /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-center gap-3 px-6 py-16 text-center max-[900px]:px-4", children: [
3058
+ /* @__PURE__ */ jsx12("p", { className: "text-[14px] text-foreground", children: "Could not load media." }),
3059
+ /* @__PURE__ */ jsx12("button", { type: "button", onClick: history.retry, className: OUTLINE_PILL, children: "Retry" })
3060
+ ] }) : history.isLoadingFirst && rows.length === 0 ? /* @__PURE__ */ jsx12("div", { className: "studio-grid studio-grid-library", children: skeletonCells(FIRST_LOAD_SKELETONS, "first") }) : rows.length === 0 ? /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-center gap-1.5 px-6 py-16 text-center max-[900px]:px-4", children: [
3061
+ /* @__PURE__ */ jsx12("p", { className: "text-[14px] text-foreground", children: emptyCopy.title }),
3062
+ /* @__PURE__ */ jsx12("p", { className: "max-w-[380px] text-[13px] text-muted-foreground", children: emptyCopy.body }),
3063
+ isFiltered && /* @__PURE__ */ jsx12("button", { type: "button", onClick: clearFilters, className: `${OUTLINE_PILL} mt-2`, children: "Clear" })
3064
+ ] }) : /* @__PURE__ */ jsx12("div", { className: selectMode ? "studio-selectmode" : void 0, children: /* @__PURE__ */ jsxs11("div", { className: "studio-grid studio-grid-library", children: [
3065
+ rows.map((row) => /* @__PURE__ */ jsx12(
3066
+ MediaTile,
3067
+ {
3068
+ generation: row,
3069
+ context: "history",
3070
+ onOpen: setViewer,
3071
+ actions,
3072
+ selectMode,
3073
+ selected: selected.has(row.id),
3074
+ onToggleSelect: toggleSelect,
3075
+ onRequestDelete: actions?.remove ? (generation) => setConfirmTargets([generation]) : void 0,
3076
+ onSaved
3077
+ },
3078
+ row.id
3079
+ )),
3080
+ history.isLoadingMore && skeletonCells(MORE_SKELETONS, "more")
3081
+ ] }) }),
3082
+ history.isError && rows.length > 0 && /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-center gap-3 px-6 py-4 text-[13px] text-muted-foreground max-[900px]:px-4", children: [
3083
+ /* @__PURE__ */ jsx12("span", { children: "Could not load more media." }),
3084
+ /* @__PURE__ */ jsx12("button", { type: "button", onClick: history.retry, className: OUTLINE_PILL, children: "Retry" })
3085
+ ] }),
3086
+ /* @__PURE__ */ jsx12("div", { ref: sentinelRef, "aria-hidden": true }),
3087
+ /* @__PURE__ */ jsx12(
3088
+ MediaViewerModal,
3089
+ {
3090
+ generation: viewerRow,
3091
+ onClose: () => setViewer(null),
3092
+ actions,
3093
+ onRequestDelete: actions?.remove ? (generation) => setConfirmTargets([generation]) : void 0,
3094
+ onSaved
3095
+ }
3096
+ ),
3097
+ /* @__PURE__ */ jsx12(
3098
+ StudioConfirmDialog,
3099
+ {
3100
+ open: confirmTargets !== null,
3101
+ count: confirmTargets?.length ?? 0,
3102
+ onConfirm: confirmDelete,
3103
+ onCancel: () => setConfirmTargets(null)
3104
+ }
3105
+ )
1447
3106
  ] });
1448
3107
  }
1449
-
1450
- // src/studio-react/generation-detail-modal.tsx
1451
- import { Dialog as Dialog2, DialogContent, DialogHeader, DialogTitle } from "@tangle-network/sandbox-ui/primitives";
1452
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1453
- function GenerationDetailModal({
1454
- generation,
1455
- vaultHref,
1456
- onClose
1457
- }) {
1458
- const cfg = generation ? typeConfigFor(generation.type) : null;
1459
- return /* @__PURE__ */ jsx13(Dialog2, { open: generation != null, onOpenChange: (open) => {
1460
- if (!open) onClose();
1461
- }, children: /* @__PURE__ */ jsxs12(DialogContent, { className: "max-h-[85vh] max-w-2xl overflow-y-auto border-card-edge bg-popover shadow-[var(--shadow-overlay)]", children: [
1462
- /* @__PURE__ */ jsx13(DialogHeader, { children: /* @__PURE__ */ jsx13(DialogTitle, { className: "text-left text-[15px] font-semibold", children: cfg ? `${cfg.label} generation` : "Generation" }) }),
1463
- generation && /* @__PURE__ */ jsx13(GenerationDetail, { generation, vaultHref, onNavigate: onClose })
1464
- ] }) });
1465
- }
1466
3108
  export {
1467
- GenerationCard,
1468
- GenerationDetail,
1469
- GenerationDetailModal,
1470
- GenerationGrid,
3109
+ AudioTogglePill,
3110
+ ComposerBand,
3111
+ CustomSizeForm,
1471
3112
  GenerationNoticeChip,
1472
- GenerationStatusBadge,
1473
- LibraryDrawer,
1474
- LibraryPanel,
1475
- ResultCanvas,
3113
+ MediaTile,
3114
+ MediaTypeSegments,
3115
+ MediaViewerModal,
3116
+ MenuPill,
3117
+ ModelPill,
3118
+ OptionPill,
3119
+ ReferencePill,
1476
3120
  StudioComposer,
1477
- StudioHeader,
1478
- StudioSheet,
1479
- StudioWorkspace,
1480
- TYPE_CONFIG,
1481
- filterGenerations,
1482
- typeConfigFor,
1483
- useStudioGenerations
3121
+ StudioConfirmDialog,
3122
+ StudioGenerationScreen,
3123
+ StudioHistoryScreen,
3124
+ StudioHomeScreen,
3125
+ StudioPlaybackProvider,
3126
+ StudioToastProvider,
3127
+ VaultPathPopover,
3128
+ downloadGenerationsViaAnchor,
3129
+ formatClock,
3130
+ optionValueLabel,
3131
+ useBatchNavigation,
3132
+ useDeferredDelete,
3133
+ useGenerationHistory,
3134
+ useStudioGenerations,
3135
+ useStudioPlayback,
3136
+ useStudioToast
1484
3137
  };
1485
3138
  //# sourceMappingURL=index.js.map