@dailies-studio/shot-editor 0.1.0
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/index-B7notge7.css +1 -0
- package/dist/assets/index-BBE6Ta9L.js +52 -0
- package/dist/index.html +13 -0
- package/package.json +45 -0
- package/src/api.ts +289 -0
- package/src/app/App.tsx +394 -0
- package/src/audio/index.tsx +74 -0
- package/src/audio/preview.ts +364 -0
- package/src/bridge.ts +45 -0
- package/src/bridgeTypes.ts +582 -0
- package/src/channels.ts +40 -0
- package/src/main.tsx +15 -0
- package/src/panels/ResizeHandle.tsx +113 -0
- package/src/panels/agent/AgentPanel.tsx +478 -0
- package/src/panels/agent/agent.css +322 -0
- package/src/panels/export/ExportDialog.tsx +284 -0
- package/src/panels/files/FilePanel.tsx +564 -0
- package/src/panels/files/McpSection.tsx +66 -0
- package/src/panels/files/icons.tsx +118 -0
- package/src/panels/header/DocSwitcher.tsx +85 -0
- package/src/panels/header/Header.tsx +132 -0
- package/src/panels/header/HistoryControls.tsx +20 -0
- package/src/panels/header/StatusBadges.tsx +49 -0
- package/src/panels/header/TransportBar.tsx +86 -0
- package/src/panels/header/ZoomControls.tsx +38 -0
- package/src/panels/help/HelpSection.tsx +190 -0
- package/src/panels/inspector/FramingRows.tsx +69 -0
- package/src/panels/inspector/InspectorPanel.tsx +178 -0
- package/src/panels/inspector/MarkerSection.tsx +63 -0
- package/src/panels/inspector/ParamRow.tsx +71 -0
- package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
- package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
- package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
- package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
- package/src/panels/inspector/animate/ShotSection.tsx +126 -0
- package/src/panels/inspector/animate/useBlocks.ts +43 -0
- package/src/panels/inspector/design/AlignRow.tsx +63 -0
- package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
- package/src/panels/inspector/design/ClonerSection.tsx +142 -0
- package/src/panels/inspector/design/ComponentSection.tsx +94 -0
- package/src/panels/inspector/design/CornerSection.tsx +52 -0
- package/src/panels/inspector/design/FillSection.tsx +89 -0
- package/src/panels/inspector/design/HtmlSection.tsx +50 -0
- package/src/panels/inspector/design/InstanceSection.tsx +302 -0
- package/src/panels/inspector/design/MediaSection.tsx +251 -0
- package/src/panels/inspector/design/MeshSection.tsx +172 -0
- package/src/panels/inspector/design/ShadowSection.tsx +73 -0
- package/src/panels/inspector/design/StrokeSection.tsx +85 -0
- package/src/panels/inspector/design/TransformSection.tsx +91 -0
- package/src/panels/inspector/design/TypographySection.tsx +170 -0
- package/src/panels/inspector/scene/CameraSection.tsx +59 -0
- package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
- package/src/panels/inspector/scene/SceneSection.tsx +71 -0
- package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
- package/src/panels/inspector/useDocWrite.ts +268 -0
- package/src/panels/material/MaterialPicker.tsx +172 -0
- package/src/panels/stage/StageMenu.tsx +38 -0
- package/src/panels/stage/StagePanel.tsx +131 -0
- package/src/panels/timeline/AudioLane.tsx +340 -0
- package/src/panels/timeline/BlockRow.tsx +242 -0
- package/src/panels/timeline/ChannelRow.tsx +266 -0
- package/src/panels/timeline/LifespanRow.tsx +165 -0
- package/src/panels/timeline/MarkerLane.tsx +146 -0
- package/src/panels/timeline/SceneStrip.tsx +145 -0
- package/src/panels/timeline/TimelineDock.tsx +488 -0
- package/src/panels/timeline/model.ts +641 -0
- package/src/panels/timeline/snap.ts +191 -0
- package/src/panels/toolbar/AddMenu.tsx +230 -0
- package/src/panels/toolbar/CameraViewBar.tsx +70 -0
- package/src/panels/toolbar/ShadingBar.tsx +42 -0
- package/src/panels/toolbar/ToolDockBar.tsx +110 -0
- package/src/panels/toolbar/ViewModeBar.tsx +49 -0
- package/src/shotTokens.css +131 -0
- package/src/stage/FrustumGizmo.tsx +184 -0
- package/src/stage/GridPlane.tsx +274 -0
- package/src/stage/MoveGizmo.tsx +411 -0
- package/src/stage/PerfHud.tsx +206 -0
- package/src/stage/SplitRenderer.tsx +511 -0
- package/src/stage/StageApp.tsx +3423 -0
- package/src/stage/StageTools.tsx +581 -0
- package/src/stage/bridgeClient.ts +278 -0
- package/src/stage/feed.ts +243 -0
- package/src/stage/frustumGeometry.ts +172 -0
- package/src/stage/gizmoMath.ts +494 -0
- package/src/stage/handles.ts +151 -0
- package/src/stage/instanceModules.ts +152 -0
- package/src/stage/interactions/move.ts +172 -0
- package/src/stage/interactions/panZoom.ts +60 -0
- package/src/stage/isolation.ts +155 -0
- package/src/stage/main.tsx +110 -0
- package/src/stage/modalState.ts +427 -0
- package/src/stage/overlays/Overlays.tsx +1046 -0
- package/src/stage/panes.ts +121 -0
- package/src/stage/penState.ts +112 -0
- package/src/stage/project.ts +402 -0
- package/src/stage/shading.ts +55 -0
- package/src/stage/tokenColor.ts +26 -0
- package/src/stage/view.ts +182 -0
- package/src/stage/workingView.ts +636 -0
- package/src/state/agentActions.ts +203 -0
- package/src/state/clientId.ts +25 -0
- package/src/state/context.tsx +33 -0
- package/src/state/insert.ts +507 -0
- package/src/state/keymap.ts +560 -0
- package/src/state/menu.ts +141 -0
- package/src/state/panelSize.ts +26 -0
- package/src/state/renderEvents.ts +100 -0
- package/src/state/store.ts +4094 -0
- package/src/state/triangles.ts +70 -0
- package/src/state/types.ts +384 -0
- package/src/styles.css +2948 -0
- package/src/tools.ts +181 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Async flows for the shot editor's agent drawer. The store holds state; every
|
|
2
|
+
// await lives here — the sibling chromes' posture, over the SHARED
|
|
3
|
+
// @dailies-studio/agent-client core so the four panels cannot interpret the same
|
|
4
|
+
// SSE stream four different ways.
|
|
5
|
+
//
|
|
6
|
+
// ── WHAT THIS LANE DOES THAT THE OTHERS DO NOT NEED TO ──────────────────
|
|
7
|
+
//
|
|
8
|
+
// Nothing, and that is the point of having one document. The canvas chrome's
|
|
9
|
+
// version carries an `openBoard` arm and a "which board am I focused on"
|
|
10
|
+
// branch. A shot chrome has ONE document open, so an apply is either "the open
|
|
11
|
+
// shot" (the SSE bus adopts it, exactly as it adopts another tab's save) or
|
|
12
|
+
// "some other shot" (it landed on disk; the switcher will show it), and there
|
|
13
|
+
// is no third case to get wrong.
|
|
14
|
+
import { AgentApiError, applyAgentEvent, createItemIds } from "@dailies-studio/agent-client";
|
|
15
|
+
import type { AgentEvent, AgentProposalItem, TranscriptSink } from "@dailies-studio/agent-client";
|
|
16
|
+
import { ApiError, agent } from "../api.js";
|
|
17
|
+
import type { ShotEditorStore } from "./store.js";
|
|
18
|
+
import type { ShotEditorState } from "./types.js";
|
|
19
|
+
|
|
20
|
+
function message(e: unknown): string {
|
|
21
|
+
return e instanceof Error ? e.message : String(e);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 409 from either error class — the shared client throws AgentApiError, this
|
|
25
|
+
* chrome's own request helper throws ApiError, and a stale card must look the
|
|
26
|
+
* same whichever leg reported it. */
|
|
27
|
+
function isConflict(e: unknown): boolean {
|
|
28
|
+
return (e instanceof AgentApiError || e instanceof ApiError) && e.status === 409;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const itemId = createItemIds("sh");
|
|
32
|
+
|
|
33
|
+
export async function refreshAgentCredentials(store: ShotEditorStore): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
store.setAgentCredentials(await agent.credentials());
|
|
36
|
+
} catch (e) {
|
|
37
|
+
store.setAgentCredentials(null, message(e));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function saveAgentKey(store: ShotEditorStore, key: string): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
store.setAgentCredentials(await agent.setKey(key));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Keep the last good credential state; the error rides beside it.
|
|
46
|
+
store.setAgentCredentials(store.getState().agentCredentials, message(e));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function clearAgentKey(store: ShotEditorStore): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
store.setAgentCredentials(await agent.clearKey());
|
|
53
|
+
} catch (e) {
|
|
54
|
+
store.setAgentCredentials(store.getState().agentCredentials, message(e));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What "this" means in the shot chrome: which surface is asking, which shot is
|
|
60
|
+
* open, which scene and frame the author is looking at.
|
|
61
|
+
*
|
|
62
|
+
* `surface: "shot"` is load-bearing, not decoration — it is what makes the
|
|
63
|
+
* server append the shot addendum to the system prompt AND compose the shot
|
|
64
|
+
* tools for the session. Everything else is clamped to ids and names: the
|
|
65
|
+
* document itself is never pinned, because the agent pulls it (fresh, and only
|
|
66
|
+
* when it needs it) through `get_shot_doc`.
|
|
67
|
+
*/
|
|
68
|
+
export function buildShotAgentContext(s: ShotEditorState): unknown {
|
|
69
|
+
const shot: Record<string, unknown> = {};
|
|
70
|
+
if (s.name !== null) shot.docName = s.name;
|
|
71
|
+
if (s.doc !== null) shot.componentName = s.doc.name;
|
|
72
|
+
// The playhead and the open scene, unconditionally. Every layer in this lane
|
|
73
|
+
// renders AT a frame, so "which moment am I looking at" is always a real
|
|
74
|
+
// statement about what the author means by "this".
|
|
75
|
+
shot.frame = s.frame;
|
|
76
|
+
if (s.sceneId !== null) shot.sceneId = s.sceneId;
|
|
77
|
+
const primary = s.selection[0];
|
|
78
|
+
if (primary !== undefined) shot.selectedLayerId = primary;
|
|
79
|
+
return { surface: "shot", ...(Object.keys(shot).length > 0 ? { shot } : {}) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every key `buildShotAgentContext` may put under `context.shot`. The server
|
|
83
|
+
* rejects anything else, so this list and the server's are one contract. */
|
|
84
|
+
export const SHOT_CONTEXT_KEYS = ["docName", "componentName", "frame", "sceneId", "selectedLayerId"];
|
|
85
|
+
|
|
86
|
+
/** The store, seen as the shared reducer's three transcript verbs. */
|
|
87
|
+
function sinkFor(store: ShotEditorStore): TranscriptSink {
|
|
88
|
+
return {
|
|
89
|
+
append: (item) => store.appendAgentItem(item),
|
|
90
|
+
patchProposal: (id, patch) => store.patchAgentProposal(id, patch),
|
|
91
|
+
setCost: (costUsd) => store.setAgentCost(costUsd),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One turn: create a session, stream it to the end. Resolves when the
|
|
96
|
+
* transcript is complete (or the transport failed). */
|
|
97
|
+
export async function askAgent(store: ShotEditorStore, prompt: string): Promise<void> {
|
|
98
|
+
const s = store.getState();
|
|
99
|
+
if (s.agentRunning || prompt.trim() === "") return;
|
|
100
|
+
store.setAgentError(null);
|
|
101
|
+
store.appendAgentItem({ id: itemId(), kind: "user", text: prompt });
|
|
102
|
+
store.setAgentRunning(true);
|
|
103
|
+
try {
|
|
104
|
+
const { sessionId } = await agent.createSession({
|
|
105
|
+
prompt,
|
|
106
|
+
...(s.agentModel !== null ? { model: s.agentModel } : {}),
|
|
107
|
+
context: buildShotAgentContext(s),
|
|
108
|
+
});
|
|
109
|
+
store.setAgentSession(sessionId);
|
|
110
|
+
await agent.events(sessionId, (event: AgentEvent) => {
|
|
111
|
+
applyAgentEvent(sinkFor(store), sessionId, event, itemId);
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
store.setAgentError(message(e));
|
|
115
|
+
} finally {
|
|
116
|
+
store.setAgentRunning(false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function cancelAgent(store: ShotEditorStore): Promise<void> {
|
|
121
|
+
const sessionId = store.getState().agentSessionId;
|
|
122
|
+
if (sessionId === null || !store.getState().agentRunning) return;
|
|
123
|
+
try {
|
|
124
|
+
await agent.cancel(sessionId);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
store.setAgentError(message(e));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function proposalOf(store: ShotEditorStore, id: string): AgentProposalItem | undefined {
|
|
131
|
+
return store
|
|
132
|
+
.getState()
|
|
133
|
+
.agentItems.find((i): i is AgentProposalItem => i.kind === "proposal" && i.id === id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Apply one proposal.
|
|
138
|
+
*
|
|
139
|
+
* The apply POST is the authority — it validates baseRev, writes the doc,
|
|
140
|
+
* re-runs codegen and audits.
|
|
141
|
+
*
|
|
142
|
+
* ── AND THEN NOTHING, DELIBERATELY ─────────────────────────────────────
|
|
143
|
+
*
|
|
144
|
+
* There is no re-read here. The server's store emits `doc-changed` on the SSE
|
|
145
|
+
* bus for EVERY write, so this chrome adopts an agent apply through exactly the
|
|
146
|
+
* same path it adopts a second tab's save — `adoptExternalChange`, which also
|
|
147
|
+
* owns the "you have unsaved edits" refusal. A second adoption path here would
|
|
148
|
+
* be a second answer to "what happens when the document moves underneath me",
|
|
149
|
+
* and only one of them would have that refusal in it.
|
|
150
|
+
*
|
|
151
|
+
* The one thing the bus cannot do is OPEN a document this chrome does not have,
|
|
152
|
+
* which is what a create needs.
|
|
153
|
+
*/
|
|
154
|
+
export async function applyAgentProposal(store: ShotEditorStore, proposalId: string): Promise<void> {
|
|
155
|
+
const item = proposalOf(store, proposalId);
|
|
156
|
+
if (item === undefined || item.status !== "pending") return;
|
|
157
|
+
store.patchAgentProposal(proposalId, { status: "applying", error: null });
|
|
158
|
+
// THE AUTHOR'S OWN EDITS GO FIRST. Apply is a button in the same window as
|
|
159
|
+
// the canvas, so "drag a layer, then click Apply" is an ordinary gesture —
|
|
160
|
+
// and on loopback the apply round trip beats the autosave debounce. Flushing
|
|
161
|
+
// first means the author's edit either lands (and this proposal correctly
|
|
162
|
+
// goes stale, visibly) or fails visibly — never disappears.
|
|
163
|
+
try {
|
|
164
|
+
await store.flush();
|
|
165
|
+
} catch {
|
|
166
|
+
// flush records its own error; the apply still gets its chance, and a stale
|
|
167
|
+
// baseRev now shows up as a 409 rather than a silent loss.
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
// Against the MINTING session — later asks created newer sessions.
|
|
171
|
+
const result = (await agent.apply(item.sessionId, proposalId)) as { warning?: unknown } | null;
|
|
172
|
+
// "applied with warnings" is a real state: the document was written but
|
|
173
|
+
// codegen refused (a hand-edited generated file). A green card there is the
|
|
174
|
+
// one place a half-landed change reads as a landed one.
|
|
175
|
+
const warning = typeof result?.warning === "string" ? result.warning : null;
|
|
176
|
+
store.patchAgentProposal(proposalId, { status: "applied", warning });
|
|
177
|
+
} catch (e) {
|
|
178
|
+
store.patchAgentProposal(proposalId, {
|
|
179
|
+
status: isConflict(e) ? "stale" : "failed",
|
|
180
|
+
error: message(e),
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (item.proposalKind !== "shot") return;
|
|
186
|
+
if (store.getState().name !== item.docName && item.baseRev === null) {
|
|
187
|
+
// A CREATE names a shot this chrome does not have open — by definition, it
|
|
188
|
+
// did not exist. Open it: "ask for a video, get an editable one" is the
|
|
189
|
+
// whole promise of the lane, and leaving the author staring at the previous
|
|
190
|
+
// document with a green card would be a worse lie than any error.
|
|
191
|
+
try {
|
|
192
|
+
await store.openShot(item.docName);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
store.patchAgentProposal(proposalId, {
|
|
195
|
+
status: "applied",
|
|
196
|
+
warning: `the shot was written, but opening "${item.docName}" failed: ${message(e)}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// …but an EDIT of some other shot never yanks the author out of the document
|
|
201
|
+
// they are working in. It landed on disk; the switcher will show it.
|
|
202
|
+
void store.refreshShots();
|
|
203
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Who this browser surface IS, for the length of one page load.
|
|
2
|
+
//
|
|
3
|
+
// The shot twin of the canvas lane's clientId. It is ATTRIBUTION and never
|
|
4
|
+
// authorization: the server echoes it back on every change event so a surface
|
|
5
|
+
// can recognise its OWN write and skip adopting it. Without that, an autosave
|
|
6
|
+
// round-trips as a "someone else changed this document" push and the chrome
|
|
7
|
+
// fights its own save.
|
|
8
|
+
//
|
|
9
|
+
// In memory rather than sessionStorage on purpose: two tabs of the same
|
|
10
|
+
// document must be two clients, and sessionStorage is per-tab but survives a
|
|
11
|
+
// reload — which would make a reloaded tab claim the pre-reload tab's writes.
|
|
12
|
+
|
|
13
|
+
export const CLIENT_ID: string = crypto.randomUUID();
|
|
14
|
+
|
|
15
|
+
export const CLIENT_ID_HEADER = "x-studio-client-id";
|
|
16
|
+
|
|
17
|
+
/** The origin label this chrome's own writes carry. */
|
|
18
|
+
export const MY_ORIGIN = "shot-editor";
|
|
19
|
+
|
|
20
|
+
/** Is this change event our own echo? Client id wins when present; origin is
|
|
21
|
+
* the fallback for a server that predates it. */
|
|
22
|
+
export function isOwnEcho(payload: { origin?: string; clientId?: string }): boolean {
|
|
23
|
+
if (payload.clientId !== undefined) return payload.clientId === CLIENT_ID;
|
|
24
|
+
return payload.origin === MY_ORIGIN;
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { ShotEditorStore } from "./store.js";
|
|
4
|
+
import type { ShotEditorState } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const StoreContext = createContext<ShotEditorStore | null>(null);
|
|
7
|
+
|
|
8
|
+
export function StoreProvider(props: { store: ShotEditorStore; children: ReactNode }) {
|
|
9
|
+
return <StoreContext.Provider value={props.store}>{props.children}</StoreContext.Provider>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useStore(): ShotEditorStore {
|
|
13
|
+
const store = useContext(StoreContext);
|
|
14
|
+
if (!store) throw new Error("useStore outside <StoreProvider>");
|
|
15
|
+
return store;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to a slice of shot-editor state.
|
|
20
|
+
*
|
|
21
|
+
* The selector result is compared with Object.is per notification, so return
|
|
22
|
+
* state slices and primitives — never a freshly-built object or array, which
|
|
23
|
+
* would re-render on every unrelated notification and which
|
|
24
|
+
* `useSyncExternalStore` would reject outright as an unstable snapshot.
|
|
25
|
+
*/
|
|
26
|
+
export function useShotEditor<T>(selector: (s: ShotEditorState) => T): T {
|
|
27
|
+
const store = useStore();
|
|
28
|
+
return useSyncExternalStore(
|
|
29
|
+
store.subscribe,
|
|
30
|
+
() => selector(store.getState()),
|
|
31
|
+
() => selector(store.getState()),
|
|
32
|
+
);
|
|
33
|
+
}
|