@orangecatai/adgen-canvas 0.0.30 → 0.0.32

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-X6KPARLJ.js → chunk-OWDDSUUQ.js} +3 -3
  2. package/dist/dev/{chunk-2WAJTVZS.js → chunk-VL47Q2RH.js} +2 -2
  3. package/dist/dev/data/{image-HGVO7LLH.js → image-5Y3GORNT.js} +3 -3
  4. package/dist/dev/index.css +118 -1
  5. package/dist/dev/index.css.map +3 -3
  6. package/dist/dev/index.js +3633 -2579
  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-SZMIDXG6.js → chunk-452N23ZE.js} +1 -1
  11. package/dist/prod/{chunk-GTYNSLQE.js → chunk-AQSA3HNR.js} +2 -2
  12. package/dist/prod/data/image-5D6ZUEOG.js +1 -0
  13. package/dist/prod/index.css +1 -1
  14. package/dist/prod/index.js +112 -94
  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 +13 -3
  18. package/dist/types/excalidraw/components/App.d.ts +8 -0
  19. package/dist/types/excalidraw/components/ChipSourceMenu.d.ts +14 -0
  20. package/dist/types/excalidraw/components/ElementPickHighlight.d.ts +8 -0
  21. package/dist/types/excalidraw/components/ai-chat/agentLoop.d.ts +18 -1
  22. package/dist/types/excalidraw/components/ai-chat/canvasTools.d.ts +113 -9
  23. package/dist/types/excalidraw/components/ai-chat/reviewerAgent.d.ts +7 -1
  24. package/dist/types/excalidraw/components/auto-resize/autoResizeEngine.d.ts +6 -0
  25. package/dist/types/excalidraw/components/elementPicker.d.ts +33 -0
  26. package/dist/types/excalidraw/components/frameToolbarVariant.d.ts +14 -1
  27. package/dist/types/excalidraw/components/useCanvasFramePicker.d.ts +28 -0
  28. package/dist/types/excalidraw/types.d.ts +3 -2
  29. package/dist/types/excalidraw/utils/brandContextUtils.d.ts +16 -3
  30. package/dist/types/excalidraw/utils/imageApi.d.ts +41 -1
  31. package/dist/types/excalidraw/utils/videoFrameSource.d.ts +17 -0
  32. package/dist/types/excalidraw/utils/videoStitch.d.ts +7 -7
  33. package/package.json +1 -1
  34. package/dist/prod/data/image-B2RV64CG.js +0 -1
  35. /package/dist/dev/{chunk-X6KPARLJ.js.map → chunk-OWDDSUUQ.js.map} +0 -0
  36. /package/dist/dev/{chunk-2WAJTVZS.js.map → chunk-VL47Q2RH.js.map} +0 -0
  37. /package/dist/dev/data/{image-HGVO7LLH.js.map → image-5Y3GORNT.js.map} +0 -0
@@ -1 +1 @@
1
- import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-SZMIDXG6.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-452N23ZE.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-SZMIDXG6.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-452N23ZE.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,5 +1,5 @@
1
1
  import React from "react";
2
- import { type ToolAction } from "./ai-chat/agentLoop";
2
+ import { type ToolAction, type FrameGenerationMeta } from "./ai-chat/agentLoop";
3
3
  import "./AIChatPanel.scss";
4
4
  import type { ReviewerFeedback } from "./ai-chat/reviewerAgent";
5
5
  import type { ExcalidrawImperativeAPI } from "../types";
@@ -18,6 +18,16 @@ export type ChatMessage = {
18
18
  }>;
19
19
  /** Reviewer feedback attached to an assistant message after HTML ad generation */
20
20
  reviewFeedback?: ReviewerFeedback;
21
+ /**
22
+ * Set on an assistant message that generated/regenerated an image for a
23
+ * frame. Persisted so that on reload / session switch,
24
+ * resetAndSeedFrameGenerationMemory can rebuild canvasTools' in-memory
25
+ * frame-continuity state (original prompt anchor, reference-image reuse,
26
+ * "frames generated in this conversation" list) from chat history — none
27
+ * of that survives a reload on its own since it lives in a JS module, not
28
+ * the DB. See FrameGenerationMeta in agentLoop.ts.
29
+ */
30
+ generatedFrame?: FrameGenerationMeta;
21
31
  };
