@hyperframes/studio 0.7.60 → 0.7.62
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-CWpmBjzv.js +459 -0
- package/dist/assets/{index-D6etaey-.js → index-BJzSiIBj.js} +1 -1
- package/dist/assets/index-Ceyz8Qt2.css +1 -0
- package/dist/assets/{index-Dh_WhagG.js → index-DJQPB78e.js} +1 -1
- package/dist/assets/index-vDRVQtTK.js +426 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.html +2 -2
- package/dist/index.js +4392 -3314
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +15 -15
- package/src/components/DesignPanelPromoteProvider.tsx +27 -1
- package/src/components/EditorShell.tsx +12 -0
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioLeftSidebar.tsx +3 -0
- package/src/components/StudioRightPanel.tsx +34 -26
- package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
- package/src/components/editor/PromotableControl.tsx +4 -2
- package/src/components/editor/PropertyPanel.tsx +151 -149
- package/src/components/editor/PropertyPanelFlat.tsx +50 -42
- package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
- package/src/components/editor/domEditing.test.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +6 -6
- package/src/components/editor/manualEditingAvailability.ts +5 -3
- package/src/components/editor/propertyPanelColor.test.tsx +31 -0
- package/src/components/editor/propertyPanelColor.tsx +150 -28
- package/src/components/editor/propertyPanelCommitField.tsx +178 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +44 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +26 -10
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +21 -32
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -2
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +18 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +3 -92
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +6 -0
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/components/sidebar/LeftSidebar.tsx +3 -0
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
- package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
- package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
- package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
- package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
- package/src/components/storyboard/storyboardReviewStage.ts +45 -0
- package/src/components/storyboard/useFrameComments.ts +22 -6
- package/src/contexts/StudioContext.tsx +4 -0
- package/src/contexts/ViewModeContext.tsx +60 -7
- package/src/hooks/timelineEditingHelpers.ts +10 -4
- package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
- package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
- package/src/hooks/usePreviewPersistence.ts +7 -1
- package/src/hooks/useRazorSplit.history.test.tsx +21 -28
- package/src/hooks/useRazorSplit.test.tsx +20 -9
- package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
- package/src/hooks/useRazorSplit.ts +78 -350
- package/src/hooks/useSlideshowTabState.test.ts +96 -0
- package/src/hooks/useSlideshowTabState.ts +61 -0
- package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +52 -2
- package/src/hooks/useTimelineEditing.ts +19 -17
- package/src/hooks/useTimelineGroupEditing.ts +6 -7
- package/src/player/components/Timeline.tsx +12 -1
- package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
- package/src/player/components/timelineAuthoredTrack.ts +29 -0
- package/src/player/components/timelineCallbacks.ts +4 -0
- package/src/player/components/timelineClipDragCommit.test.ts +176 -0
- package/src/player/components/timelineClipDragCommit.ts +77 -105
- package/src/player/components/timelineClipDragPreview.test.ts +33 -1
- package/src/player/components/timelineClipDragPreview.ts +2 -1
- package/src/player/components/timelineCollision.test.ts +18 -0
- package/src/player/components/timelineCollision.ts +12 -9
- package/src/player/components/timelineDragDrop.ts +28 -7
- package/src/player/components/timelineEditCapabilities.ts +4 -1
- package/src/player/components/timelineGroupEditing.test.ts +10 -0
- package/src/player/components/timelineGroupEditing.ts +4 -3
- package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
- package/src/player/components/timelineLayout.ts +3 -1
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.ts +5 -0
- package/src/player/components/useTimelineEditPinning.ts +12 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/lib/playbackTypes.ts +9 -0
- package/src/player/lib/runtimeProtocol.test.ts +6 -1
- package/src/player/lib/timelineDOM.test.ts +58 -0
- package/src/player/lib/timelineDOM.ts +26 -12
- package/src/player/lib/timelineElementHelpers.ts +21 -11
- package/src/player/lib/timelineIframeHelpers.ts +153 -107
- package/src/player/store/playerStore.ts +2 -1
- package/src/utils/razorSplitTransaction.test.ts +175 -0
- package/src/utils/razorSplitTransaction.ts +200 -0
- package/src/utils/studioPreviewHelpers.test.ts +27 -0
- package/src/utils/timelineCompositionDrop.test.ts +12 -0
- package/src/utils/timelineCompositionDrop.ts +16 -0
- package/src/utils/timelineCompositionInsert.test.ts +142 -0
- package/src/utils/timelineCompositionInsert.ts +90 -0
- package/src/utils/timelineElementSplit.test.ts +14 -2
- package/src/utils/timelineElementSplit.ts +5 -1
- package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
- package/dist/assets/index-DXbu6IPT.css +0 -1
- package/dist/assets/index-cH6NfVV_.js +0 -426
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { setFrameVoiceover } from "@hyperframes/core/storyboard";
|
|
3
3
|
import type { StoryboardFrameView } from "../../hooks/useStoryboard";
|
|
4
4
|
import { useFileManagerContext } from "../../contexts/FileManagerContext";
|
|
5
5
|
import { useViewMode } from "../../contexts/ViewModeContext";
|
|
6
6
|
import { Button } from "../ui/Button";
|
|
7
7
|
import { FramePoster, posterTime } from "./FramePoster";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
AgentChatMessageButton,
|
|
10
|
+
APPLY_STORYBOARD_FEEDBACK_MESSAGE,
|
|
11
|
+
} from "./AgentChatMessageButton";
|
|
12
|
+
import { FRAME_STATUS_META } from "./frameStatus";
|
|
13
|
+
import type { CommentsSubmitState } from "./useFrameComments";
|
|
9
14
|
|
|
10
15
|
export interface StoryboardFrameFocusProps {
|
|
11
16
|
projectId: string;
|
|
@@ -19,13 +24,26 @@ export interface StoryboardFrameFocusProps {
|
|
|
19
24
|
onSaved: () => void;
|
|
20
25
|
/** Select a composition in the timeline (sets active comp + editing file + sidebar highlight). */
|
|
21
26
|
onSelectComposition: (path: string) => void;
|
|
27
|
+
/** Whether SCRIPT.md exists and owns final narration/TTS. */
|
|
28
|
+
scriptExists: boolean;
|
|
29
|
+
/** Shared board draft for this frame, preserved when entering/leaving focus. */
|
|
30
|
+
commentDraft: string;
|
|
31
|
+
onCommentDraftChange: (text: string) => void;
|
|
32
|
+
pendingComment: string | null;
|
|
33
|
+
pendingCommentCount: number;
|
|
34
|
+
commentDraftCount: number;
|
|
35
|
+
commentsSubmitState: CommentsSubmitState;
|
|
36
|
+
commentsSubmitError: string | null;
|
|
37
|
+
feedbackMessageCopied: boolean;
|
|
38
|
+
onFeedbackMessageCopied: () => void;
|
|
39
|
+
onSaveFeedback: () => void;
|
|
22
40
|
/** Project signature the board was loaded with (busts the poster cache). */
|
|
23
41
|
posterVersion?: string;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
/**
|
|
27
45
|
* Full-area focus on a single frame: large poster, editable voiceover guide,
|
|
28
|
-
*
|
|
46
|
+
* review feedback, full narrative, and a jump into the live preview. Edits
|
|
29
47
|
* are written back to STORYBOARD.md in place (markdown stays canonical).
|
|
30
48
|
*
|
|
31
49
|
* Mounted with a `key` per frame, so `draft` initializes from the frame and a
|
|
@@ -41,25 +59,39 @@ export function StoryboardFrameFocus({
|
|
|
41
59
|
onNavigate,
|
|
42
60
|
onSaved,
|
|
43
61
|
onSelectComposition,
|
|
62
|
+
scriptExists,
|
|
63
|
+
commentDraft,
|
|
64
|
+
onCommentDraftChange,
|
|
65
|
+
pendingComment,
|
|
66
|
+
pendingCommentCount,
|
|
67
|
+
commentDraftCount,
|
|
68
|
+
commentsSubmitState,
|
|
69
|
+
commentsSubmitError,
|
|
70
|
+
feedbackMessageCopied,
|
|
71
|
+
onFeedbackMessageCopied,
|
|
72
|
+
onSaveFeedback,
|
|
44
73
|
posterVersion,
|
|
45
74
|
}: StoryboardFrameFocusProps) {
|
|
46
75
|
const { readProjectFile, writeProjectFile } = useFileManagerContext();
|
|
47
|
-
const { setViewMode } = useViewMode();
|
|
76
|
+
const { setViewMode, registerViewModeGuard } = useViewMode();
|
|
48
77
|
const [draft, setDraft] = useState(frame.voiceover ?? "");
|
|
78
|
+
const [savedVoiceover, setSavedVoiceover] = useState(frame.voiceover ?? "");
|
|
49
79
|
const [busy, setBusy] = useState(false);
|
|
50
80
|
const [error, setError] = useState<string | null>(null);
|
|
51
81
|
|
|
52
82
|
const applyEdit = useCallback(
|
|
53
83
|
async (edit: (source: string) => string) => {
|
|
54
|
-
if (busy) return; // one read-modify-write at a time; avoids a lost update
|
|
84
|
+
if (busy) return false; // one read-modify-write at a time; avoids a lost update
|
|
55
85
|
setBusy(true);
|
|
56
86
|
setError(null);
|
|
57
87
|
try {
|
|
58
88
|
const source = await readProjectFile(storyboardPath);
|
|
59
89
|
await writeProjectFile(storyboardPath, edit(source));
|
|
60
90
|
onSaved();
|
|
91
|
+
return true;
|
|
61
92
|
} catch (err: unknown) {
|
|
62
93
|
setError(err instanceof Error ? err.message : "failed to save");
|
|
94
|
+
return false;
|
|
63
95
|
} finally {
|
|
64
96
|
setBusy(false);
|
|
65
97
|
}
|
|
@@ -68,11 +100,12 @@ export function StoryboardFrameFocus({
|
|
|
68
100
|
);
|
|
69
101
|
|
|
70
102
|
const title = frame.title ?? `Frame ${frame.index}`;
|
|
71
|
-
const dirty = draft !==
|
|
72
|
-
const canOpenPreview = frame.srcExists && Boolean(frame.src);
|
|
103
|
+
const dirty = draft !== savedVoiceover;
|
|
104
|
+
const canOpenPreview = frame.status !== "outline" && frame.srcExists && Boolean(frame.src);
|
|
73
105
|
|
|
74
|
-
const saveVoiceover = useCallback(() => {
|
|
75
|
-
|
|
106
|
+
const saveVoiceover = useCallback(async () => {
|
|
107
|
+
const saved = await applyEdit((src) => setFrameVoiceover(src, frame.index, draft));
|
|
108
|
+
if (saved) setSavedVoiceover(draft);
|
|
76
109
|
}, [applyEdit, frame.index, draft]);
|
|
77
110
|
|
|
78
111
|
// Closing the tab with a dirty voiceover would lose it silently — same
|
|
@@ -90,7 +123,12 @@ export function StoryboardFrameFocus({
|
|
|
90
123
|
// dirty. An in-flight save does NOT count as safe: if it fails after unmount
|
|
91
124
|
// the error lands on an unmounted component and the draft is silently lost,
|
|
92
125
|
// so keep confirming until the save actually lands (dirty clears on success).
|
|
93
|
-
const confirmLeave = (
|
|
126
|
+
const confirmLeave = useCallback(
|
|
127
|
+
() => !dirty || window.confirm("Discard unsaved voiceover changes?"),
|
|
128
|
+
[dirty],
|
|
129
|
+
);
|
|
130
|
+
useEffect(() => registerViewModeGuard(confirmLeave), [confirmLeave, registerViewModeGuard]);
|
|
131
|
+
|
|
94
132
|
const handleBack = () => {
|
|
95
133
|
if (confirmLeave()) onBack();
|
|
96
134
|
};
|
|
@@ -113,12 +151,12 @@ export function StoryboardFrameFocus({
|
|
|
113
151
|
});
|
|
114
152
|
|
|
115
153
|
const openInPreview = () => {
|
|
154
|
+
if (!setViewMode("timeline")) return;
|
|
116
155
|
if (frame.src) onSelectComposition(frame.src);
|
|
117
|
-
setViewMode("timeline");
|
|
118
156
|
};
|
|
119
157
|
|
|
120
158
|
return (
|
|
121
|
-
<div className="flex flex-1 min-h-0 flex-col bg-neutral-950 text-neutral-200">
|
|
159
|
+
<div className="flex w-full max-w-[100vw] flex-1 min-h-0 min-w-0 flex-col overflow-hidden bg-neutral-950 text-neutral-200">
|
|
122
160
|
<div className="flex items-center gap-3 border-b border-neutral-800 px-4 py-2">
|
|
123
161
|
<button
|
|
124
162
|
type="button"
|
|
@@ -127,10 +165,10 @@ export function StoryboardFrameFocus({
|
|
|
127
165
|
>
|
|
128
166
|
← Board
|
|
129
167
|
</button>
|
|
130
|
-
<span className="text-sm font-medium text-neutral-200">
|
|
168
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium text-neutral-200">
|
|
131
169
|
Frame {frame.number ?? frame.index} — {title}
|
|
132
170
|
</span>
|
|
133
|
-
<div className="
|
|
171
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
134
172
|
<NavButton
|
|
135
173
|
label="‹ Prev"
|
|
136
174
|
disabled={frame.index <= 1}
|
|
@@ -144,8 +182,41 @@ export function StoryboardFrameFocus({
|
|
|
144
182
|
</div>
|
|
145
183
|
</div>
|
|
146
184
|
|
|
147
|
-
|
|
148
|
-
<div className="flex
|
|
185
|
+
{(commentDraftCount > 0 || pendingCommentCount > 0) && (
|
|
186
|
+
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-neutral-800 bg-neutral-900/80 px-4 py-2">
|
|
187
|
+
<div>
|
|
188
|
+
<div className="text-xs font-medium text-neutral-200">
|
|
189
|
+
{commentDraftCount > 0 ? "Feedback ready to save" : "Feedback saved"}
|
|
190
|
+
</div>
|
|
191
|
+
<div className="text-[11px] text-neutral-500">
|
|
192
|
+
{commentDraftCount > 0
|
|
193
|
+
? "Save this batch and copy the message for your agent."
|
|
194
|
+
: feedbackMessageCopied
|
|
195
|
+
? "Message copied — paste it in your terminal or IDE agent chat."
|
|
196
|
+
: "The agent has not been notified yet."}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
{commentDraftCount > 0 ? (
|
|
200
|
+
<Button
|
|
201
|
+
size="sm"
|
|
202
|
+
variant="primary"
|
|
203
|
+
onClick={onSaveFeedback}
|
|
204
|
+
loading={commentsSubmitState === "saving"}
|
|
205
|
+
>
|
|
206
|
+
Save & copy message ({commentDraftCount})
|
|
207
|
+
</Button>
|
|
208
|
+
) : (
|
|
209
|
+
<AgentChatMessageButton
|
|
210
|
+
message={APPLY_STORYBOARD_FEEDBACK_MESSAGE}
|
|
211
|
+
label={feedbackMessageCopied ? "Copy again" : "Copy prompt for agent"}
|
|
212
|
+
onCopied={onFeedbackMessageCopied}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<div className="flex flex-1 min-h-0 flex-col overflow-auto lg:flex-row lg:overflow-hidden">
|
|
219
|
+
<div className="flex w-full shrink-0 items-center justify-center bg-neutral-900/40 p-4 sm:p-8 lg:h-full lg:w-3/5">
|
|
149
220
|
<div className="aspect-video w-full max-w-[900px] overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900">
|
|
150
221
|
{canOpenPreview && frame.src ? (
|
|
151
222
|
<FramePoster
|
|
@@ -157,19 +228,13 @@ export function StoryboardFrameFocus({
|
|
|
157
228
|
posterVersion={posterVersion}
|
|
158
229
|
/>
|
|
159
230
|
) : (
|
|
160
|
-
<
|
|
161
|
-
{frame.status === "outline" ? "Not built yet" : "No preview"}
|
|
162
|
-
</div>
|
|
231
|
+
<FramePlan frame={frame} />
|
|
163
232
|
)}
|
|
164
233
|
</div>
|
|
165
234
|
</div>
|
|
166
235
|
|
|
167
|
-
<div className="w-
|
|
168
|
-
<
|
|
169
|
-
status={frame.status}
|
|
170
|
-
busy={busy}
|
|
171
|
-
onSet={(s) => applyEdit((src) => setFrameStatus(src, frame.index, s))}
|
|
172
|
-
/>
|
|
236
|
+
<div className="w-full shrink-0 space-y-6 border-t border-neutral-800 px-4 py-5 sm:px-6 lg:h-full lg:w-2/5 lg:overflow-auto lg:border-t-0 lg:border-l">
|
|
237
|
+
<ReadOnlyStatus status={frame.status} />
|
|
173
238
|
|
|
174
239
|
<div className="flex flex-wrap gap-x-6 gap-y-1 text-[11px] text-neutral-500">
|
|
175
240
|
{frame.duration && <span>Duration {frame.duration}</span>}
|
|
@@ -178,7 +243,10 @@ export function StoryboardFrameFocus({
|
|
|
178
243
|
|
|
179
244
|
<section>
|
|
180
245
|
<div className="mb-1 flex items-center justify-between">
|
|
181
|
-
<h3
|
|
246
|
+
<h3
|
|
247
|
+
className="text-xs font-semibold uppercase tracking-wider text-neutral-400"
|
|
248
|
+
title="Storyboard voiceover is a guide; SCRIPT.md is the final TTS source."
|
|
249
|
+
>
|
|
182
250
|
🎙 Voiceover <span className="font-normal normal-case text-neutral-600">guide</span>
|
|
183
251
|
</h3>
|
|
184
252
|
<Button
|
|
@@ -187,31 +255,70 @@ export function StoryboardFrameFocus({
|
|
|
187
255
|
onClick={saveVoiceover}
|
|
188
256
|
disabled={!dirty}
|
|
189
257
|
loading={busy}
|
|
190
|
-
className="bg-emerald-600 text-white enabled:hover:bg-emerald-500 shadow-none"
|
|
191
258
|
>
|
|
192
|
-
{busy ? "Saving…" : "Save"}
|
|
259
|
+
{busy ? "Saving…" : "Save voiceover"}
|
|
193
260
|
</Button>
|
|
194
261
|
</div>
|
|
195
262
|
<textarea
|
|
196
263
|
value={draft}
|
|
197
264
|
onChange={(e) => setDraft(e.target.value)}
|
|
198
|
-
onBlur={() => {
|
|
199
|
-
// Same autosave paradigm as the status row above — mixed save
|
|
200
|
-
// models inside one panel taught users the panel autosaves,
|
|
201
|
-
// then lost their voiceover. Explicit Save stays as the
|
|
202
|
-
// affordance; blur is the safety net.
|
|
203
|
-
if (dirty && !busy) void saveVoiceover();
|
|
204
|
-
}}
|
|
205
265
|
rows={3}
|
|
206
266
|
placeholder="What the narrator says over this frame…"
|
|
207
267
|
className="w-full resize-y rounded border border-neutral-800 bg-neutral-900 p-2 text-sm text-neutral-200 outline-none focus:border-neutral-600"
|
|
208
268
|
/>
|
|
209
269
|
<p className="mt-1 text-[11px] text-neutral-600">
|
|
210
|
-
|
|
270
|
+
{dirty
|
|
271
|
+
? "Unsaved changes"
|
|
272
|
+
: scriptExists
|
|
273
|
+
? "Saved. SCRIPT.md drives final TTS."
|
|
274
|
+
: "Saved to STORYBOARD.md. This voiceover guides narration for the frame."}
|
|
211
275
|
</p>
|
|
212
276
|
{error && <p className="mt-1 text-[11px] text-red-400">{error}</p>}
|
|
213
277
|
</section>
|
|
214
278
|
|
|
279
|
+
<section>
|
|
280
|
+
<div className="mb-1">
|
|
281
|
+
<h3 className="text-xs font-semibold uppercase tracking-wider text-neutral-400">
|
|
282
|
+
Frame feedback
|
|
283
|
+
</h3>
|
|
284
|
+
</div>
|
|
285
|
+
<textarea
|
|
286
|
+
value={commentDraft}
|
|
287
|
+
onChange={(e) => onCommentDraftChange(e.target.value)}
|
|
288
|
+
rows={3}
|
|
289
|
+
placeholder="Tell your agent what to change in this frame…"
|
|
290
|
+
aria-label={`Comment on ${title}`}
|
|
291
|
+
className="w-full resize-y rounded border border-neutral-800 bg-neutral-900 p-2 text-sm text-neutral-200 placeholder:text-neutral-600 outline-none focus:border-sky-700"
|
|
292
|
+
/>
|
|
293
|
+
{pendingCommentCount > 0 ? (
|
|
294
|
+
<div className="mt-2 flex flex-wrap items-center justify-between gap-2 rounded-md border border-sky-900/70 bg-sky-950/20 px-2.5 py-2">
|
|
295
|
+
<p className="text-[11px] text-sky-200">
|
|
296
|
+
Paste the agent prompt in your terminal or IDE chat.
|
|
297
|
+
</p>
|
|
298
|
+
<AgentChatMessageButton
|
|
299
|
+
message={APPLY_STORYBOARD_FEEDBACK_MESSAGE}
|
|
300
|
+
onCopied={onFeedbackMessageCopied}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
) : (
|
|
304
|
+
<p className="mt-1 text-[11px] text-neutral-600">
|
|
305
|
+
{commentDraftCount > 0
|
|
306
|
+
? "Your change is ready. Save it using the review bar above."
|
|
307
|
+
: "Add a change to prepare feedback for the agent."}
|
|
308
|
+
</p>
|
|
309
|
+
)}
|
|
310
|
+
{pendingComment && (
|
|
311
|
+
<p className="mt-1 text-[11px] text-sky-400/90">
|
|
312
|
+
<span className="font-medium">Pending:</span> “{pendingComment}”
|
|
313
|
+
</p>
|
|
314
|
+
)}
|
|
315
|
+
{commentsSubmitError && (
|
|
316
|
+
<p className="mt-1 text-[11px] text-red-400">
|
|
317
|
+
Couldn’t submit: {commentsSubmitError}
|
|
318
|
+
</p>
|
|
319
|
+
)}
|
|
320
|
+
</section>
|
|
321
|
+
|
|
215
322
|
{frame.narrative && (
|
|
216
323
|
<section>
|
|
217
324
|
<h3 className="mb-1 text-xs font-semibold uppercase tracking-wider text-neutral-400">
|
|
@@ -221,9 +328,15 @@ export function StoryboardFrameFocus({
|
|
|
221
328
|
</section>
|
|
222
329
|
)}
|
|
223
330
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
331
|
+
{canOpenPreview ? (
|
|
332
|
+
<Button size="sm" variant="secondary" onClick={openInPreview}>
|
|
333
|
+
Open in Preview →
|
|
334
|
+
</Button>
|
|
335
|
+
) : (
|
|
336
|
+
<div className="rounded-md border border-neutral-800 bg-neutral-900/60 px-3 py-2 text-xs text-neutral-500">
|
|
337
|
+
Preview becomes available after your agent builds this frame.
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
227
340
|
</div>
|
|
228
341
|
</div>
|
|
229
342
|
</div>
|
|
@@ -251,38 +364,48 @@ function NavButton({
|
|
|
251
364
|
);
|
|
252
365
|
}
|
|
253
366
|
|
|
254
|
-
function
|
|
255
|
-
status
|
|
256
|
-
busy,
|
|
257
|
-
onSet,
|
|
258
|
-
}: {
|
|
259
|
-
status: FrameStatus;
|
|
260
|
-
busy: boolean;
|
|
261
|
-
onSet: (next: FrameStatus) => void;
|
|
262
|
-
}) {
|
|
367
|
+
function ReadOnlyStatus({ status }: { status: StoryboardFrameView["status"] }) {
|
|
368
|
+
const meta = FRAME_STATUS_META[status];
|
|
263
369
|
return (
|
|
264
370
|
<div className="flex items-center gap-2">
|
|
265
371
|
<span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">
|
|
266
372
|
Status
|
|
267
373
|
</span>
|
|
268
|
-
<
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
374
|
+
<span className={`rounded px-2 py-1 text-xs font-medium ${meta.chipClass}`}>
|
|
375
|
+
{meta.label}
|
|
376
|
+
</span>
|
|
377
|
+
<span className="text-[11px] text-neutral-600">Updated by your agent</span>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// The empty state selects copy from frame status and whichever storyboard fields are available.
|
|
383
|
+
// fallow-ignore-next-line complexity
|
|
384
|
+
function FramePlan({ frame }: { frame: StoryboardFrameView }) {
|
|
385
|
+
const title = frame.title ?? `Frame ${frame.index}`;
|
|
386
|
+
const isOutline = frame.status === "outline";
|
|
387
|
+
return (
|
|
388
|
+
<div className="flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_center,_rgba(38,38,38,0.8),_rgba(10,10,10,1))] p-10">
|
|
389
|
+
<div className="max-w-xl text-center">
|
|
390
|
+
<span className="rounded-full border border-neutral-700 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-neutral-400">
|
|
391
|
+
{isOutline ? "Planned frame" : "Preview unavailable"}
|
|
392
|
+
</span>
|
|
393
|
+
<h2 className="mt-4 text-2xl font-semibold text-neutral-100">{title}</h2>
|
|
394
|
+
{frame.scene && (
|
|
395
|
+
<p className="mt-3 text-base leading-relaxed text-neutral-300">{frame.scene}</p>
|
|
396
|
+
)}
|
|
397
|
+
{!frame.scene && frame.narrative && (
|
|
398
|
+
<p className="mt-3 line-clamp-4 text-sm leading-relaxed text-neutral-400">
|
|
399
|
+
{frame.narrative}
|
|
400
|
+
</p>
|
|
401
|
+
)}
|
|
402
|
+
<p className="mt-5 text-xs text-neutral-600">
|
|
403
|
+
{isOutline
|
|
404
|
+
? "A visual preview will appear when your agent builds the sketch."
|
|
405
|
+
: frame.src
|
|
406
|
+
? `Frame file not found: ${frame.src}`
|
|
407
|
+
: "This frame does not link to a source file."}
|
|
408
|
+
</p>
|
|
286
409
|
</div>
|
|
287
410
|
</div>
|
|
288
411
|
);
|
|
@@ -3,10 +3,14 @@ import type { StoryboardResponse } from "../../hooks/useStoryboard";
|
|
|
3
3
|
import { Button } from "../ui/Button";
|
|
4
4
|
import { StoryboardDirection } from "./StoryboardDirection";
|
|
5
5
|
import { StoryboardGrid } from "./StoryboardGrid";
|
|
6
|
-
import { StoryboardStatusLegend } from "./StoryboardStatusLegend";
|
|
7
6
|
import { StoryboardScriptPanel } from "./StoryboardScriptPanel";
|
|
8
7
|
import { StoryboardSourceEditor, type SourceFile } from "./StoryboardSourceEditor";
|
|
9
8
|
import { StoryboardFrameFocus } from "./StoryboardFrameFocus";
|
|
9
|
+
import { StoryboardReviewGuide } from "./StoryboardReviewGuide";
|
|
10
|
+
import {
|
|
11
|
+
AgentChatMessageButton,
|
|
12
|
+
APPLY_STORYBOARD_FEEDBACK_MESSAGE,
|
|
13
|
+
} from "./AgentChatMessageButton";
|
|
10
14
|
import { useFrameComments, type CommentsSubmitState } from "./useFrameComments";
|
|
11
15
|
|
|
12
16
|
type SubView = "board" | "source";
|
|
@@ -35,6 +39,7 @@ export function StoryboardLoaded({
|
|
|
35
39
|
const [subView, setSubView] = useState<SubView>("board");
|
|
36
40
|
const [sourceDirty, setSourceDirty] = useState(false);
|
|
37
41
|
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
42
|
+
const [feedbackMessageCopied, setFeedbackMessageCopied] = useState(false);
|
|
38
43
|
const comments = useFrameComments(data.frames);
|
|
39
44
|
// When the board refreshes off a project change (agent revised frames), the
|
|
40
45
|
// agent has likely consumed the comments file too — re-check so the pending
|
|
@@ -43,6 +48,20 @@ export function StoryboardLoaded({
|
|
|
43
48
|
useEffect(() => {
|
|
44
49
|
void refreshPending();
|
|
45
50
|
}, [data.signature, refreshPending]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (comments.draftCount > 0) setFeedbackMessageCopied(false);
|
|
53
|
+
}, [comments.draftCount]);
|
|
54
|
+
|
|
55
|
+
const saveFeedbackAndCopyMessage = async () => {
|
|
56
|
+
const saved = await comments.submit();
|
|
57
|
+
if (!saved) return;
|
|
58
|
+
try {
|
|
59
|
+
await navigator.clipboard.writeText(APPLY_STORYBOARD_FEEDBACK_MESSAGE);
|
|
60
|
+
setFeedbackMessageCopied(true);
|
|
61
|
+
} catch {
|
|
62
|
+
setFeedbackMessageCopied(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
46
65
|
const sourceFiles = useMemo<SourceFile[]>(() => {
|
|
47
66
|
const files: SourceFile[] = [{ path: data.path, label: data.path }];
|
|
48
67
|
if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
|
|
@@ -82,31 +101,54 @@ export function StoryboardLoaded({
|
|
|
82
101
|
}
|
|
83
102
|
onSaved={reload}
|
|
84
103
|
onSelectComposition={onSelectComposition}
|
|
104
|
+
scriptExists={Boolean(data.script?.exists)}
|
|
105
|
+
commentDraft={comments.drafts[focusedFrame.index] ?? ""}
|
|
106
|
+
onCommentDraftChange={(text) => comments.setDraft(focusedFrame.index, text)}
|
|
107
|
+
pendingComment={
|
|
108
|
+
comments.pending?.find((entry) => entry.frame === focusedFrame.index)?.text ?? null
|
|
109
|
+
}
|
|
110
|
+
pendingCommentCount={comments.pending?.length ?? 0}
|
|
111
|
+
commentDraftCount={comments.draftCount}
|
|
112
|
+
commentsSubmitState={comments.submitState}
|
|
113
|
+
commentsSubmitError={comments.submitError}
|
|
114
|
+
feedbackMessageCopied={feedbackMessageCopied}
|
|
115
|
+
onFeedbackMessageCopied={() => setFeedbackMessageCopied(true)}
|
|
116
|
+
onSaveFeedback={() => void saveFeedbackAndCopyMessage()}
|
|
85
117
|
posterVersion={data.signature}
|
|
86
118
|
/>
|
|
87
119
|
);
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
return (
|
|
91
|
-
<div className="flex flex-1 min-h-0 flex-col bg-neutral-950 text-neutral-200">
|
|
92
|
-
<div className="flex items-center gap-3 border-b border-neutral-800 px-4 py-2">
|
|
123
|
+
<div className="flex w-full max-w-[100vw] flex-1 min-h-0 min-w-0 flex-col overflow-hidden bg-neutral-950 text-neutral-200">
|
|
124
|
+
<div className="flex flex-wrap items-center gap-3 border-b border-neutral-800 px-4 py-2">
|
|
93
125
|
<SubViewToggle value={subView} onChange={changeSubView} />
|
|
94
126
|
{subView === "board" && (
|
|
95
127
|
<CommentsSubmitBar
|
|
96
128
|
draftCount={comments.draftCount}
|
|
97
129
|
pendingCount={comments.pending?.length ?? 0}
|
|
98
130
|
submitState={comments.submitState}
|
|
99
|
-
|
|
131
|
+
submitError={comments.submitError}
|
|
132
|
+
messageCopied={feedbackMessageCopied}
|
|
133
|
+
onSave={() => void saveFeedbackAndCopyMessage()}
|
|
134
|
+
onMessageCopied={() => setFeedbackMessageCopied(true)}
|
|
100
135
|
/>
|
|
101
136
|
)}
|
|
102
137
|
</div>
|
|
103
138
|
{subView === "board" ? (
|
|
104
139
|
<div className="flex-1 min-h-0 overflow-auto">
|
|
105
|
-
<div className="mx-auto max-w-[1400px] px-8 py-8">
|
|
140
|
+
<div className="mx-auto max-w-[1400px] px-4 py-5 sm:px-8 sm:py-8">
|
|
106
141
|
<StoryboardDirection globals={data.globals} frameCount={data.frames.length} />
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
<StoryboardReviewGuide
|
|
143
|
+
frames={data.frames}
|
|
144
|
+
draftCount={comments.draftCount}
|
|
145
|
+
pendingCount={comments.pending?.length ?? 0}
|
|
146
|
+
onFeedbackMessageCopied={() => setFeedbackMessageCopied(true)}
|
|
147
|
+
/>
|
|
148
|
+
<StoryboardWarnings
|
|
149
|
+
warnings={data.warnings}
|
|
150
|
+
onOpenSource={() => changeSubView("source")}
|
|
151
|
+
/>
|
|
110
152
|
<StoryboardGrid
|
|
111
153
|
projectId={projectId}
|
|
112
154
|
frames={data.frames}
|
|
@@ -135,34 +177,90 @@ function CommentsSubmitBar({
|
|
|
135
177
|
draftCount,
|
|
136
178
|
pendingCount,
|
|
137
179
|
submitState,
|
|
138
|
-
|
|
180
|
+
submitError,
|
|
181
|
+
messageCopied,
|
|
182
|
+
onSave,
|
|
183
|
+
onMessageCopied,
|
|
139
184
|
}: {
|
|
140
185
|
draftCount: number;
|
|
141
186
|
pendingCount: number;
|
|
142
187
|
submitState: CommentsSubmitState;
|
|
143
|
-
|
|
188
|
+
submitError: string | null;
|
|
189
|
+
messageCopied: boolean;
|
|
190
|
+
onSave: () => void;
|
|
191
|
+
onMessageCopied: () => void;
|
|
144
192
|
}) {
|
|
145
193
|
return (
|
|
146
|
-
<div className="ml-auto flex items-center gap-
|
|
194
|
+
<div className="ml-auto flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none">
|
|
147
195
|
{pendingCount > 0 && (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
196
|
+
<>
|
|
197
|
+
<span className="text-xs text-sky-300">
|
|
198
|
+
{messageCopied
|
|
199
|
+
? "Feedback saved · Message copied — paste it in your terminal or IDE agent chat."
|
|
200
|
+
: "Feedback saved · Agent not notified."}
|
|
201
|
+
</span>
|
|
202
|
+
<AgentChatMessageButton
|
|
203
|
+
message={APPLY_STORYBOARD_FEEDBACK_MESSAGE}
|
|
204
|
+
label={messageCopied ? "Copy again" : "Copy prompt for agent"}
|
|
205
|
+
onCopied={onMessageCopied}
|
|
206
|
+
/>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
{pendingCount === 0 && draftCount === 0 && (
|
|
210
|
+
<span className="text-xs text-neutral-500">Add frame comments to request changes.</span>
|
|
211
|
+
)}
|
|
212
|
+
{submitError && (
|
|
213
|
+
<span className="max-w-64 truncate text-xs text-red-400" title={submitError}>
|
|
214
|
+
Couldn’t submit: {submitError}
|
|
151
215
|
</span>
|
|
152
216
|
)}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
217
|
+
{draftCount > 0 && (
|
|
218
|
+
<Button
|
|
219
|
+
variant="primary"
|
|
220
|
+
size="sm"
|
|
221
|
+
loading={submitState === "saving"}
|
|
222
|
+
disabled={submitState === "saving"}
|
|
223
|
+
onClick={onSave}
|
|
224
|
+
>
|
|
225
|
+
Save & copy message ({draftCount})
|
|
226
|
+
</Button>
|
|
227
|
+
)}
|
|
162
228
|
</div>
|
|
163
229
|
);
|
|
164
230
|
}
|
|
165
231
|
|
|
232
|
+
function StoryboardWarnings({
|
|
233
|
+
warnings,
|
|
234
|
+
onOpenSource,
|
|
235
|
+
}: {
|
|
236
|
+
warnings: StoryboardResponse["warnings"];
|
|
237
|
+
onOpenSource: () => void;
|
|
238
|
+
}) {
|
|
239
|
+
if (warnings.length === 0) return null;
|
|
240
|
+
return (
|
|
241
|
+
<details className="mt-3 rounded-lg border border-amber-900/60 bg-amber-950/20 px-4 py-2 text-xs text-amber-200">
|
|
242
|
+
<summary className="cursor-pointer font-medium">
|
|
243
|
+
{warnings.length} storyboard warning{warnings.length === 1 ? "" : "s"}
|
|
244
|
+
</summary>
|
|
245
|
+
<ul className="mt-2 space-y-1 text-amber-200/80">
|
|
246
|
+
{warnings.map((warning, index) => (
|
|
247
|
+
<li key={`${warning.line ?? "unknown"}-${index}`}>
|
|
248
|
+
{warning.line ? `Line ${warning.line}: ` : ""}
|
|
249
|
+
{warning.message}
|
|
250
|
+
</li>
|
|
251
|
+
))}
|
|
252
|
+
</ul>
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
onClick={onOpenSource}
|
|
256
|
+
className="mt-2 rounded text-amber-100 underline underline-offset-2 hover:text-white"
|
|
257
|
+
>
|
|
258
|
+
Open source to fix
|
|
259
|
+
</button>
|
|
260
|
+
</details>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
166
264
|
const SUB_VIEWS: Array<{ value: SubView; label: string }> = [
|
|
167
265
|
{ value: "board", label: "Board" },
|
|
168
266
|
{ value: "source", label: "Source" },
|