@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.
- package/README.md +1 -1
- package/dist/assistant/index.js +2 -1
- package/dist/assistant/index.js.map +1 -1
- package/dist/chunk-KKBTPZIE.js +805 -0
- package/dist/chunk-KKBTPZIE.js.map +1 -0
- package/dist/{chunk-2RTYQU4W.js → chunk-LEGZX5MQ.js} +150 -4
- package/dist/chunk-LEGZX5MQ.js.map +1 -0
- package/dist/{chunk-MBKNKAN2.js → chunk-RZ6MYWMJ.js} +416 -1210
- package/dist/chunk-RZ6MYWMJ.js.map +1 -0
- package/dist/stories/studio/StudioProviders.d.ts +10 -0
- package/dist/stories/studio/fixtures.d.ts +10 -2
- package/dist/studio/audio-preview.d.ts +12 -0
- package/dist/studio/generation.d.ts +22 -1
- package/dist/studio/index.d.ts +2 -0
- package/dist/studio/index.js +29 -1
- package/dist/studio/ports.d.ts +54 -0
- package/dist/studio-react/composer-option-controls.d.ts +19 -8
- package/dist/studio-react/download-generations.d.ts +4 -0
- package/dist/studio-react/index.d.ts +38 -22
- package/dist/studio-react/index.js +2198 -545
- package/dist/studio-react/index.js.map +1 -1
- package/dist/studio-react/media-tile.d.ts +21 -0
- package/dist/studio-react/media-viewer.d.ts +12 -0
- package/dist/studio-react/studio-confirm.d.ts +8 -0
- package/dist/studio-react/studio-generation-screen.d.ts +42 -0
- package/dist/studio-react/studio-history-screen.d.ts +28 -0
- package/dist/studio-react/studio-home-screen.d.ts +20 -0
- package/dist/studio-react/studio-playback.d.ts +31 -0
- package/dist/studio-react/studio-toasts.d.ts +21 -0
- package/dist/studio-react/studio.css +142 -72
- package/dist/studio-react/use-batch-navigation.d.ts +20 -0
- package/dist/studio-react/use-deferred-delete.d.ts +19 -0
- package/dist/studio-react/use-generation-history.d.ts +22 -0
- package/dist/studio-react/vault-path-popover.d.ts +12 -0
- package/dist/web-react/index.js +8 -6
- package/package.json +2 -7
- package/dist/chunk-2RTYQU4W.js.map +0 -1
- package/dist/chunk-MBKNKAN2.js.map +0 -1
- package/dist/studio-react/generation-card.d.ts +0 -9
- package/dist/studio-react/generation-detail-modal.d.ts +0 -11
- package/dist/studio-react/generation-detail.d.ts +0 -6
- package/dist/studio-react/generation-grid.d.ts +0 -15
- package/dist/studio-react/library-drawer.d.ts +0 -12
- package/dist/studio-react/library-panel.d.ts +0 -15
- package/dist/studio-react/result-canvas.d.ts +0 -6
- package/dist/studio-react/studio-header.d.ts +0 -5
- package/dist/studio-react/studio-sheet.d.ts +0 -12
- package/dist/studio-react/studio-workspace.d.ts +0 -21
- 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-
|
|
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
|
-
|
|
262
|
-
|
|
347
|
+
/* @__PURE__ */ jsx(
|
|
348
|
+
MenuRows,
|
|
263
349
|
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
onSelect(choice.value);
|
|
350
|
+
value,
|
|
351
|
+
choices,
|
|
352
|
+
onSelect: (next) => {
|
|
353
|
+
onSelect(next);
|
|
269
354
|
close();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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/
|
|
913
|
-
import {
|
|
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
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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/
|
|
935
|
-
import {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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/
|
|
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
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1033
|
+
function VaultPathPopover({
|
|
1034
|
+
open,
|
|
1035
|
+
triggerRef,
|
|
1036
|
+
panelRef,
|
|
1037
|
+
generations,
|
|
1038
|
+
onSubmit,
|
|
1039
|
+
onCancel,
|
|
1040
|
+
pending = false
|
|
959
1041
|
}) {
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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(
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
|
|
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/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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/
|
|
1284
|
-
import {
|
|
1285
|
-
function
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
|
1294
|
-
const
|
|
1295
|
-
const [
|
|
1296
|
-
const [
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
|
1313
|
-
|
|
1314
|
-
|
|
1453
|
+
function onKeyDown(event) {
|
|
1454
|
+
if (event.target !== event.currentTarget || event.key !== "Enter" && event.key !== " ") return;
|
|
1455
|
+
event.preventDefault();
|
|
1456
|
+
activate();
|
|
1315
1457
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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__ */
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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__ */
|
|
1339
|
-
|
|
2535
|
+
/* @__PURE__ */ jsx11(
|
|
2536
|
+
"p",
|
|
1340
2537
|
{
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
] })
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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/
|
|
1379
|
-
import {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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/
|
|
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
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
2944
|
+
"button",
|
|
1417
2945
|
{
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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(
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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__ */
|
|
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
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
GenerationGrid,
|
|
3109
|
+
AudioTogglePill,
|
|
3110
|
+
ComposerBand,
|
|
3111
|
+
CustomSizeForm,
|
|
1471
3112
|
GenerationNoticeChip,
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
3113
|
+
MediaTile,
|
|
3114
|
+
MediaTypeSegments,
|
|
3115
|
+
MediaViewerModal,
|
|
3116
|
+
MenuPill,
|
|
3117
|
+
ModelPill,
|
|
3118
|
+
OptionPill,
|
|
3119
|
+
ReferencePill,
|
|
1476
3120
|
StudioComposer,
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|