@hyperframes/studio 0.6.0-alpha.1 → 0.6.0-alpha.10
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/dist/assets/hyperframes-player-DjsVzYFP.js +418 -0
- package/dist/assets/index-14zH9lqh.css +1 -0
- package/dist/assets/index-B-16fRnH.js +108 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +398 -70
- package/src/components/editor/PropertyPanel.test.ts +49 -0
- package/src/components/editor/PropertyPanel.tsx +277 -337
- package/src/components/editor/domEditing.test.ts +248 -0
- package/src/components/editor/domEditing.ts +126 -2
- package/src/components/editor/manualEditingAvailability.test.ts +15 -4
- package/src/components/editor/manualEditingAvailability.ts +4 -2
- package/src/components/editor/manualEdits.ts +15 -3
- package/src/components/nle/NLELayout.test.ts +12 -0
- package/src/components/nle/NLELayout.tsx +63 -24
- package/src/components/nle/NLEPreview.tsx +6 -0
- package/src/components/renders/RenderQueue.tsx +66 -4
- package/src/components/renders/useRenderQueue.ts +30 -6
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/player/components/Player.test.ts +58 -0
- package/src/player/components/Player.tsx +71 -4
- package/src/player/components/PlayerControls.tsx +20 -7
- package/src/player/components/Timeline.tsx +45 -20
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/hyperframes-player-Cd8vYWxP.js +0 -198
- package/dist/assets/index-D04_ZoMm.js +0 -107
- package/dist/assets/index-UWFaHilT.css +0 -1
package/src/App.tsx
CHANGED
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
useRef,
|
|
5
5
|
useEffect,
|
|
6
6
|
useMemo,
|
|
7
|
+
type CSSProperties,
|
|
7
8
|
type MouseEvent,
|
|
8
9
|
type ReactNode,
|
|
9
10
|
} from "react";
|
|
10
11
|
import { useMountEffect } from "./hooks/useMountEffect";
|
|
11
12
|
import { NLELayout } from "./components/nle/NLELayout";
|
|
12
13
|
import { SourceEditor } from "./components/editor/SourceEditor";
|
|
13
|
-
import { LeftSidebar } from "./components/sidebar/LeftSidebar";
|
|
14
|
+
import { LeftSidebar, type LeftSidebarHandle } from "./components/sidebar/LeftSidebar";
|
|
14
15
|
import { RenderQueue } from "./components/renders/RenderQueue";
|
|
15
16
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
16
17
|
import { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player";
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
} from "./player/components/timelineZoom";
|
|
56
57
|
import {
|
|
57
58
|
getTimelineToggleTitle,
|
|
59
|
+
isEditableTarget,
|
|
58
60
|
shouldHandleTimelineToggleHotkey,
|
|
59
61
|
} from "./utils/timelineDiscovery";
|
|
60
62
|
import { buildFrameCaptureFilename, buildFrameCaptureUrl } from "./utils/frameCapture";
|
|
@@ -78,10 +80,10 @@ import {
|
|
|
78
80
|
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
79
81
|
STUDIO_MOTION_PANEL_ENABLED,
|
|
80
82
|
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
|
83
|
+
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
81
84
|
STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED,
|
|
82
85
|
} from "./components/editor/manualEditingAvailability";
|
|
83
86
|
import {
|
|
84
|
-
buildDefaultDomEditTextField,
|
|
85
87
|
buildDomEditStylePatchOperation,
|
|
86
88
|
buildDomEditTextPatchOperation,
|
|
87
89
|
buildElementAgentPrompt,
|
|
@@ -91,12 +93,16 @@ import {
|
|
|
91
93
|
findElementForTimelineElement,
|
|
92
94
|
getDomEditLayerKey,
|
|
93
95
|
getDomEditTargetKey,
|
|
96
|
+
isLargeRasterDomEditSelection,
|
|
94
97
|
isTextEditableSelection,
|
|
98
|
+
resolveVisualDomEditSelectionTarget,
|
|
95
99
|
serializeDomEditTextFields,
|
|
96
100
|
resolveDomEditSelection,
|
|
101
|
+
type DomEditViewport,
|
|
97
102
|
type DomEditLayerItem,
|
|
98
103
|
type DomEditTextField,
|
|
99
104
|
type DomEditSelection,
|
|
105
|
+
buildDefaultDomEditTextField,
|
|
100
106
|
} from "./components/editor/domEditing";
|
|
101
107
|
import {
|
|
102
108
|
STUDIO_MANUAL_EDITS_PATH,
|
|
@@ -358,10 +364,64 @@ function isManualGeometryStyleProperty(property: string): boolean {
|
|
|
358
364
|
return property === "left" || property === "top" || property === "width" || property === "height";
|
|
359
365
|
}
|
|
360
366
|
|
|
367
|
+
interface PreviewLocalPointer {
|
|
368
|
+
x: number;
|
|
369
|
+
y: number;
|
|
370
|
+
viewport: DomEditViewport;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface AgentModalAnchorPoint {
|
|
374
|
+
x: number;
|
|
375
|
+
y: number;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function resolvePreviewLocalPointer(
|
|
379
|
+
iframe: HTMLIFrameElement,
|
|
380
|
+
doc: Document,
|
|
381
|
+
win: Window,
|
|
382
|
+
clientX: number,
|
|
383
|
+
clientY: number,
|
|
384
|
+
): PreviewLocalPointer | null {
|
|
385
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
386
|
+
const root =
|
|
387
|
+
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
388
|
+
const rootRect = root?.getBoundingClientRect();
|
|
389
|
+
const rootWidth = rootRect?.width || win.innerWidth;
|
|
390
|
+
const rootHeight = rootRect?.height || win.innerHeight;
|
|
391
|
+
if (!rootWidth || !rootHeight) return null;
|
|
392
|
+
|
|
393
|
+
const scaleX = iframeRect.width / rootWidth;
|
|
394
|
+
const scaleY = iframeRect.height / rootHeight;
|
|
395
|
+
return {
|
|
396
|
+
x: (clientX - iframeRect.left) / scaleX,
|
|
397
|
+
y: (clientY - iframeRect.top) / scaleY,
|
|
398
|
+
viewport: { width: rootWidth, height: rootHeight },
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getPreviewLocalPointer(
|
|
403
|
+
iframe: HTMLIFrameElement,
|
|
404
|
+
clientX: number,
|
|
405
|
+
clientY: number,
|
|
406
|
+
): PreviewLocalPointer | null {
|
|
407
|
+
let doc: Document | null = null;
|
|
408
|
+
let win: Window | null = null;
|
|
409
|
+
try {
|
|
410
|
+
doc = iframe.contentDocument;
|
|
411
|
+
win = iframe.contentWindow;
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
if (!doc || !win) return null;
|
|
416
|
+
|
|
417
|
+
return resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
418
|
+
}
|
|
419
|
+
|
|
361
420
|
function getPreviewTargetFromPointer(
|
|
362
421
|
iframe: HTMLIFrameElement,
|
|
363
422
|
clientX: number,
|
|
364
423
|
clientY: number,
|
|
424
|
+
activeCompositionPath: string | null,
|
|
365
425
|
): HTMLElement | null {
|
|
366
426
|
let doc: Document | null = null;
|
|
367
427
|
let win: Window | null = null;
|
|
@@ -373,20 +433,35 @@ function getPreviewTargetFromPointer(
|
|
|
373
433
|
}
|
|
374
434
|
if (!doc || !win) return null;
|
|
375
435
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
379
|
-
const rootRect = root?.getBoundingClientRect();
|
|
380
|
-
const rootWidth = rootRect?.width || win.innerWidth;
|
|
381
|
-
const rootHeight = rootRect?.height || win.innerHeight;
|
|
382
|
-
if (!rootWidth || !rootHeight) return null;
|
|
436
|
+
const localPointer = resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
437
|
+
if (!localPointer) return null;
|
|
383
438
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
439
|
+
if (typeof doc.elementsFromPoint === "function") {
|
|
440
|
+
const visualTarget = resolveVisualDomEditSelectionTarget(
|
|
441
|
+
doc.elementsFromPoint(localPointer.x, localPointer.y),
|
|
442
|
+
{
|
|
443
|
+
activeCompositionPath,
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
if (visualTarget) return visualTarget;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
450
|
+
}
|
|
388
451
|
|
|
389
|
-
|
|
452
|
+
function buildRasterClickSelectionContext(
|
|
453
|
+
selection: DomEditSelection,
|
|
454
|
+
localPointer: PreviewLocalPointer,
|
|
455
|
+
): string {
|
|
456
|
+
return [
|
|
457
|
+
"The user clicked a large raster/background element in the Studio preview.",
|
|
458
|
+
`Preview click: x=${Math.round(localPointer.x)}px, y=${Math.round(localPointer.y)}px in a ${Math.round(
|
|
459
|
+
localPointer.viewport.width,
|
|
460
|
+
)}x${Math.round(localPointer.viewport.height)} composition.`,
|
|
461
|
+
`Selected target: <${selection.tagName}> ${selection.selector ?? selection.id ?? selection.label}.`,
|
|
462
|
+
"Visible copy or artwork at that point may be baked into the selected image/background rather than a selectable DOM text layer.",
|
|
463
|
+
"If the request mentions text seen at the click location, inspect or replace the image asset, or recreate that visible copy as editable DOM.",
|
|
464
|
+
].join("\n");
|
|
390
465
|
}
|
|
391
466
|
|
|
392
467
|
function domEditSelectionsTargetSame(
|
|
@@ -592,17 +667,47 @@ function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number |
|
|
|
592
667
|
|
|
593
668
|
// ── Ask Agent Modal ──
|
|
594
669
|
|
|
670
|
+
function clampNumber(value: number, min: number, max: number): number {
|
|
671
|
+
if (max < min) return min;
|
|
672
|
+
return Math.min(Math.max(value, min), max);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function getAgentModalPositionStyle(
|
|
676
|
+
anchorPoint: AgentModalAnchorPoint | null,
|
|
677
|
+
): CSSProperties | undefined {
|
|
678
|
+
if (!anchorPoint || typeof window === "undefined") return undefined;
|
|
679
|
+
|
|
680
|
+
const modalWidth = 480;
|
|
681
|
+
const estimatedModalHeight = 270;
|
|
682
|
+
const margin = 16;
|
|
683
|
+
const left = clampNumber(
|
|
684
|
+
anchorPoint.x,
|
|
685
|
+
margin + modalWidth / 2,
|
|
686
|
+
window.innerWidth - margin - modalWidth / 2,
|
|
687
|
+
);
|
|
688
|
+
const top = clampNumber(
|
|
689
|
+
anchorPoint.y + 12,
|
|
690
|
+
margin,
|
|
691
|
+
window.innerHeight - margin - estimatedModalHeight,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
return { left, top, transform: "translateX(-50%)" };
|
|
695
|
+
}
|
|
696
|
+
|
|
595
697
|
function AskAgentModal({
|
|
596
698
|
selectionLabel,
|
|
699
|
+
anchorPoint = null,
|
|
597
700
|
onSubmit,
|
|
598
701
|
onClose,
|
|
599
702
|
}: {
|
|
600
703
|
selectionLabel: string;
|
|
704
|
+
anchorPoint?: AgentModalAnchorPoint | null;
|
|
601
705
|
onSubmit: (instruction: string) => void;
|
|
602
706
|
onClose: () => void;
|
|
603
707
|
}) {
|
|
604
708
|
const [value, setValue] = useState("");
|
|
605
709
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
710
|
+
const modalPositionStyle = getAgentModalPositionStyle(anchorPoint);
|
|
606
711
|
|
|
607
712
|
useMountEffect(() => {
|
|
608
713
|
requestAnimationFrame(() => inputRef.current?.focus());
|
|
@@ -615,11 +720,18 @@ function AskAgentModal({
|
|
|
615
720
|
|
|
616
721
|
return (
|
|
617
722
|
<div
|
|
618
|
-
className=
|
|
723
|
+
className={
|
|
724
|
+
anchorPoint
|
|
725
|
+
? "fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm"
|
|
726
|
+
: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
727
|
+
}
|
|
619
728
|
onClick={onClose}
|
|
620
729
|
>
|
|
621
730
|
<div
|
|
622
|
-
className=
|
|
731
|
+
className={`w-[480px] rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl ${
|
|
732
|
+
anchorPoint ? "fixed" : ""
|
|
733
|
+
}`}
|
|
734
|
+
style={modalPositionStyle}
|
|
623
735
|
onClick={(e) => e.stopPropagation()}
|
|
624
736
|
>
|
|
625
737
|
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
@@ -774,13 +886,17 @@ export function StudioApp() {
|
|
|
774
886
|
const [domEditGroupSelections, setDomEditGroupSelections] = useState<DomEditSelection[]>([]);
|
|
775
887
|
const [domEditHoverSelection, setDomEditHoverSelection] = useState<DomEditSelection | null>(null);
|
|
776
888
|
const [agentPromptTagSnippet, setAgentPromptTagSnippet] = useState<string | undefined>();
|
|
889
|
+
const [agentPromptSelectionContext, setAgentPromptSelectionContext] = useState<
|
|
890
|
+
string | undefined
|
|
891
|
+
>();
|
|
892
|
+
const [agentModalAnchorPoint, setAgentModalAnchorPoint] = useState<AgentModalAnchorPoint | null>(
|
|
893
|
+
null,
|
|
894
|
+
);
|
|
777
895
|
const [copiedAgentPrompt, setCopiedAgentPrompt] = useState(false);
|
|
778
896
|
const [agentModalOpen, setAgentModalOpen] = useState(false);
|
|
779
897
|
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
|
780
898
|
const [inspectedTimelineElementId, setInspectedTimelineElementId] = useState<string | null>(null);
|
|
781
|
-
const [
|
|
782
|
-
ReadonlySet<string>
|
|
783
|
-
>(() => new Set());
|
|
899
|
+
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
784
900
|
const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
|
|
785
901
|
const refreshPreviewDocumentVersion = useCallback(() => {
|
|
786
902
|
setPreviewDocumentVersion((version) => version + 1);
|
|
@@ -916,6 +1032,8 @@ export function StudioApp() {
|
|
|
916
1032
|
const lastBlockedDomMoveToastAtRef = useRef(0);
|
|
917
1033
|
const importedFontAssetsRef = useRef<ImportedFontAsset[]>([]);
|
|
918
1034
|
const previewHotkeyWindowRef = useRef<Window | null>(null);
|
|
1035
|
+
const handleAppKeyDownRef = useRef<((event: KeyboardEvent) => void) | undefined>(undefined);
|
|
1036
|
+
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
919
1037
|
const previewHistoryHotkeyCleanupRef = useRef<(() => void) | null>(null);
|
|
920
1038
|
const panelDragRef = useRef<{
|
|
921
1039
|
side: "left" | "right";
|
|
@@ -983,34 +1101,31 @@ export function StudioApp() {
|
|
|
983
1101
|
[toggleTimelineVisibility],
|
|
984
1102
|
);
|
|
985
1103
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
window.removeEventListener("keydown", handleTimelineToggleHotkey);
|
|
990
|
-
};
|
|
991
|
-
});
|
|
1104
|
+
const previewAppKeyDownHandler = useCallback((event: KeyboardEvent) => {
|
|
1105
|
+
handleAppKeyDownRef.current?.(event);
|
|
1106
|
+
}, []);
|
|
992
1107
|
|
|
993
1108
|
const syncPreviewTimelineHotkey = useCallback(
|
|
994
1109
|
(iframe: HTMLIFrameElement | null) => {
|
|
995
1110
|
const nextWindow = iframe?.contentWindow ?? null;
|
|
996
1111
|
if (previewHotkeyWindowRef.current === nextWindow) return;
|
|
997
1112
|
if (previewHotkeyWindowRef.current) {
|
|
998
|
-
previewHotkeyWindowRef.current.removeEventListener("keydown",
|
|
1113
|
+
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
|
|
999
1114
|
}
|
|
1000
1115
|
previewHotkeyWindowRef.current = nextWindow;
|
|
1001
|
-
nextWindow?.addEventListener("keydown",
|
|
1116
|
+
nextWindow?.addEventListener("keydown", previewAppKeyDownHandler, true);
|
|
1002
1117
|
},
|
|
1003
|
-
[
|
|
1118
|
+
[previewAppKeyDownHandler],
|
|
1004
1119
|
);
|
|
1005
1120
|
|
|
1006
1121
|
useEffect(
|
|
1007
1122
|
() => () => {
|
|
1008
1123
|
if (previewHotkeyWindowRef.current) {
|
|
1009
|
-
previewHotkeyWindowRef.current.removeEventListener("keydown",
|
|
1124
|
+
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
|
|
1010
1125
|
previewHotkeyWindowRef.current = null;
|
|
1011
1126
|
}
|
|
1012
1127
|
},
|
|
1013
|
-
[
|
|
1128
|
+
[previewAppKeyDownHandler],
|
|
1014
1129
|
);
|
|
1015
1130
|
|
|
1016
1131
|
const renderClipContent = useCallback(
|
|
@@ -1393,6 +1508,10 @@ export function StudioApp() {
|
|
|
1393
1508
|
// Debounce the server write (600ms)
|
|
1394
1509
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
1395
1510
|
saveTimerRef.current = setTimeout(() => {
|
|
1511
|
+
// Suppress the file-change watcher echo — the save callback triggers
|
|
1512
|
+
// its own refresh, so a second one from the watcher causes a double-reload
|
|
1513
|
+
// race that can leave the player in a non-playable state.
|
|
1514
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1396
1515
|
saveProjectFilesWithHistory({
|
|
1397
1516
|
projectId: pid,
|
|
1398
1517
|
label: "Edit source",
|
|
@@ -1491,6 +1610,7 @@ export function StudioApp() {
|
|
|
1491
1610
|
throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
|
|
1492
1611
|
}
|
|
1493
1612
|
|
|
1613
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1494
1614
|
await saveProjectFilesWithHistory({
|
|
1495
1615
|
projectId: pid,
|
|
1496
1616
|
label: "Move timeline clip",
|
|
@@ -1575,6 +1695,7 @@ export function StudioApp() {
|
|
|
1575
1695
|
throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
|
|
1576
1696
|
}
|
|
1577
1697
|
|
|
1698
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1578
1699
|
await saveProjectFilesWithHistory({
|
|
1579
1700
|
projectId: pid,
|
|
1580
1701
|
label: "Resize timeline clip",
|
|
@@ -1713,6 +1834,7 @@ export function StudioApp() {
|
|
|
1713
1834
|
});
|
|
1714
1835
|
}
|
|
1715
1836
|
|
|
1837
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1716
1838
|
await saveProjectFilesWithHistory({
|
|
1717
1839
|
projectId: pid,
|
|
1718
1840
|
label: "Delete timeline clip",
|
|
@@ -1741,6 +1863,155 @@ export function StudioApp() {
|
|
|
1741
1863
|
[activeCompPath, editHistory.recordEdit, showToast, timelineElements, writeProjectFile],
|
|
1742
1864
|
);
|
|
1743
1865
|
|
|
1866
|
+
const handleDomEditElementDelete = useCallback(
|
|
1867
|
+
async (selection: DomEditSelection) => {
|
|
1868
|
+
const pid = projectIdRef.current;
|
|
1869
|
+
if (!pid) return;
|
|
1870
|
+
|
|
1871
|
+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
1872
|
+
try {
|
|
1873
|
+
const response = await fetch(
|
|
1874
|
+
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
1875
|
+
);
|
|
1876
|
+
if (!response.ok) throw new Error(`Failed to read ${targetPath}`);
|
|
1877
|
+
|
|
1878
|
+
const data = (await response.json()) as { content?: string };
|
|
1879
|
+
const originalContent = data.content;
|
|
1880
|
+
if (typeof originalContent !== "string")
|
|
1881
|
+
throw new Error(`Missing file contents for ${targetPath}`);
|
|
1882
|
+
|
|
1883
|
+
const patchTarget: { id?: string; selector?: string; selectorIndex?: number } = selection.id
|
|
1884
|
+
? {
|
|
1885
|
+
id: selection.id,
|
|
1886
|
+
selector: selection.selector,
|
|
1887
|
+
selectorIndex: selection.selectorIndex,
|
|
1888
|
+
}
|
|
1889
|
+
: selection.selector
|
|
1890
|
+
? { selector: selection.selector, selectorIndex: selection.selectorIndex }
|
|
1891
|
+
: ({} as never);
|
|
1892
|
+
if (!patchTarget.id && !patchTarget.selector) {
|
|
1893
|
+
throw new Error("Selected element has no patchable target");
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const removeResponse = await fetch(
|
|
1897
|
+
`/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
|
|
1898
|
+
{
|
|
1899
|
+
method: "POST",
|
|
1900
|
+
headers: { "Content-Type": "application/json" },
|
|
1901
|
+
body: JSON.stringify({ target: patchTarget }),
|
|
1902
|
+
},
|
|
1903
|
+
);
|
|
1904
|
+
if (!removeResponse.ok) throw new Error(`Failed to delete element from ${targetPath}`);
|
|
1905
|
+
|
|
1906
|
+
const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
|
|
1907
|
+
const patchedContent =
|
|
1908
|
+
typeof removeData.content === "string" ? removeData.content : originalContent;
|
|
1909
|
+
|
|
1910
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1911
|
+
await saveProjectFilesWithHistory({
|
|
1912
|
+
projectId: pid,
|
|
1913
|
+
label: "Delete element",
|
|
1914
|
+
kind: "timeline",
|
|
1915
|
+
files: { [targetPath]: patchedContent },
|
|
1916
|
+
readFile: async () => originalContent,
|
|
1917
|
+
writeFile: writeProjectFile,
|
|
1918
|
+
recordEdit: editHistory.recordEdit,
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
domEditSelectionRef.current = null;
|
|
1922
|
+
domEditGroupSelectionsRef.current = [];
|
|
1923
|
+
setDomEditSelection(null);
|
|
1924
|
+
setDomEditGroupSelections([]);
|
|
1925
|
+
usePlayerStore.getState().setSelectedElementId(null);
|
|
1926
|
+
setRefreshKey((k) => k + 1);
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
const message = error instanceof Error ? error.message : "Failed to delete element";
|
|
1929
|
+
showToast(message);
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
[activeCompPath, editHistory.recordEdit, showToast, writeProjectFile],
|
|
1933
|
+
);
|
|
1934
|
+
|
|
1935
|
+
// ── Consolidated keyboard shortcuts ────────────────────────────────
|
|
1936
|
+
// All app-level window keydown handlers live here.
|
|
1937
|
+
// Component-scoped shortcuts (playback J/K/L/Space, caption nudge)
|
|
1938
|
+
// stay in their respective hooks.
|
|
1939
|
+
const handleToggleRef = useRef(handleTimelineToggleHotkey);
|
|
1940
|
+
handleToggleRef.current = handleTimelineToggleHotkey;
|
|
1941
|
+
const handleDeleteRef = useRef(handleTimelineElementDelete);
|
|
1942
|
+
handleDeleteRef.current = handleTimelineElementDelete;
|
|
1943
|
+
const handleDomEditDeleteRef = useRef(handleDomEditElementDelete);
|
|
1944
|
+
handleDomEditDeleteRef.current = handleDomEditElementDelete;
|
|
1945
|
+
|
|
1946
|
+
handleAppKeyDownRef.current = (event: KeyboardEvent) => {
|
|
1947
|
+
// Shift+T — toggle timeline
|
|
1948
|
+
handleToggleRef.current(event);
|
|
1949
|
+
|
|
1950
|
+
// Cmd/Ctrl+Z — undo, Cmd/Ctrl+Shift+Z or Ctrl+Y — redo
|
|
1951
|
+
if (event.metaKey || event.ctrlKey) {
|
|
1952
|
+
if (!shouldIgnoreHistoryShortcut(event.target)) {
|
|
1953
|
+
const key = event.key.toLowerCase();
|
|
1954
|
+
if (key === "z" && !event.shiftKey) {
|
|
1955
|
+
event.preventDefault();
|
|
1956
|
+
void handleUndoRef.current();
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
|
|
1960
|
+
event.preventDefault();
|
|
1961
|
+
void handleRedoRef.current();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Cmd/Ctrl+1 — sidebar: Compositions tab
|
|
1967
|
+
if (event.key === "1") {
|
|
1968
|
+
event.preventDefault();
|
|
1969
|
+
leftSidebarRef.current?.selectTab("compositions");
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Cmd/Ctrl+2 — sidebar: Assets tab
|
|
1974
|
+
if (event.key === "2") {
|
|
1975
|
+
event.preventDefault();
|
|
1976
|
+
leftSidebarRef.current?.selectTab("assets");
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Delete / Backspace — remove selected element (timeline clip or preview selection)
|
|
1982
|
+
if (
|
|
1983
|
+
(event.key === "Delete" || event.key === "Backspace") &&
|
|
1984
|
+
!event.metaKey &&
|
|
1985
|
+
!event.ctrlKey &&
|
|
1986
|
+
!event.altKey &&
|
|
1987
|
+
!isEditableTarget(event.target)
|
|
1988
|
+
) {
|
|
1989
|
+
const { selectedElementId, elements } = usePlayerStore.getState();
|
|
1990
|
+
if (selectedElementId) {
|
|
1991
|
+
const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
|
|
1992
|
+
if (element) {
|
|
1993
|
+
event.preventDefault();
|
|
1994
|
+
void handleDeleteRef.current(element);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const domSelection = domEditSelectionRef.current;
|
|
1999
|
+
if (domSelection) {
|
|
2000
|
+
event.preventDefault();
|
|
2001
|
+
void handleDomEditDeleteRef.current(domSelection);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
2007
|
+
useEffect(() => {
|
|
2008
|
+
function handleAppKeyDown(event: KeyboardEvent) {
|
|
2009
|
+
handleAppKeyDownRef.current?.(event);
|
|
2010
|
+
}
|
|
2011
|
+
window.addEventListener("keydown", handleAppKeyDown, true);
|
|
2012
|
+
return () => window.removeEventListener("keydown", handleAppKeyDown, true);
|
|
2013
|
+
}, []);
|
|
2014
|
+
|
|
1744
2015
|
const handleBlockedTimelineEdit = useCallback(
|
|
1745
2016
|
(_element: TimelineElement) => {
|
|
1746
2017
|
const now = Date.now();
|
|
@@ -1771,6 +2042,8 @@ export function StudioApp() {
|
|
|
1771
2042
|
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
|
1772
2043
|
) => {
|
|
1773
2044
|
setAgentPromptTagSnippet(undefined);
|
|
2045
|
+
setAgentPromptSelectionContext(undefined);
|
|
2046
|
+
setAgentModalAnchorPoint(null);
|
|
1774
2047
|
setCopiedAgentPrompt(false);
|
|
1775
2048
|
if (!selection) {
|
|
1776
2049
|
domEditSelectionRef.current = null;
|
|
@@ -2228,6 +2501,8 @@ export function StudioApp() {
|
|
|
2228
2501
|
handleUndoRef.current = handleUndo;
|
|
2229
2502
|
handleRedoRef.current = handleRedo;
|
|
2230
2503
|
|
|
2504
|
+
// History hotkey — no longer has its own window listener (consolidated
|
|
2505
|
+
// handler covers it), but kept as a named callback for iframe forwarding.
|
|
2231
2506
|
const handleHistoryHotkey = useCallback((event: KeyboardEvent) => {
|
|
2232
2507
|
if (!(event.metaKey || event.ctrlKey)) return;
|
|
2233
2508
|
if (shouldIgnoreHistoryShortcut(event.target)) return;
|
|
@@ -2243,12 +2518,6 @@ export function StudioApp() {
|
|
|
2243
2518
|
}
|
|
2244
2519
|
}, []);
|
|
2245
2520
|
|
|
2246
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
2247
|
-
useEffect(() => {
|
|
2248
|
-
window.addEventListener("keydown", handleHistoryHotkey, true);
|
|
2249
|
-
return () => window.removeEventListener("keydown", handleHistoryHotkey, true);
|
|
2250
|
-
}, [handleHistoryHotkey]);
|
|
2251
|
-
|
|
2252
2521
|
const syncPreviewHistoryHotkey = useCallback(
|
|
2253
2522
|
(iframe: HTMLIFrameElement | null) => {
|
|
2254
2523
|
previewHistoryHotkeyCleanupRef.current?.();
|
|
@@ -2296,13 +2565,13 @@ export function StudioApp() {
|
|
|
2296
2565
|
(clientX: number, clientY: number, options?: { preferClipAncestor?: boolean }) => {
|
|
2297
2566
|
const iframe = previewIframeRef.current;
|
|
2298
2567
|
if (!iframe || captionEditMode) return null;
|
|
2299
|
-
const target = getPreviewTargetFromPointer(iframe, clientX, clientY);
|
|
2568
|
+
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
|
2300
2569
|
if (!target) return null;
|
|
2301
2570
|
return buildDomSelectionFromTarget(target, {
|
|
2302
2571
|
preferClipAncestor: options?.preferClipAncestor,
|
|
2303
2572
|
});
|
|
2304
2573
|
},
|
|
2305
|
-
[buildDomSelectionFromTarget, captionEditMode],
|
|
2574
|
+
[activeCompPath, buildDomSelectionFromTarget, captionEditMode],
|
|
2306
2575
|
);
|
|
2307
2576
|
|
|
2308
2577
|
const updateDomEditHoverSelection = useCallback((selection: DomEditSelection | null) => {
|
|
@@ -2398,8 +2667,21 @@ export function StudioApp() {
|
|
|
2398
2667
|
|
|
2399
2668
|
const selection = buildDomSelectionForTimelineElement(element);
|
|
2400
2669
|
if (selection) applyDomSelection(selection);
|
|
2670
|
+
|
|
2671
|
+
const key = getTimelineElementKey(element);
|
|
2672
|
+
if (STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED && key && canInspectTimelineElement(element)) {
|
|
2673
|
+
setInspectedTimelineElementId(key);
|
|
2674
|
+
setLeftCollapsed(false);
|
|
2675
|
+
|
|
2676
|
+
const iframe = previewIframeRef.current;
|
|
2677
|
+
if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
|
|
2678
|
+
seekStudioPreview(iframe, element.start);
|
|
2679
|
+
}
|
|
2680
|
+
} else {
|
|
2681
|
+
setInspectedTimelineElementId(null);
|
|
2682
|
+
}
|
|
2401
2683
|
},
|
|
2402
|
-
[applyDomSelection, buildDomSelectionForTimelineElement],
|
|
2684
|
+
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime],
|
|
2403
2685
|
);
|
|
2404
2686
|
|
|
2405
2687
|
const handleTimelineElementInspect = useCallback(
|
|
@@ -2426,17 +2708,6 @@ export function StudioApp() {
|
|
|
2426
2708
|
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
|
|
2427
2709
|
);
|
|
2428
2710
|
|
|
2429
|
-
const handleToggleTimelineElementThumbnail = useCallback((element: TimelineElement) => {
|
|
2430
|
-
const key = getTimelineElementKey(element);
|
|
2431
|
-
if (!key) return;
|
|
2432
|
-
setThumbnailedTimelineElementIds((current) => {
|
|
2433
|
-
const next = new Set(current);
|
|
2434
|
-
if (next.has(key)) next.delete(key);
|
|
2435
|
-
else next.add(key);
|
|
2436
|
-
return next;
|
|
2437
|
-
});
|
|
2438
|
-
}, []);
|
|
2439
|
-
|
|
2440
2711
|
const handleTimelineLayerSelect = useCallback(
|
|
2441
2712
|
(layer: DomEditLayerItem) => {
|
|
2442
2713
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
@@ -2816,15 +3087,26 @@ export function StudioApp() {
|
|
|
2816
3087
|
buildDomEditStylePatchOperation("background-size", "contain"),
|
|
2817
3088
|
);
|
|
2818
3089
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3090
|
+
try {
|
|
3091
|
+
await persistDomEditOperations(domEditSelection, operations, {
|
|
3092
|
+
label: "Edit layer style",
|
|
3093
|
+
skipRefresh: true,
|
|
3094
|
+
prepareContent: importedFont
|
|
3095
|
+
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
3096
|
+
: undefined,
|
|
3097
|
+
});
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
|
|
3100
|
+
}
|
|
3101
|
+
refreshDomEditSelectionFromPreview(domEditSelection);
|
|
2826
3102
|
},
|
|
2827
|
-
[
|
|
3103
|
+
[
|
|
3104
|
+
activeCompPath,
|
|
3105
|
+
domEditSelection,
|
|
3106
|
+
persistDomEditOperations,
|
|
3107
|
+
refreshDomEditSelectionFromPreview,
|
|
3108
|
+
resolveImportedFontAsset,
|
|
3109
|
+
],
|
|
2828
3110
|
);
|
|
2829
3111
|
|
|
2830
3112
|
const handleDomTextCommit = useCallback(
|
|
@@ -3023,6 +3305,8 @@ export function StudioApp() {
|
|
|
3023
3305
|
const handleAskAgent = useCallback(() => {
|
|
3024
3306
|
if (!domEditSelection) return;
|
|
3025
3307
|
setAgentPromptTagSnippet(undefined);
|
|
3308
|
+
setAgentPromptSelectionContext(undefined);
|
|
3309
|
+
setAgentModalAnchorPoint(null);
|
|
3026
3310
|
void preloadAgentPromptSnippet(domEditSelection);
|
|
3027
3311
|
setAgentModalOpen(true);
|
|
3028
3312
|
}, [domEditSelection, preloadAgentPromptSnippet]);
|
|
@@ -3037,6 +3321,7 @@ export function StudioApp() {
|
|
|
3037
3321
|
selection: domEditSelection,
|
|
3038
3322
|
currentTime,
|
|
3039
3323
|
tagSnippet,
|
|
3324
|
+
selectionContext: agentPromptSelectionContext,
|
|
3040
3325
|
userInstruction,
|
|
3041
3326
|
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
|
|
3042
3327
|
});
|
|
@@ -3048,11 +3333,21 @@ export function StudioApp() {
|
|
|
3048
3333
|
}
|
|
3049
3334
|
|
|
3050
3335
|
setAgentModalOpen(false);
|
|
3336
|
+
setAgentPromptSelectionContext(undefined);
|
|
3337
|
+
setAgentModalAnchorPoint(null);
|
|
3051
3338
|
if (copiedAgentTimerRef.current) clearTimeout(copiedAgentTimerRef.current);
|
|
3052
3339
|
setCopiedAgentPrompt(true);
|
|
3053
3340
|
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
|
|
3054
3341
|
},
|
|
3055
|
-
[
|
|
3342
|
+
[
|
|
3343
|
+
activeCompPath,
|
|
3344
|
+
agentPromptSelectionContext,
|
|
3345
|
+
agentPromptTagSnippet,
|
|
3346
|
+
currentTime,
|
|
3347
|
+
domEditSelection,
|
|
3348
|
+
projectDir,
|
|
3349
|
+
showToast,
|
|
3350
|
+
],
|
|
3056
3351
|
);
|
|
3057
3352
|
|
|
3058
3353
|
const handlePreviewIframeRef = useCallback(
|
|
@@ -3070,9 +3365,9 @@ export function StudioApp() {
|
|
|
3070
3365
|
|
|
3071
3366
|
const handlePreviewCanvasMouseDown = useCallback(
|
|
3072
3367
|
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3073
|
-
if (!
|
|
3368
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
|
|
3074
3369
|
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
3075
|
-
preferClipAncestor: options?.preferClipAncestor ??
|
|
3370
|
+
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
3076
3371
|
});
|
|
3077
3372
|
if (!nextSelection) {
|
|
3078
3373
|
if (!e.shiftKey) applyDomSelection(null, { revealPanel: false });
|
|
@@ -3080,14 +3375,35 @@ export function StudioApp() {
|
|
|
3080
3375
|
}
|
|
3081
3376
|
e.preventDefault();
|
|
3082
3377
|
e.stopPropagation();
|
|
3378
|
+
const localPointer = previewIframeRef.current
|
|
3379
|
+
? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
|
|
3380
|
+
: null;
|
|
3083
3381
|
applyDomSelection(nextSelection, { additive: e.shiftKey });
|
|
3382
|
+
if (
|
|
3383
|
+
!e.shiftKey &&
|
|
3384
|
+
localPointer &&
|
|
3385
|
+
isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
|
|
3386
|
+
) {
|
|
3387
|
+
setAgentPromptSelectionContext(
|
|
3388
|
+
buildRasterClickSelectionContext(nextSelection, localPointer),
|
|
3389
|
+
);
|
|
3390
|
+
setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
3391
|
+
void preloadAgentPromptSnippet(nextSelection);
|
|
3392
|
+
setAgentModalOpen(true);
|
|
3393
|
+
}
|
|
3084
3394
|
},
|
|
3085
|
-
[
|
|
3395
|
+
[
|
|
3396
|
+
applyDomSelection,
|
|
3397
|
+
captionEditMode,
|
|
3398
|
+
compositionLoading,
|
|
3399
|
+
preloadAgentPromptSnippet,
|
|
3400
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3401
|
+
],
|
|
3086
3402
|
);
|
|
3087
3403
|
|
|
3088
3404
|
const handlePreviewCanvasPointerMove = useCallback(
|
|
3089
3405
|
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3090
|
-
if (!
|
|
3406
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
|
|
3091
3407
|
updateDomEditHoverSelection(null);
|
|
3092
3408
|
return null;
|
|
3093
3409
|
}
|
|
@@ -3098,7 +3414,12 @@ export function StudioApp() {
|
|
|
3098
3414
|
updateDomEditHoverSelection(nextSelection);
|
|
3099
3415
|
return nextSelection;
|
|
3100
3416
|
},
|
|
3101
|
-
[
|
|
3417
|
+
[
|
|
3418
|
+
captionEditMode,
|
|
3419
|
+
compositionLoading,
|
|
3420
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3421
|
+
updateDomEditHoverSelection,
|
|
3422
|
+
],
|
|
3102
3423
|
);
|
|
3103
3424
|
|
|
3104
3425
|
const handlePreviewCanvasPointerLeave = useCallback(() => {
|
|
@@ -3397,6 +3718,7 @@ export function StudioApp() {
|
|
|
3397
3718
|
}),
|
|
3398
3719
|
);
|
|
3399
3720
|
|
|
3721
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
3400
3722
|
await saveProjectFilesWithHistory({
|
|
3401
3723
|
projectId: pid,
|
|
3402
3724
|
label: "Add timeline asset",
|
|
@@ -3884,6 +4206,7 @@ export function StudioApp() {
|
|
|
3884
4206
|
</div>
|
|
3885
4207
|
) : (
|
|
3886
4208
|
<LeftSidebar
|
|
4209
|
+
ref={leftSidebarRef}
|
|
3887
4210
|
width={leftWidth}
|
|
3888
4211
|
projectId={projectId}
|
|
3889
4212
|
compositions={compositions}
|
|
@@ -3965,9 +4288,8 @@ export function StudioApp() {
|
|
|
3965
4288
|
onInspectTimelineElement={handleTimelineElementInspect}
|
|
3966
4289
|
inspectedTimelineElementId={inspectedTimelineElementId}
|
|
3967
4290
|
timelineLayerChildCounts={timelineLayerChildCounts}
|
|
3968
|
-
thumbnailedTimelineElementIds={thumbnailedTimelineElementIds}
|
|
3969
|
-
onToggleTimelineElementThumbnail={handleToggleTimelineElementThumbnail}
|
|
3970
4291
|
onCompIdToSrcChange={setCompIdToSrc}
|
|
4292
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
3971
4293
|
onCompositionChange={(compPath) => {
|
|
3972
4294
|
// Sync activeCompPath when user drills down via timeline double-click
|
|
3973
4295
|
// or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
|
|
@@ -3984,7 +4306,7 @@ export function StudioApp() {
|
|
|
3984
4306
|
iframeRef={previewIframeRef}
|
|
3985
4307
|
activeCompositionPath={activeCompPath}
|
|
3986
4308
|
hoverSelection={
|
|
3987
|
-
|
|
4309
|
+
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
|
|
3988
4310
|
? domEditHoverSelection
|
|
3989
4311
|
: null
|
|
3990
4312
|
}
|
|
@@ -4093,6 +4415,7 @@ export function StudioApp() {
|
|
|
4093
4415
|
projectId={projectId}
|
|
4094
4416
|
assets={assets}
|
|
4095
4417
|
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
4418
|
+
multiSelectCount={domEditGroupSelections.length}
|
|
4096
4419
|
copiedAgentPrompt={copiedAgentPrompt}
|
|
4097
4420
|
onClearSelection={clearDomSelection}
|
|
4098
4421
|
onSetStyle={handleDomStyleCommit}
|
|
@@ -4122,9 +4445,9 @@ export function StudioApp() {
|
|
|
4122
4445
|
projectId={projectId}
|
|
4123
4446
|
onDelete={renderQueue.deleteRender}
|
|
4124
4447
|
onClearCompleted={renderQueue.clearCompleted}
|
|
4125
|
-
onStartRender={async (format, quality) => {
|
|
4448
|
+
onStartRender={async (format, quality, resolution, fps) => {
|
|
4126
4449
|
await waitForPendingDomEditSaves();
|
|
4127
|
-
await renderQueue.startRender(
|
|
4450
|
+
await renderQueue.startRender({ fps, quality, format, resolution });
|
|
4128
4451
|
}}
|
|
4129
4452
|
isRendering={renderQueue.isRendering}
|
|
4130
4453
|
/>
|
|
@@ -4155,8 +4478,13 @@ export function StudioApp() {
|
|
|
4155
4478
|
{agentModalOpen && domEditSelection && (
|
|
4156
4479
|
<AskAgentModal
|
|
4157
4480
|
selectionLabel={domEditSelection.label}
|
|
4481
|
+
anchorPoint={agentModalAnchorPoint}
|
|
4158
4482
|
onSubmit={handleAgentModalSubmit}
|
|
4159
|
-
onClose={() =>
|
|
4483
|
+
onClose={() => {
|
|
4484
|
+
setAgentModalOpen(false);
|
|
4485
|
+
setAgentPromptSelectionContext(undefined);
|
|
4486
|
+
setAgentModalAnchorPoint(null);
|
|
4487
|
+
}}
|
|
4160
4488
|
/>
|
|
4161
4489
|
)}
|
|
4162
4490
|
|