@orangecatai/adgen-canvas 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/dev/{chunk-HAGQZMXY.js → chunk-OTDOAN2W.js} +3 -3
  2. package/dist/dev/{chunk-M6SLPQMN.js → chunk-WELJ6MTO.js} +2 -2
  3. package/dist/dev/data/{image-TSTRTL65.js → image-F5HBZTBB.js} +3 -3
  4. package/dist/dev/index.css +377 -0
  5. package/dist/dev/index.css.map +3 -3
  6. package/dist/dev/index.js +4506 -2591
  7. package/dist/dev/index.js.map +4 -4
  8. package/dist/dev/subset-shared.chunk.js +1 -1
  9. package/dist/dev/subset-worker.chunk.js +1 -1
  10. package/dist/prod/{chunk-FABLWJ5B.js → chunk-INDX7AB6.js} +1 -1
  11. package/dist/prod/{chunk-PIJUOUUJ.js → chunk-RI6ZX32X.js} +2 -2
  12. package/dist/prod/data/image-P4AE4WIT.js +1 -0
  13. package/dist/prod/index.css +1 -1
  14. package/dist/prod/index.js +69 -63
  15. package/dist/prod/subset-shared.chunk.js +1 -1
  16. package/dist/prod/subset-worker.chunk.js +1 -1
  17. package/dist/types/excalidraw/components/AIChatPanel.d.ts +25 -0
  18. package/dist/types/excalidraw/components/App.d.ts +9 -0
  19. package/dist/types/excalidraw/components/CampaignDropdown.d.ts +14 -0
  20. package/dist/types/excalidraw/components/FrameToolbar.d.ts +11 -2
  21. package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +14 -0
  22. package/dist/types/excalidraw/components/VideoChainPanel.d.ts +58 -0
  23. package/dist/types/excalidraw/components/VideoElement.d.ts +26 -1
  24. package/dist/types/excalidraw/components/ai-chat/canvasTools.d.ts +272 -3
  25. package/dist/types/excalidraw/components/frameToolbarVariant.d.ts +23 -0
  26. package/dist/types/excalidraw/components/video/videoActions.d.ts +37 -0
  27. package/dist/types/excalidraw/index.d.ts +1 -0
  28. package/dist/types/excalidraw/types.d.ts +61 -0
  29. package/dist/types/excalidraw/utils/brandContextUtils.d.ts +21 -0
  30. package/dist/types/excalidraw/utils/videoApi.d.ts +81 -0
  31. package/dist/types/excalidraw/utils/videoDurationStops.d.ts +14 -0
  32. package/dist/types/excalidraw/utils/videoStitch.d.ts +24 -0
  33. package/package.json +2 -1
  34. package/dist/prod/data/image-D6B2C7FH.js +0 -1
  35. /package/dist/dev/{chunk-HAGQZMXY.js.map → chunk-OTDOAN2W.js.map} +0 -0
  36. /package/dist/dev/{chunk-M6SLPQMN.js.map → chunk-WELJ6MTO.js.map} +0 -0
  37. /package/dist/dev/data/{image-TSTRTL65.js.map → image-F5HBZTBB.js.map} +0 -0