22
32
  export type ChatSession = {
23
33
  id: string;
@@ -121,8 +131,8 @@ export type AIChatPanelProps = {
121
131
  transcribeUrl?: string;
122
132
  /**
123
133
  * OpenRouter model ID used by the agent's generate_image tool.
124
- * @default "google/gemini-3.1-flash-image-preview"
125
- * @example "google/gemini-2.5-flash-image"
134
+ * @default "openai/gpt-image-2"
135
+ * @example "google/gemini-3.1-flash-image-preview"
126
136
  */
127
137
  agentImageModel?: string;
128
138
  /**
@@ -11,6 +11,7 @@ import { LaserTrails } from "../laser-trails";
11
11
  import { isOverScrollBars } from "../scene/scrollbars";
12
12
  import { LassoTrail } from "../lasso";
13
13
  import { EraserTrail } from "../eraser";
14
+ import { type ElementPickerState } from "./elementPicker";
14
15
  import type { ExcalidrawElement, NonDeleted, ExcalidrawImageElement, NonDeletedExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawIframeElement, ExcalidrawEmbeddableElement, Ordered, SceneElementsMap } from "@orangecatai/element/types";
15
16
  import type { Mutable } from "@orangecatai/common/utility-types";
16
17
  import type { ExportedElements } from "../data";
@@ -239,6 +240,13 @@ declare class App extends React.Component<AppProps, AppState> {
239
240
  missingPointerEventCleanupEmitter: Emitter<[event: PointerEvent | null]>;
240
241
  onRemoveEventListenersEmitter: Emitter<[]>;
241
242
  constructor(props: AppProps);
243
+ /**
244
+ * Arms (or disarms) canvas element picking. While armed, the next canvas
245
+ * pointer-down resolves the element under the cursor instead of changing
246
+ * selection — see `handleCanvasPointerDown` and `elementPicker.ts` for why
247
+ * selection must not move.
248
+ */
249
+ setElementPicker: (picker: ElementPickerState | null) => void;
242
250
  updateEditorAtom: <Value, Args extends unknown[], Result>(atom: WritableAtom<Value, Args, Result>, ...args: Args) => Result;
243
251
  private onWindowMessage;
244
252
  private handleSkipBindMode;
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import "./ChipSourceMenu.scss";
3
+ export declare const ChipSourceMenu: React.FC<{
4
+ onUpload: () => void;
5
+ onPickFromCanvas: () => void;
6
+ onClose: () => void;
7
+ /** Label for the canvas option — videos are not always eligible. */
8
+ pickLabel?: string;
9
+ }>;
10
+ /** Asks which frame of a picked video should fill the chip. */
11
+ export declare const VideoFrameChoiceMenu: React.FC<{
12
+ onChoose: (which: "first" | "last") => void;
13
+ onCancel: () => void;
14
+ }>;
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import "./ElementPickHighlight.scss";
3
+ import type { ExcalidrawElement } from "@orangecatai/element/types";
4
+ import type { AppState } from "../types";
5
+ export declare const ElementPickHighlight: React.FC<{
6
+ element: ExcalidrawElement | null;
7
+ appState: AppState;
8
+ }>;
@@ -56,9 +56,26 @@ export type AgentUpdate = {
56
56
  message: string;
57
57
  feedback: import("./reviewerAgent").ReviewerFeedback;
58
58
  };
59
+ /**
60
+ * Metadata about a frame this turn generated an image for. The caller
61
+ * (AIChatPanel) attaches this to the persisted assistant ChatMessage so that
62
+ * on a later page load / session switch, canvasTools' in-memory
63
+ * frameGenerationMemory / chatCreatedFrames (which otherwise reset with the
64
+ * page) can be reconstructed from chat history via
65
+ * resetAndSeedFrameGenerationMemory — without this, "fix the previous image"
66
+ * only works within the same unreloaded tab, which defeats the point.
67
+ */
68
+ export type FrameGenerationMeta = {
69
+ frameId: string;
70
+ prompt: string;
71
+ referenceImageIndex?: number;
72
+ userBrief: string;
73
+ };
59
74
  export type AgentResult = {
60
75
  reply: string;
61
76
  toolActions: ToolAction[];
77
+ /** Set when this turn generated (or regenerated) an image for a frame. */
78
+ frameGeneration?: FrameGenerationMeta;
62
79
  };
63
80
  export type FileAttachment = {
64
81
  name: string;
@@ -107,7 +124,7 @@ export declare function runAgentLoop(opts: {
107
124
  * Maximum number of reviewer rounds per banner.
108
125
  * After this many rounds the reviewer is skipped and the outer MAX_ITERATIONS
109
126
  * cap handles loop termination.
110
- * @default 2
127
+ * @default 1
111
128
  */
112
129
  maxReviewRounds?: number;
113
130
  }): Promise<AgentResult>;
@@ -1,5 +1,11 @@
1
1
  import type { ExcalidrawElement } from "@orangecatai/element/types";
2
2
  import type { ExcalidrawImperativeAPI, BrandContext } from "../../types";
3
+ /**
4
+ * Image model the chat agent's `generate_image` tool falls back to when the host
5
+ * does not pass `agentImageModel`. Hosts should pass the prop — this only keeps
6
+ * the SDK usable standalone.
7
+ */
8
+ export declare const DEFAULT_AGENT_IMAGE_MODEL = "openai/gpt-image-2";
3
9
  /** The only video model the chat agent can drive. */
4
10
  export declare const CHAT_VIDEO_MODEL = "google/veo-3.1-fast";
5
11
  export declare const VIDEO_TOOLS: ({
@@ -12,11 +18,21 @@ export declare const VIDEO_TOOLS: ({
12
18
  properties: {
13
19
  first_frame_element_id: {
14
20
  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.";
21
+ readonly description: "ID of an existing IMAGE or VIDEO 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, or to continue from an existing clip. When it names a video, first_frame_which selects which frame of that clip is taken.";
22
+ };
23
+ first_frame_which: {
24
+ readonly type: "string";
25
+ readonly enum: readonly ["first", "last"];
26
+ readonly description: "Only meaningful when first_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last', which is what continuing a clip needs.";
16
27
  };
17
28
  last_frame_element_id: {
18
29
  readonly type: "string";
19
- readonly description: "ID of an existing IMAGE element on the canvas to use as the video's closing frame. Optional.";
30
+ readonly description: "ID of an existing IMAGE or VIDEO element on the canvas to use as the video's closing frame. Optional.";
31
+ };
32
+ last_frame_which: {
33
+ readonly type: "string";
34
+ readonly enum: readonly ["first", "last"];
35
+ readonly description: "Only meaningful when last_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last'.";
20
36
  };
21
37
  first_frame_attachment_index: {
22
38
  readonly type: "number";
@@ -68,7 +84,12 @@ export declare const VIDEO_TOOLS: ({
68
84
  };
69
85
  first_frame_element_id: {
70
86
  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.";
87
+ readonly description: "ID of an existing IMAGE or VIDEO 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, or to continue from an existing clip. When it names a video, first_frame_which selects which frame of that clip is taken.";
88
+ };
89
+ first_frame_which: {
90
+ readonly type: "string";
91
+ readonly enum: readonly ["first", "last"];
92
+ readonly description: "Only meaningful when first_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last', which is what continuing a clip needs.";
72
93
  };
73
94
  first_frame_attachment_index: {
74
95
  readonly type: "number";
@@ -257,11 +278,21 @@ export declare const CANVAS_TOOLS: readonly [{
257
278
  properties: {
258
279
  first_frame_element_id: {
259
280
  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.";
281
+ readonly description: "ID of an existing IMAGE or VIDEO 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, or to continue from an existing clip. When it names a video, first_frame_which selects which frame of that clip is taken.";
282
+ };
283
+ first_frame_which: {
284
+ readonly type: "string";
285
+ readonly enum: readonly ["first", "last"];
286
+ readonly description: "Only meaningful when first_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last', which is what continuing a clip needs.";
261
287
  };
262
288
  last_frame_element_id: {
263
289
  readonly type: "string";
264
- readonly description: "ID of an existing IMAGE element on the canvas to use as the video's closing frame. Optional.";
290
+ readonly description: "ID of an existing IMAGE or VIDEO element on the canvas to use as the video's closing frame. Optional.";
291
+ };
292
+ last_frame_which: {
293
+ readonly type: "string";
294
+ readonly enum: readonly ["first", "last"];
295
+ readonly description: "Only meaningful when last_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last'.";
265
296
  };
266
297
  first_frame_attachment_index: {
267
298
  readonly type: "number";
@@ -313,7 +344,12 @@ export declare const CANVAS_TOOLS: readonly [{
313
344
  };
314
345
  first_frame_element_id: {
315
346
  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.";
347
+ readonly description: "ID of an existing IMAGE or VIDEO 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, or to continue from an existing clip. When it names a video, first_frame_which selects which frame of that clip is taken.";
348
+ };
349
+ first_frame_which: {
350
+ readonly type: "string";
351
+ readonly enum: readonly ["first", "last"];
352
+ readonly description: "Only meaningful when first_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last', which is what continuing a clip needs.";
317
353
  };
318
354
  first_frame_attachment_index: {
319
355
  readonly type: "number";
@@ -435,11 +471,21 @@ export declare const BANNER_TOOLS: readonly [...({
435
471
  properties: {
436
472
  first_frame_element_id: {
437
473
  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.";
474
+ readonly description: "ID of an existing IMAGE or VIDEO 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, or to continue from an existing clip. When it names a video, first_frame_which selects which frame of that clip is taken.";
475
+ };
476
+ first_frame_which: {
477
+ readonly type: "string";
478
+ readonly enum: readonly ["first", "last"];
479
+ readonly description: "Only meaningful when first_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last', which is what continuing a clip needs.";
439
480
  };
440
481
  last_frame_element_id: {
441
482
  readonly type: "string";
442
- readonly description: "ID of an existing IMAGE element on the canvas to use as the video's closing frame. Optional.";
483
+ readonly description: "ID of an existing IMAGE or VIDEO element on the canvas to use as the video's closing frame. Optional.";
484
+ };
485
+ last_frame_which: {
486
+ readonly type: "string";
487
+ readonly enum: readonly ["first", "last"];
488
+ readonly description: "Only meaningful when last_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last'.";
443
489
  };
444
490
  first_frame_attachment_index: {
445
491
  readonly type: "number";
@@ -491,7 +537,12 @@ export declare const BANNER_TOOLS: readonly [...({
491
537
  };
492
538
  first_frame_element_id: {
493
539
  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.";
540
+ readonly description: "ID of an existing IMAGE or VIDEO 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, or to continue from an existing clip. When it names a video, first_frame_which selects which frame of that clip is taken.";
541
+ };
542
+ first_frame_which: {
543
+ readonly type: "string";
544
+ readonly enum: readonly ["first", "last"];
545
+ readonly description: "Only meaningful when first_frame_element_id names a VIDEO. Which frame of that clip to use. Defaults to 'last', which is what continuing a clip needs.";
495
546
  };
496
547
  first_frame_attachment_index: {
497
548
  readonly type: "number";
@@ -1011,6 +1062,18 @@ export type ToolExecutionContext = {
1011
1062
  * failure mode for private Vercel Blob URLs.
1012
1063
  */
1013
1064
  _templateJsonCache?: Map<string, string>;
1065
+ /**
1066
+ * The user's own first message, verbatim (not the LLM's paraphrase of it).
1067
+ * generate_image's `prompt` argument is always authored fresh by the chat
1068
+ * model — nothing forces it to preserve specific subject/scene/action
1069
+ * details the user actually typed (only on-image copy is instructed to be
1070
+ * verbatim). execGenerateImage appends this raw text to every image-gen
1071
+ * call for a frame as a fidelity backstop, so a model that quietly drops or
1072
+ * substitutes a described detail (e.g. "boy playing guitar" -> "boy
1073
+ * walking") doesn't silently ship — the image model sees the user's exact
1074
+ * words either way.
1075
+ */
1076
+ originalUserBrief?: string;
1014
1077
  };
1015
1078
  export type ToolResult = {
1016
1079
  success: boolean;
@@ -1019,3 +1082,44 @@ export type ToolResult = {
1019
1082
  statusMessage: string;
1020
1083
  };
1021
1084
  export declare function executeCanvasTool(toolName: string, rawArgs: string, ctx: ToolExecutionContext): Promise<ToolResult>;
1085
+ /**
1086
+ * Renders `chatCreatedFrames` as a system-message string for injection into
1087
+ * the agent loop, or "" if this session hasn't generated anything yet (in
1088
+ * which case the caller should skip adding a message at all).
1089
+ */
1090
+ export declare function getChatCreatedFramesContext(): string;
1091
+ /**
1092
+ * Rebuilds `frameGenerationMemory` and `chatCreatedFrames` from persisted
1093
+ * chat history (the assistant messages' `generatedFrame` metadata — see
1094
+ * `FrameGenerationMeta` in agentLoop.ts). Both are in-memory module state
1095
+ * that would otherwise be empty after a page reload or a chat-session
1096
+ * switch, silently breaking "fix the previous image" the moment the tab
1097
+ * isn't the one that generated it. Call this whenever chat history is
1098
+ * (re)loaded — on mount and on every session switch — BEFORE the next
1099
+ * message is sent.
1100
+ *
1101
+ * Always clears first: this also fixes the memory leaking across sessions —
1102
+ * without a reset, switching to a different chat would keep the previous
1103
+ * session's frames in the "generated in this conversation" list.
1104
+ *
1105
+ * Takes a loosely-typed array (not the SDK's `ChatMessage[]`) to avoid a
1106
+ * circular import — `AIChatPanel.tsx` imports FROM this file already.
1107
+ */
1108
+ export declare function resetAndSeedFrameGenerationMemory(messages: Array<{
1109
+ generatedFrame?: {
1110
+ frameId: string;
1111
+ prompt: string;
1112
+ referenceImageIndex?: number;
1113
+ userBrief: string;
1114
+ };
1115
+ }>): void;
1116
+ /**
1117
+ * Fits a generated image to its frame, returning the size to place it at and
1118
+ * its offset from the frame's top-left corner.
1119
+ */
1120
+ export declare function fitGeneratedImageToFrame(actualW: number, actualH: number, frameW: number, frameH: number): {
1121
+ width: number;
1122
+ height: number;
1123
+ offsetX: number;
1124
+ offsetY: number;
1125
+ };
@@ -62,7 +62,13 @@ export declare function runReviewerAgent(opts: {
62
62
  width: number;
63
63
  height: number;
64
64
  };
65
- /** Original user request — first message only, not full conversation history */
65
+ /**
66
+ * All of the user's text turns in the conversation, concatenated — not
67
+ * just the first message. The actual creative brief is often not
68
+ * userMessages[0] (e.g. "hi" -> clarifying questions -> the message that
69
+ * actually describes the ad), so anchoring to only the first message can
70
+ * silently drop the real brief.
71
+ */
66
72
  userBrief: string;
67
73
  /**
68
74
  * The original user-supplied reference image used to generate this frame
@@ -152,6 +152,12 @@ export declare function enforceMinProductBox({ x, y, width, height, targetW, tar
152
152
  width: number;
153
153
  height: number;
154
154
  };
155
+ export declare function buildAutoResizeImagePrompt(srcW: number, srcH: number, tgtW: number, tgtH: number, aspectRatio: string, brandImages?: {
156
+ logo?: string;
157
+ fontPreview?: string;
158
+ fontUrl?: string;
159
+ name?: string;
160
+ }, sourceTextColors?: string[]): string;
155
161
  export type PreCreatedFrameInfo = {
156
162
  frameId: string;
157
163
  x: number;
@@ -0,0 +1,33 @@
1
+ import type { ExcalidrawElement } from "@orangecatai/element/types";
2
+ /** Which element types a given pick will accept. */
3
+ export type PickerAccept = "image" | "imageOrVideo";
4
+ export type ElementPickerState = {
5
+ accept: PickerAccept;
6
+ /** Fired with the picked element. The picker is disarmed first. */
7
+ onPick: (element: ExcalidrawElement) => void;
8
+ /** Fired on Esc, or when the arming component unmounts. */
9
+ onCancel: () => void;
10
+ /**
11
+ * Fired when the user clicks canvas that holds nothing pickable. The picker
12
+ * stays armed — a stray click should not force the user to re-arm.
13
+ */
14
+ onMiss?: () => void;
15
+ /**
16
+ * Fired as the pointer moves with the element currently under it (or null).
17
+ * The arming component draws the highlight, so App never re-renders the
18
+ * editor on hover.
19
+ */
20
+ onHover?: (element: ExcalidrawElement | null) => void;
21
+ };
22
+ export declare const elementPickerAtom: import("jotai/vanilla/atom").PrimitiveAtom<ElementPickerState | null> & {
23
+ init: ElementPickerState | null;
24
+ };
25
+ export declare const isPickable: (element: ExcalidrawElement, accept: PickerAccept) => boolean;
26
+ /**
27
+ * Returns the topmost pickable element containing the given scene point, or
28
+ * null. Deliberately does its own hit test rather than reusing
29
+ * `App.getElementAtPosition`: that method is private, weighs z-index against
30
+ * selection and bound text, and considers every element type — whereas a pick
31
+ * only ever targets images and videos, both plain rectangles.
32
+ */
33
+ export declare const pickElementAt: (elements: readonly ExcalidrawElement[], x: number, y: number, accept: PickerAccept) => ExcalidrawElement | null;
@@ -1,4 +1,4 @@
1
- import type { ElementsMapOrArray, ExcalidrawFrameLikeElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
1
+ import type { ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
2
2
  export type FrameToolbarVariant = "frame" | "video";
3
3
  export type FrameToolbarVariantResult = {
4
4
  variant: FrameToolbarVariant;
@@ -21,3 +21,16 @@ export type FrameToolbarVariantResult = {
21
21
  * Campaign on one would present an action that is guaranteed to fail.
22
22
  */
23
23
  export declare const getFrameToolbarVariant: (frame: ExcalidrawFrameLikeElement, elements: ElementsMapOrArray) => FrameToolbarVariantResult | null;
24
+ /**
25
+ * Resolves the frame that Resize should act on when an *image* is selected.
26
+ *
27
+ * An ad frame is covered edge to edge by its generated image, so clicking the
28
+ * artwork selects that child image, not the frame — and the child shows
29
+ * `ImageEditToolbar`, which has no Resize. From the user's side the button
30
+ * simply vanishes depending on where they clicked. This lets the image toolbar
31
+ * offer the same Resize action, targeting the parent frame.
32
+ *
33
+ * Returns null unless the parent frame would itself show the full "frame"
34
+ * toolbar, so the child never surfaces an action the frame does not have.
35
+ */
36
+ export declare const getResizeTargetFrame: (element: ExcalidrawElement, elements: readonly ExcalidrawElement[]) => ExcalidrawFrameLikeElement | null;
@@ -0,0 +1,28 @@
1
+ import type { PickerAccept } from "./elementPicker";
2
+ import type { VideoFrameChoice } from "../utils/videoFrameSource";
3
+ import type { AppClassProperties } from "../types";
4
+ import type { ExcalidrawElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
5
+ /**
6
+ * `slot` is opaque to the hook — panels use it to route the resolved data URL
7
+ * back to the right chip ("first", "last", "reference", …).
8
+ */
9
+ export type CanvasFramePicker = {
10
+ /** Slot currently awaiting a canvas click, or null. */
11
+ pickingSlot: string | null;
12
+ /** Video awaiting a first/last choice, or null. */
13
+ pendingVideo: {
14
+ slot: string;
15
+ element: ExcalidrawVideoElement;
16
+ } | null;
17
+ /** True while a video frame is being downloaded and decoded. */
18
+ isResolving: boolean;
19
+ error: string | null;
20
+ /** True when the last canvas click landed on nothing pickable. */
21
+ missed: boolean;
22
+ /** Element currently under the pointer while picking, for the highlight. */
23
+ hoveredElement: ExcalidrawElement | null;
24
+ begin: (slot: string, accept: PickerAccept) => void;
25
+ cancel: () => void;
26
+ chooseVideoFrame: (which: VideoFrameChoice) => void;
27
+ };
28
+ export declare const useCanvasFramePicker: (app: AppClassProperties, onResolved: (slot: string, dataUrl: string) => void) => CanvasFramePicker;
@@ -761,8 +761,8 @@ export interface ExcalidrawProps {
761
761
  * background regeneration and the agent chat `generate_image` tool.
762
762
  * Pass the same value here and to `AIChatPanel.agentImageModel` to keep
763
763
  * both surfaces on the same model with a single configuration.
764
- * @default "google/gemini-3.1-flash-image-preview"
765
- * @example "google/gemini-2.5-flash-image"
764
+ * @default "openai/gpt-image-2"
765
+ * @example "google/gemini-3.1-flash-image-preview"
766
766
  */
767
767
  agentImageModel?: string;
768
768
  /**
@@ -923,6 +923,7 @@ export type AppClassProperties = {
923
923
  editorInterface: App["editorInterface"];
924
924
  scene: App["scene"];
925
925
  syncActionResult: App["syncActionResult"];
926
+ setElementPicker: App["setElementPicker"];
926
927
  fonts: App["fonts"];
927
928
  pasteFromClipboard: App["pasteFromClipboard"];
928
929
  id: App["id"];
@@ -17,9 +17,16 @@ export declare function buildBrandContextMessage(ctx: BrandContext): string;
17
17
  * block before the prompt. This function only appends the per-asset mandatory
18
18
  * instructions that reference the visual attachments by position.
19
19
  */
20
- export declare function buildImageGenSafePrompt(prompt: string, brandContext: BrandContext, dimensions?: {
21
- width: number;
22
- height: number;
20
+ export declare function buildImageGenSafePrompt(prompt: string, brandContext: BrandContext, opts?: {
21
+ /**
22
+ * True when the user attached their own reference images. The brand logo
23
+ * and font specimens are then not attached at all (see imageApi), so the
24
+ * mandates that point at them — "embed the logo VERBATIM", "render ALL text
25
+ * in this exact typeface" — must go too: they would order the model to
26
+ * reproduce images it can no longer see, and they compete with the
27
+ * reference the user actually chose.
28
+ */
29
+ hasUserReferences?: boolean;
23
30
  }): string;
24
31
  /**
25
32
  * Augments a video prompt with brand direction reframed for motion.
@@ -41,4 +48,10 @@ export declare function buildImageGenSafePrompt(prompt: string, brandContext: Br
41
48
  */
42
49
  export declare function buildVideoBrandPrompt(prompt: string, ctx: BrandContext, opts?: {
43
50
  hasLogoReference?: boolean;
51
+ /**
52
+ * How many of the attached references the user supplied themselves. They
53
+ * are attached ahead of the brand logo, so this also fixes the logo's
54
+ * position in the numbered manifest below.
55
+ */
56
+ userReferenceCount?: number;
44
57
  }): string;
@@ -1,4 +1,27 @@
1
- export declare function callOpenRouterImageAPI(prompt: string, modelId: string, aspectRatio: string, imageSize: string | null, textOutput: boolean, imageGenUrl: string, referenceImageDataUrl?: string, signal?: AbortSignal, brandImages?: {
1
+ /**
2
+ * Hard cap on user-supplied reference images per generation. Providers differ on
3
+ * how many references they accept; six covers every realistic ad brief and keeps
4
+ * the request well inside the limits of the models we route to.
5
+ */
6
+ export declare const MAX_USER_REFERENCE_IMAGES = 6;
7
+ /**
8
+ * Describes each attached reference image by its real 1-based position, and
9
+ * states the precedence rule between them.
10
+ *
11
+ * This exists because attachment order alone does not steer the model — it
12
+ * follows whichever image the prompt names. Previously only the brand assets
13
+ * were named ("the first reference image is the exact brand logo"), so a
14
+ * user-supplied reference sat in the array unmentioned and was ignored.
15
+ *
16
+ * When the user attaches anything, their references are the authority: the
17
+ * brand kit, stored logo, and reference ads are explicitly demoted so a
18
+ * contradiction resolves in the user's favour rather than the brand's.
19
+ *
20
+ * Returns "" when there is nothing worth numbering, so prompts that carry no
21
+ * attachments are left byte-identical to before.
22
+ */
23
+ export declare function buildReferenceManifest(userReferenceCount: number, brandLabels: string[]): string;
24
+ export declare function callOpenRouterImageAPI(prompt: string, modelId: string, aspectRatio: string, imageSize: string | null, textOutput: boolean, imageGenUrl: string, referenceImageDataUrl?: string | string[], signal?: AbortSignal, brandImages?: {
2
25
  logo?: string;
3
26
  fontPreview?: string;
4
27
  /** Optional second font preview (brand body typeface) shown as a reference. */
@@ -16,4 +39,21 @@ export declare function callOpenRouterImageAPI(prompt: string, modelId: string,
16
39
  * prepended to the prompt so the model applies brand guidelines to the image.
17
40
  */
18
41
  brandContextText?: string;
42
+ }, options?: {
43
+ /**
44
+ * How to treat `referenceImageDataUrl`.
45
+ *
46
+ * "user-priority" — the references came from the end user. They are
47
+ * attached FIRST, named individually in the reference manifest, and
48
+ * explicitly overrule the brand kit, stored logo, and brand reference ads
49
+ * on any contradiction. Used by the surfaces where the user picks the
50
+ * reference themselves: image generation, quick edit, and canvas chat.
51
+ *
52
+ * "legacy-positional" (default) — the reference is a pipeline input rather
53
+ * than a user choice (e.g. the auto-resize source ad), and the caller has
54
+ * already written its own numbered reference list into the prompt assuming
55
+ * brand assets come first. Preserves the original ordering and emits no
56
+ * manifest, so those callers are unaffected by this option.
57
+ */
58
+ referenceMode?: "user-priority" | "legacy-positional";
19
59
  }): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import type { ExcalidrawVideoElement } from "@orangecatai/element/types";
2
+ export type VideoFrameChoice = "first" | "last";
3
+ /**
4
+ * Downloads the video bytes. Routes through the host's download proxy for the
5
+ * same reason playback does — presigned URLs expire on long-lived canvases —
6
+ * while `blob:` URLs are fetched directly (they resolve only in this tab, and
7
+ * the proxy could not reach them anyway).
8
+ */
9
+ export declare const fetchVideoBlob: (videoUrl: string, videoDownloadProxyUrl?: string) => Promise<Blob>;
10
+ /**
11
+ * Resolves one frame of a canvas video element to a PNG/JPEG data URL.
12
+ *
13
+ * The first frame is already cached on the element as `thumbnailDataUrl`, so
14
+ * that case avoids the download entirely; only a last-frame pick (or a video
15
+ * whose thumbnail never materialised) pays for the fetch and decode.
16
+ */
17
+ export declare const extractFrameFromVideoElement: (element: ExcalidrawVideoElement, which: VideoFrameChoice, videoDownloadProxyUrl?: string) => Promise<string>;
@@ -1,14 +1,14 @@
1
1
  export type AudioPolicy = "first-only" | "none";
2
2
  /**
3
- * Decodes the final frame of a video and returns it as a lossless PNG data
4
- * URL, suitable as the next chain segment's `firstFrameDataUrl`.
5
- *
6
- * Uses `CanvasSink.getCanvas(Infinity)` rather than seeking a `<video>`
7
- * element to `duration - epsilon`: a time-based seek can land on a keyframe
8
- * boundary or a black tail frame depending on the encoder, which poisons the
9
- * next segment's starting state.
3
+ * Decodes the final frame of a video, suitable as the next chain segment's
4
+ * `firstFrameDataUrl`.
10
5
  */
11
6
  export declare function extractLastFrame(video: Blob): Promise<string>;
7
+ /**
8
+ * Decodes the opening frame of a video. Used when linking an existing canvas
9
+ * video into a generator panel's start/end frame chip.
10
+ */
11
+ export declare function extractFirstFrame(video: Blob): Promise<string>;
12
12
  /**
13
13
  * Stream-copy concatenates segments produced by the same model at the same
14
14
  * codec/resolution/fps into one MP4. No re-encode — packets are demuxed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecatai/adgen-canvas",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module",
5
5
  "types": "./dist/types/excalidraw/index.d.ts",
6
6
  "main": "./dist/prod/index.js",
@@ -1 +0,0 @@
1
- import{S as a,T as b,U as c}from"../chunk-GTYNSLQE.js";import"../chunk-SZMIDXG6.js";import"../chunk-SRAX5OIU.js";export{c as decodePngMetadata,b as encodePngMetadata,a as getTEXtChunk};