@@ -1 +1 @@
1
- import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-FABLWJ5B.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
1
+ import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-INDX7AB6.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
@@ -1 +1 @@
1
- import{a as r,c as t}from"./chunk-Z5NKEFVG.js";import"./chunk-FABLWJ5B.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
1
+ import{a as r,c as t}from"./chunk-Z5NKEFVG.js";import"./chunk-INDX7AB6.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
@@ -43,6 +43,21 @@ export type AIChatPanelProps = {
43
43
  * `chatUrl`. Keeps the provider key out of the browser.
44
44
  */
45
45
  imageGenUrl?: string;
46
+ /**
47
+ * Host proxy URL for the agent's generate_video / generate_video_chain
48
+ * tools. Same contract as `imageGenUrl` — the host injects the provider key,
49
+ * gates credits, and returns a `{ jobId }` to poll. Without this pair the
50
+ * video tools report themselves as unconfigured instead of failing mid-run.
51
+ */
52
+ videoGenUrl?: string;
53
+ /** Host status endpoint for a submitted video job; the jobId is appended as a path segment. */
54
+ videoGenStatusUrl?: string;
55
+ /** Host shot-splitter URL. Required for the agent's generate_video_chain tool. */
56
+ videoPlanUrl?: string;
57
+ /** Host upload URL for a stitched long-form video. Falls back to an in-memory object URL. */
58
+ videoUploadUrl?: string;
59
+ /** Host proxy that re-fetches presigned S3 video URLs server-side (browser CORS workaround). */
60
+ videoDownloadProxyUrl?: string;
46
61
  excalidrawAPI?: ExcalidrawImperativeAPI;
47
62
  initialMessages?: ChatMessage[];
48
63
  initialSessionId?: string;
@@ -76,6 +91,16 @@ export type AIChatPanelProps = {
76
91
  error?: string;
77
92
  }>;
78
93
  onAfterImageGen?: () => void;
94
+ /**
95
+ * Credit gate for a video generated by the chat agent. Called once per
96
+ * generated video — and once per segment of a long-form chain, since each
97
+ * segment is separately billed.
98
+ */
99
+ onBeforeVideoGen?: () => Promise<{
100
+ allowed: boolean;
101
+ error?: string;
102
+ }>;
103
+ onAfterVideoGen?: () => void;
79
104
  /**
80
105
  * OpenRouter model tag for the chat system (both plain chat and agent loop).
81
106
  * @default "minimax/minimax-m3"
@@ -259,6 +259,15 @@ declare class App extends React.Component<AppProps, AppState> {
259
259
  private updateEmbedValidationStatus;
260
260
  private updateEmbeddables;
261
261
  private renderEmbeddables;
262
+ /**
263
+ * True when this video's parent frame already offers Download and Campaign
264
+ * through the video variant of the frame toolbar. The in-video toolbar is
265
+ * suppressed in that case so the two actions never appear twice at once.
266
+ *
267
+ * Videos with no such frame — pasted, dragged in, agent-placed — keep their
268
+ * own toolbar as the only way to reach those actions.
269
+ */
270
+ private frameToolbarOwnsVideo;
262
271
  private renderVideos;
263
272
  private getFrameNameDOMId;
264
273
  frameNameBoundsCache: FrameNameBoundsCache;
@@ -0,0 +1,14 @@
1
+ import "./CampaignDropdown.scss";
2
+ import type { CampaignSummary } from "../types";
3
+ export declare const CampaignDropdown: ({ onFetchCampaigns, onCreateCampaign, onAdd, disabled, disabledReason, }: {
4
+ onFetchCampaigns?: () => Promise<{
5
+ campaigns: CampaignSummary[];
6
+ }>;
7
+ onCreateCampaign?: (name: string) => Promise<CampaignSummary>;
8
+ onAdd?: (campaignId: string) => Promise<{
9
+ ok: boolean;
10
+ error?: string;
11
+ }>;
12
+ disabled?: boolean;
13
+ disabledReason?: string;
14
+ }) => import("react/jsx-runtime").JSX.Element | null;
@@ -1,7 +1,16 @@
1
1
  import "./FrameToolbar.scss";
2
- import type { ExcalidrawFrameLikeElement, NonDeletedExcalidrawElement } from "@orangecatai/element/types";
2
+ import type { FrameToolbarVariant } from "./frameToolbarVariant";
3
+ import type { ExcalidrawFrameLikeElement, ExcalidrawVideoElement, NonDeletedExcalidrawElement } from "@orangecatai/element/types";
3
4
  import type { AppClassProperties } from "../types";
4
- export declare const FrameToolbar: ({ element, app, }: {
5
+ export declare const FrameToolbar: ({ element, app, variant, videoElement, }: {
5
6
  element: NonDeletedExcalidrawElement & ExcalidrawFrameLikeElement;
6
7
  app: AppClassProperties;
8
+ /**
9
+ * "frame" is the full toolbar. "video" drops Resize and Template — both are
10
+ * image-only — and swaps the export and campaign actions for video ones.
11
+ * See `getFrameToolbarVariant`, which resolves this from the frame.
12
+ */
13
+ variant?: FrameToolbarVariant;
14
+ /** The frame's ready video child. Required when variant is "video". */
15
+ videoElement?: ExcalidrawVideoElement | null;
7
16
  }) => import("react/jsx-runtime").JSX.Element | null;
@@ -65,6 +65,20 @@ export declare const MODEL_CONFIG: {
65
65
  readonly supportedResolutions: readonly ["1K", "2K", "4K"];
66
66
  readonly supportedRatios: readonly ["1:1", "21:9", "16:9", "3:2", "4:3", "5:4", "4:5", "3:4", "2:3", "9:16"];
67
67
  };
68
+ readonly "gpt image 1 mini": {
69
+ readonly modelId: "openai/gpt-image-1-mini";
70
+ readonly textOutput: false;
71
+ readonly supportsImageSize: false;
72
+ readonly supportedResolutions: readonly ["1K"];
73
+ readonly supportedRatios: readonly ["1:1", "2:3", "3:2"];
74
+ };
75
+ readonly "gpt image 2": {
76
+ readonly modelId: "openai/gpt-image-2";
77
+ readonly textOutput: false;
78
+ readonly supportsImageSize: false;
79
+ readonly supportedResolutions: readonly ["1K", "2K"];
80
+ readonly supportedRatios: readonly ["21:9", "16:9", "4:3", "3:2", "1:1", "9:16", "3:4", "2:3", "5:4", "4:5"];
81
+ };
68
82
  };
69
83
  export type ModelName = keyof typeof MODEL_CONFIG;
70
84
  export declare const MODEL_NAMES: ModelName[];
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { type VideoJobParams, type ChainSegmentBeat } from "../utils/videoApi";
3
+ import "./VideoChainPanel.scss";
4
+ import type { ExcalidrawFrameLikeElement, NonDeletedExcalidrawElement } from "@orangecatai/element/types";
5
+ import type { AppClassProperties } from "../types";
6
+ export type ChainSegmentStatus = "queued" | "generating" | "done" | "failed";
7
+ export type { ChainSegmentBeat };
8
+ export interface ChainSegmentState {
9
+ index: number;
10
+ prompt: string;
11
+ status: ChainSegmentStatus;
12
+ beat?: ChainSegmentBeat;
13
+ jobId?: string;
14
+ videoS3Key?: string;
15
+ error?: string;
16
+ }
17
+ export type ChainPhase = "idle" | "planning" | "generating" | "stitching" | "done" | "error";
18
+ export interface VideoChainPanelHandle {
19
+ /**
20
+ * Runs the whole plan → generate → stitch pipeline as one motion. If the
21
+ * last run left a failed segment, resumes from it instead of re-planning.
22
+ * Otherwise (no prior chain, or a fully-completed one) discards any
23
+ * previous plan and starts fresh — a changed concept/duration should not
24
+ * silently resume stale segments.
25
+ */
26
+ start: () => void;
27
+ stop: () => void;
28
+ }
29
+ export interface VideoChainPanelProps {
30
+ element: NonDeletedExcalidrawElement & ExcalidrawFrameLikeElement;
31
+ app: AppClassProperties;
32
+ /** The overall ad concept — sent to the shot-splitter as-is. */
33
+ concept: string;
34
+ /** How many segments the currently-selected duration implies (>1, or this panel wouldn't be mounted). */
35
+ segmentCount: number;
36
+ /** Every segment's duration — always the model's native single-generation max. */
37
+ segmentDurationSeconds: number;
38
+ /** Every video-gen field held fixed across the whole chain (model, ratio, resolution, audio toggle, seed, ...). */
39
+ baseParams: Omit<VideoJobParams, "prompt" | "firstFrameDataUrl">;
40
+ /** Applied to each segment's shot-writer prompt before submission (e.g. brand tone wrapping). Defaults to identity. */
41
+ wrapPrompt?: (segmentPrompt: string) => string;
42
+ /** First frame for segment 0, e.g. a user-uploaded starting image. */
43
+ initialFirstFrameDataUrl?: string;
44
+ videoGenUrl: string;
45
+ videoGenStatusUrl: string;
46
+ videoPlanUrl?: string;
47
+ videoUploadUrl?: string;
48
+ /** Fetches a presigned S3 video URL server-side to work around browser CORS — see resolveFetchUrl. */
49
+ videoDownloadProxyUrl?: string;
50
+ onBeforeVideoGen?: () => Promise<{
51
+ allowed: boolean;
52
+ error?: string;
53
+ }>;
54
+ onAfterVideoGen?: () => void;
55
+ /** Lets the parent reflect chain progress in its own Generate/Stop button and shimmer overlay. */
56
+ onPhaseChange?: (phase: ChainPhase) => void;
57
+ }
58
+ export declare const VideoChainPanel: React.ForwardRefExoticComponent<VideoChainPanelProps & React.RefAttributes<VideoChainPanelHandle>>;
@@ -1,9 +1,34 @@
1
1
  import type { ExcalidrawVideoElement } from "@orangecatai/element/types";
2
+ import type { CampaignSummary } from "../types";
2
3
  import "./VideoElement.scss";
3
4
  interface VideoElementProps {
4
5
  element: ExcalidrawVideoElement;
5
6
  isActive: boolean;
6
7
  videoDownloadProxyUrl?: string;
8
+ /**
9
+ * Whether to render the in-video Download / Campaign toolbar.
10
+ *
11
+ * Defaults to true. `App.tsx` passes false when the video sits inside a
12
+ * video-generator frame whose frame toolbar already offers both actions, so
13
+ * the two surfaces never appear at once. Videos with no such frame (pasted,
14
+ * dragged in, agent-placed) keep this toolbar as their only access.
15
+ */
16
+ showToolbar?: boolean;
17
+ onFetchCampaigns?: () => Promise<{
18
+ campaigns: CampaignSummary[];
19
+ }>;
20
+ onCreateCampaign?: (name: string) => Promise<CampaignSummary>;
21
+ onAddToCampaign?: (payload: {
22
+ kind: "video";
23
+ campaignId: string;
24
+ videoUrl: string;
25
+ mimeType: string;
26
+ width: number;
27
+ height: number;
28
+ }) => Promise<{
29
+ ok: boolean;
30
+ error?: string;
31
+ }>;
7
32
  }
8
- export declare const VideoElement: ({ element, videoDownloadProxyUrl, }: VideoElementProps) => import("react/jsx-runtime").JSX.Element;
33
+ export declare const VideoElement: ({ element, videoDownloadProxyUrl, showToolbar, onFetchCampaigns, onCreateCampaign, onAddToCampaign, }: VideoElementProps) => import("react/jsx-runtime").JSX.Element;
9
34
  export {};
@@ -1,5 +1,85 @@
1
1
  import type { ExcalidrawElement } from "@orangecatai/element/types";
2
- import type { ExcalidrawImperativeAPI } from "../../types";
2
+ import type { ExcalidrawImperativeAPI, BrandContext } from "../../types";
3
+ /** The only video model the chat agent can drive. */
4
+ export declare const CHAT_VIDEO_MODEL = "google/veo-3.1-fast";
5
+ export declare const VIDEO_TOOLS: ({
6
+ type: "function";
7
+ function: {
8
+ name: string;
9
+ description: string;
10
+ parameters: {
11
+ type: string;
12
+ properties: {
13
+ first_frame_element_id: {
14
+ readonly type: "string";
15
+ readonly description: "ID of an existing IMAGE element already on the canvas to use as the video's opening frame (image-to-video). Use this to animate an image the user can see on the canvas.";
16
+ };
17
+ last_frame_element_id: {
18
+ readonly type: "string";
19
+ readonly description: "ID of an existing IMAGE element on the canvas to use as the video's closing frame. Optional.";
20
+ };
21
+ first_frame_attachment_index: {
22
+ readonly type: "number";
23
+ readonly description: "0-based index of an image the user ATTACHED to the chat, to use as the opening frame. Use this instead of first_frame_element_id when the source image was uploaded in chat rather than placed on the canvas.";
24
+ };
25
+ last_frame_attachment_index: {
26
+ readonly type: "number";
27
+ readonly description: "0-based index of an attached chat image to use as the closing frame. Optional.";
28
+ };
29
+ frameId: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ prompt: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ duration: {
38
+ type: string;
39
+ enum: (4 | 6 | 8)[];
40
+ description: string;
41
+ };
42
+ narrative_prompt?: undefined;
43
+ total_duration?: undefined;
44
+ };
45
+ required: string[];
46
+ additionalProperties: boolean;
47
+ };
48
+ };
49
+ } | {
50
+ type: "function";
51
+ function: {
52
+ name: string;
53
+ description: string;
54
+ parameters: {
55
+ type: string;
56
+ properties: {
57
+ frameId: {
58
+ type: string;
59
+ description: string;
60
+ };
61
+ narrative_prompt: {
62
+ type: string;
63
+ description: string;
64
+ };
65
+ total_duration: {
66
+ type: string;
67
+ description: string;
68
+ };
69
+ first_frame_element_id: {
70
+ readonly type: "string";
71
+ readonly description: "ID of an existing IMAGE element already on the canvas to use as the video's opening frame (image-to-video). Use this to animate an image the user can see on the canvas.";
72
+ };
73
+ first_frame_attachment_index: {
74
+ readonly type: "number";
75
+ readonly description: "0-based index of an image the user ATTACHED to the chat, to use as the opening frame. Use this instead of first_frame_element_id when the source image was uploaded in chat rather than placed on the canvas.";
76
+ };
77
+ };
78
+ required: string[];
79
+ additionalProperties: boolean;
80
+ };
81
+ };
82
+ })[];
3
83
  export declare const CANVAS_TOOLS: readonly [{
4
84
  readonly type: "function";
5
85
  readonly function: {
@@ -167,7 +247,84 @@ export declare const CANVAS_TOOLS: readonly [{
167
247
  readonly additionalProperties: false;
168
248
  };
169
249
  };
170
- }, {
250
+ }, ...({
251
+ type: "function";
252
+ function: {
253
+ name: string;
254
+ description: string;
255
+ parameters: {
256
+ type: string;
257
+ properties: {
258
+ first_frame_element_id: {
259
+ readonly type: "string";
260
+ readonly description: "ID of an existing IMAGE element already on the canvas to use as the video's opening frame (image-to-video). Use this to animate an image the user can see on the canvas.";
261
+ };
262
+ last_frame_element_id: {
263
+ readonly type: "string";
264
+ readonly description: "ID of an existing IMAGE element on the canvas to use as the video's closing frame. Optional.";
265
+ };
266
+ first_frame_attachment_index: {
267
+ readonly type: "number";
268
+ readonly description: "0-based index of an image the user ATTACHED to the chat, to use as the opening frame. Use this instead of first_frame_element_id when the source image was uploaded in chat rather than placed on the canvas.";
269
+ };
270
+ last_frame_attachment_index: {
271
+ readonly type: "number";
272
+ readonly description: "0-based index of an attached chat image to use as the closing frame. Optional.";
273
+ };
274
+ frameId: {
275
+ type: string;
276
+ description: string;
277
+ };
278
+ prompt: {
279
+ type: string;
280
+ description: string;
281
+ };
282
+ duration: {
283
+ type: string;
284
+ enum: (4 | 6 | 8)[];
285
+ description: string;
286
+ };
287
+ narrative_prompt?: undefined;
288
+ total_duration?: undefined;
289
+ };
290
+ required: string[];
291
+ additionalProperties: boolean;
292
+ };
293
+ };
294
+ } | {
295
+ type: "function";
296
+ function: {
297
+ name: string;
298
+ description: string;
299
+ parameters: {
300
+ type: string;
301
+ properties: {
302
+ frameId: {
303
+ type: string;
304
+ description: string;
305
+ };
306
+ narrative_prompt: {
307
+ type: string;
308
+ description: string;
309
+ };
310
+ total_duration: {
311
+ type: string;
312
+ description: string;
313
+ };
314
+ first_frame_element_id: {
315
+ readonly type: "string";
316
+ readonly description: "ID of an existing IMAGE element already on the canvas to use as the video's opening frame (image-to-video). Use this to animate an image the user can see on the canvas.";
317
+ };
318
+ first_frame_attachment_index: {
319
+ readonly type: "number";
320
+ readonly description: "0-based index of an image the user ATTACHED to the chat, to use as the opening frame. Use this instead of first_frame_element_id when the source image was uploaded in chat rather than placed on the canvas.";
321
+ };
322
+ };
323
+ required: string[];
324
+ additionalProperties: boolean;
325
+ };
326
+ };
327
+ })[], {
171
328
  readonly type: "function";
172
329
  readonly function: {
173
330
  readonly name: "update_element";
@@ -268,7 +425,84 @@ export declare const CANVAS_TOOLS: readonly [{
268
425
  * agent knows it can use those numbers in `add_text` calls.
269
426
  */
270
427
  export declare function getCanvasTools(extraFontIds?: number[]): typeof CANVAS_TOOLS;
271
- export declare const BANNER_TOOLS: readonly [{
428
+ export declare const BANNER_TOOLS: readonly [...({
429
+ type: "function";
430
+ function: {
431
+ name: string;
432
+ description: string;
433
+ parameters: {
434
+ type: string;
435
+ properties: {
436
+ first_frame_element_id: {
437
+ readonly type: "string";
438
+ readonly description: "ID of an existing IMAGE element already on the canvas to use as the video's opening frame (image-to-video). Use this to animate an image the user can see on the canvas.";
439
+ };
440
+ last_frame_element_id: {
441
+ readonly type: "string";
442
+ readonly description: "ID of an existing IMAGE element on the canvas to use as the video's closing frame. Optional.";
443
+ };
444
+ first_frame_attachment_index: {
445
+ readonly type: "number";
446
+ readonly description: "0-based index of an image the user ATTACHED to the chat, to use as the opening frame. Use this instead of first_frame_element_id when the source image was uploaded in chat rather than placed on the canvas.";
447
+ };
448
+ last_frame_attachment_index: {
449
+ readonly type: "number";
450
+ readonly description: "0-based index of an attached chat image to use as the closing frame. Optional.";
451
+ };
452
+ frameId: {
453
+ type: string;
454
+ description: string;
455
+ };
456
+ prompt: {
457
+ type: string;
458
+ description: string;
459
+ };
460
+ duration: {
461
+ type: string;
462
+ enum: (4 | 6 | 8)[];
463
+ description: string;
464
+ };
465
+ narrative_prompt?: undefined;
466
+ total_duration?: undefined;
467
+ };
468
+ required: string[];
469
+ additionalProperties: boolean;
470
+ };
471
+ };
472
+ } | {
473
+ type: "function";
474
+ function: {
475
+ name: string;
476
+ description: string;
477
+ parameters: {
478
+ type: string;
479
+ properties: {
480
+ frameId: {
481
+ type: string;
482
+ description: string;
483
+ };
484
+ narrative_prompt: {
485
+ type: string;
486
+ description: string;
487
+ };
488
+ total_duration: {
489
+ type: string;
490
+ description: string;
491
+ };
492
+ first_frame_element_id: {
493
+ readonly type: "string";
494
+ readonly description: "ID of an existing IMAGE element already on the canvas to use as the video's opening frame (image-to-video). Use this to animate an image the user can see on the canvas.";
495
+ };
496
+ first_frame_attachment_index: {
497
+ readonly type: "number";
498
+ readonly description: "0-based index of an image the user ATTACHED to the chat, to use as the opening frame. Use this instead of first_frame_element_id when the source image was uploaded in chat rather than placed on the canvas.";
499
+ };
500
+ };
501
+ required: string[];
502
+ additionalProperties: boolean;
503
+ };
504
+ };
505
+ })[], {
272
506
  readonly type: "function";
273
507
  readonly function: {
274
508
  readonly name: "create_frame";
@@ -696,6 +930,41 @@ export type ToolExecutionContext = {
696
930
  name: string;
697
931
  dataUrl?: string;
698
932
  }>;
933
+ /**
934
+ * Full brand identity. The agent already receives this as a system message,
935
+ * but the video tools also append it to the generation prompt deterministically
936
+ * via buildVideoBrandPrompt — the same wrapper VideoGeneratorPanel uses — so
937
+ * chat-generated footage is as on-brand as panel-generated footage instead of
938
+ * depending on whatever the model chose to restate.
939
+ */
940
+ brandContext?: BrandContext;
941
+ /** Host proxy URL for submitting a video-gen job. Required for generate_video. */
942
+ videoGenUrl?: string;
943
+ /** Host proxy URL polled for job status (the jobId is appended as a path segment). */
944
+ videoGenStatusUrl?: string;
945
+ /** Host shot-splitter URL. Required for generate_video_chain. */
946
+ videoPlanUrl?: string;
947
+ /** Host upload URL for the stitched chain output. Falls back to an object URL. */
948
+ videoUploadUrl?: string;
949
+ /** Host proxy that re-fetches presigned S3 video URLs server-side (CORS workaround). */
950
+ videoDownloadProxyUrl?: string;
951
+ /**
952
+ * Credit gate for a single video generation. Called once per generated
953
+ * video — including once per segment of a chain, since each segment is its
954
+ * own billed generation (matching the standalone panel's contract).
955
+ */
956
+ onBeforeVideoGen?: () => Promise<{
957
+ allowed: boolean;
958
+ error?: string;
959
+ }>;
960
+ /** Fired after each successful video generation (host persists / refreshes). */
961
+ onAfterVideoGen?: () => void;
962
+ /**
963
+ * Pushes an interim status line to the chat while a long-running tool is
964
+ * mid-flight. Video generation blocks for minutes, so it repeatedly calls
965
+ * this to keep the panel's status indicator moving instead of frozen.
966
+ */
967
+ onStatus?: (message: string) => void;
699
968
  onSearchBrandAssets?: (query: string, type?: string) => Promise<Array<{
700
969
  id: string;
701
970
  name: string;
@@ -0,0 +1,23 @@
1
+ import type { ElementsMapOrArray, ExcalidrawFrameLikeElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
2
+ export type FrameToolbarVariant = "frame" | "video";
3
+ export type FrameToolbarVariantResult = {
4
+ variant: FrameToolbarVariant;
5
+ /** The frame's ready video child. Only ever set for the "video" variant. */
6
+ videoElement: ExcalidrawVideoElement | null;
7
+ };
8
+ /**
9
+ * Decides which frame toolbar — if any — a selected frame should show.
10
+ *
11
+ * - `"frame"` plain frames (agent-created, manual): the full toolbar.
12
+ * - `"video"` a video-generator frame holding a ready video: sizing, download,
13
+ * campaign. No Resize or Template — both are image-only.
14
+ * - `null` image-generator frames, and video-generator frames that are
15
+ * empty, still generating, or holding a video the host could not
16
+ * act on.
17
+ *
18
+ * A video counts as ready only when it is saved, has a URL, and that URL is not
19
+ * a blob: URL. That last condition is the same guard the in-video toolbar
20
+ * applies: a blob URL cannot be fetched server-side, so offering Download or
21
+ * Campaign on one would present an action that is guaranteed to fail.
22
+ */
23
+ export declare const getFrameToolbarVariant: (frame: ExcalidrawFrameLikeElement, elements: ElementsMapOrArray) => FrameToolbarVariantResult | null;
@@ -0,0 +1,37 @@
1
+ import type { ExcalidrawVideoElement } from "@orangecatai/element/types";
2
+ import type { AddToCampaignPayload } from "../../types";
3
+ /**
4
+ * Video actions shared by the two surfaces that offer them: the in-video
5
+ * toolbar (`VideoElement`) and the video variant of `FrameToolbar`.
6
+ *
7
+ * These live here rather than in either component because the blob-URL rule
8
+ * below is easy to get wrong and impossible to notice when it drifts — a
9
+ * blob URL sent to the host surfaces as a server-side failure, not a visible
10
+ * client error.
11
+ */
12
+ /**
13
+ * blob: URLs are local-preview-only. They are assigned while an upload is still
14
+ * in flight and are resolvable only in the tab that created them, so they must
15
+ * never be handed to the server — neither the download proxy nor the campaign
16
+ * copy step can fetch one.
17
+ */
18
+ export declare const isBlobVideoUrl: (videoUrl: string | null) => boolean;
19
+ /**
20
+ * Resolves the URL to use for `<video src>`. Routing through the proxy avoids
21
+ * presigned-URL expiry on long-lived canvases.
22
+ */
23
+ export declare const resolveVideoPlaybackUrl: (videoUrl: string | null, videoDownloadProxyUrl?: string) => string | null;
24
+ /** Triggers a browser download of the video file itself — never a canvas export. */
25
+ export declare const downloadVideo: ({ videoUrl, mimeType, videoDownloadProxyUrl, }: {
26
+ videoUrl: string | null;
27
+ mimeType: ExcalidrawVideoElement["mimeType"];
28
+ videoDownloadProxyUrl?: string;
29
+ }) => void;
30
+ /**
31
+ * Builds the host payload for adding a video to a campaign.
32
+ * Returns null when the video is not in a state the host can accept, so callers
33
+ * can surface "not ready yet" instead of issuing a request that would fail.
34
+ */
35
+ export declare const buildVideoCampaignPayload: (element: ExcalidrawVideoElement, campaignId: string) => Extract<AddToCampaignPayload, {
36
+ kind: "video";
37
+ }> | null;
@@ -57,4 +57,5 @@ export type { AppState, BinaryFiles, BinaryFileData, ExcalidrawProps, Excalidraw
57
57
  export type { ExcalidrawElement, NonDeletedExcalidrawElement, NonDeleted, OrderedExcalidrawElement, ExcalidrawElementType, ExcalidrawVideoElement, } from "@orangecatai/element/types";
58
58
  export type { ImageEditToolbarCallbacks } from "./components/ImageEditToolbar";
59
59
  export type { CustomFontSpec, BrandContext } from "./types";
60
+ export type { CampaignSummary, AddToCampaignPayload } from "./types";
60
61
  export type { ReviewerFeedback, ReviewIssue, } from "./components/ai-chat/reviewerAgent";
@@ -521,6 +521,38 @@ export type BrandContext = {
521
521
  */
522
522
  headlineFontUrl?: string;
523
523
  };
524
+ /** A campaign the user can save a frame/video snapshot into. */
525
+ export type CampaignSummary = {
526
+ id: string;
527
+ name: string;
528
+ };
529
+ /**
530
+ * Payload passed to `onAddToCampaign`. `image` carries a rendered PNG blob
531
+ * of the exported frame; `video` carries the video element's existing URL
532
+ * (the host copies the file server-side — it never round-trips the browser).
533
+ */
534
+ export type AddToCampaignPayload = {
535
+ kind: "image";
536
+ campaignId: string;
537
+ blob: Blob;
538
+ mimeType: string;
539
+ width: number;
540
+ height: number;
541
+ } | {
542
+ kind: "video";
543
+ campaignId: string;
544
+ videoUrl: string;
545
+ mimeType: string;
546
+ width: number;
547
+ height: number;
548
+ /**
549
+ * The video element's existing first-frame JPEG data URL, if it has one
550
+ * (`ExcalidrawVideoElement.thumbnailDataUrl`). The host decodes and
551
+ * stores this so the campaign preview never has to fall back to a
552
+ * generic placeholder for canvas-added videos.
553
+ */
554
+ thumbnailDataUrl?: string;
555
+ };
524
556
  export interface ExcalidrawProps {
525
557
  onChange?: (elements: readonly OrderedExcalidrawElement[], appState: AppState, files: BinaryFiles) => void;
526
558
  onIncrement?: (event: DurableIncrement | EphemeralIncrement) => void;
@@ -675,6 +707,35 @@ export interface ExcalidrawProps {
675
707
  onAfterVideoGen?: () => void;
676
708
  /** Host proxy URL for video downloads — works around S3 CORS / presigned-URL expiry. */
677
709
  videoDownloadProxyUrl?: string;
710
+ /**
711
+ * Host proxy URL for the long-form video shot-splitting LLM call (POST).
712
+ * Body: `{ concept, segmentCount, segmentDurationSeconds }`. Response:
713
+ * `{ styleAnchor, segments }`. Without this prop, the "Long-form" chaining
714
+ * mode is not offered in the video generator panel.
715
+ */
716
+ videoPlanUrl?: string;
717
+ /**
718
+ * Fetches the user's campaigns for the "Add to campaign" dropdown on the
719
+ * frame toolbar and the video element toolbar. Without this prop, the
720
+ * campaign button is not rendered.
721
+ */
722
+ onFetchCampaigns?: () => Promise<{
723
+ campaigns: CampaignSummary[];
724
+ }>;
725
+ /**
726
+ * Creates a new campaign from the inline "New campaign" row in the
727
+ * dropdown. Returns the created campaign so the SDK can auto-select it.
728
+ */
729
+ onCreateCampaign?: (name: string) => Promise<CampaignSummary>;
730
+ /**
731
+ * Saves a snapshot of the selected frame (exported to PNG) or video
732
+ * element (by its existing URL) into the chosen campaign. This is a
733
+ * free action (no credit gate) since no AI call is involved.
734
+ */
735
+ onAddToCampaign?: (payload: AddToCampaignPayload) => Promise<{
736
+ ok: boolean;
737
+ error?: string;
738
+ }>;
678
739
  /**
679
740
  * Called before a background removal is run (deduct credits, check quota).
680
741
  * Return `{ allowed: false, error: "..." }` to block the operation.