@orangecatai/adgen-canvas 0.0.39 → 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/dev/chunk-2K6ZAAHB.js +1535 -0
- package/dist/dev/chunk-2K6ZAAHB.js.map +7 -0
- package/dist/dev/{chunk-E7VZGEUJ.js → chunk-2Q5Z4UTF.js} +2 -2
- package/dist/dev/{chunk-E7VZGEUJ.js.map → chunk-2Q5Z4UTF.js.map} +1 -1
- package/dist/dev/{chunk-LIELFD5T.js → chunk-3WIRZ6S7.js} +6 -1
- package/dist/dev/chunk-3WIRZ6S7.js.map +7 -0
- package/dist/dev/{chunk-5XP2WJAQ.js → chunk-7XZBXIDZ.js} +1300 -1252
- package/dist/dev/chunk-7XZBXIDZ.js.map +7 -0
- package/dist/dev/chunk-AKA4GZ3R.js +62 -0
- package/dist/dev/chunk-AKA4GZ3R.js.map +7 -0
- package/dist/dev/chunk-LWUV2KR2.js +27 -0
- package/dist/dev/chunk-LWUV2KR2.js.map +7 -0
- package/dist/dev/chunk-T57LHRZV.js +2167 -0
- package/dist/dev/chunk-T57LHRZV.js.map +7 -0
- package/dist/dev/components/auto-resize/nativeResizeAgent-5XWJJ74W.js +1074 -0
- package/dist/dev/components/auto-resize/nativeResizeAgent-5XWJJ74W.js.map +7 -0
- package/dist/dev/data/{image-PUPXCX7M.js → image-MTDE3WXB.js} +4 -3
- package/dist/dev/index.css +618 -27
- package/dist/dev/index.css.map +3 -3
- package/dist/dev/index.js +6769 -8217
- package/dist/dev/index.js.map +4 -4
- package/dist/dev/locales/{en-VCZQBJ2N.js → en-3S2H3QGO.js} +2 -2
- package/dist/dev/model-capabilities.js +47 -0
- package/dist/dev/model-capabilities.js.map +7 -0
- package/dist/dev/subset-shared.chunk.js +1 -1
- package/dist/dev/subset-worker.chunk.js +1 -1
- package/dist/prod/chunk-6YYSQB5E.js +1 -0
- package/dist/prod/{chunk-CLQPYXMS.js → chunk-PIM2DC2L.js} +3 -3
- package/dist/prod/chunk-POCDVKZK.js +142 -0
- package/dist/prod/chunk-PWBUVPCU.js +1 -0
- package/dist/prod/{chunk-WBFDC4WN.js → chunk-R4DNAEVN.js} +1 -1
- package/dist/prod/chunk-U2I2OXQ7.js +4 -0
- package/dist/prod/chunk-UR7GPHHO.js +1 -0
- package/dist/prod/components/auto-resize/nativeResizeAgent-45UNA3LO.js +5 -0
- package/dist/prod/data/image-U4RMOYQQ.js +1 -0
- package/dist/prod/index.css +1 -1
- package/dist/prod/index.js +67 -298
- package/dist/prod/locales/{en-ZZURI326.js → en-Z3CKUL4S.js} +1 -1
- package/dist/prod/model-capabilities.js +1 -0
- package/dist/prod/subset-shared.chunk.js +1 -1
- package/dist/prod/subset-worker.chunk.js +1 -1
- package/dist/types/common/src/utils.d.ts +4 -1
- package/dist/types/element/src/index.d.ts +3 -0
- package/dist/types/element/src/newElement.d.ts +2 -1
- package/dist/types/element/src/textHtml.d.ts +14 -0
- package/dist/types/element/src/textLayout.d.ts +97 -0
- package/dist/types/element/src/textStyleRanges.d.ts +62 -0
- package/dist/types/element/src/textWrapping.d.ts +27 -0
- package/dist/types/element/src/types.d.ts +35 -0
- package/dist/types/excalidraw/actions/actionFrame.d.ts +1 -0
- package/dist/types/excalidraw/actions/actionLinearEditor.d.ts +1 -0
- package/dist/types/excalidraw/actions/actionProperties.d.ts +50 -0
- package/dist/types/excalidraw/actions/index.d.ts +1 -1
- package/dist/types/excalidraw/actions/types.d.ts +1 -1
- package/dist/types/excalidraw/components/AIChatPanel.d.ts +18 -2
- package/dist/types/excalidraw/components/App.d.ts +6 -0
- package/dist/types/excalidraw/components/FontSizeInput.d.ts +19 -0
- package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +946 -45
- package/dist/types/excalidraw/components/VideoGeneratorPanel.d.ts +67 -53
- package/dist/types/excalidraw/components/ai-chat/ActivityTimeline.d.ts +18 -0
- package/dist/types/excalidraw/components/ai-chat/activitySteps.d.ts +106 -0
- package/dist/types/excalidraw/components/ai-chat/agentLoop.d.ts +66 -3
- package/dist/types/excalidraw/components/ai-chat/canvasTools.d.ts +23 -8
- package/dist/types/excalidraw/components/ai-chat/htmlToExcalidraw.d.ts +3 -0
- package/dist/types/excalidraw/components/ai-chat/openRouterStream.d.ts +13 -0
- package/dist/types/excalidraw/components/auto-resize/ResizeActivityCard.d.ts +7 -0
- package/dist/types/excalidraw/components/auto-resize/autoResizeEngine.d.ts +5 -2
- package/dist/types/excalidraw/components/auto-resize/autoResizeStore.d.ts +23 -0
- package/dist/types/excalidraw/components/auto-resize/imageBounds.d.ts +11 -0
- package/dist/types/excalidraw/components/auto-resize/nativeResizeAgent.d.ts +5 -0
- package/dist/types/excalidraw/components/auto-resize/resizeFitting.d.ts +7 -0
- package/dist/types/excalidraw/components/auto-resize/resizeLayout.d.ts +33 -0
- package/dist/types/excalidraw/components/icons.d.ts +6 -0
- package/dist/types/excalidraw/components/template-builder/TemplateBuilderTray.d.ts +1 -0
- package/dist/types/excalidraw/index.d.ts +9 -0
- package/dist/types/excalidraw/modelCapabilities/index.d.ts +17 -0
- package/dist/types/excalidraw/modelCapabilities/registry.image.d.ts +2 -0
- package/dist/types/excalidraw/modelCapabilities/registry.video.d.ts +2 -0
- package/dist/types/excalidraw/modelCapabilities/shared.d.ts +24 -0
- package/dist/types/excalidraw/modelCapabilities/types.d.ts +276 -0
- package/dist/types/excalidraw/modelCapabilities/validate.d.ts +48 -0
- package/dist/types/excalidraw/types.d.ts +4 -4
- package/dist/types/excalidraw/utils/imageApi.d.ts +3 -0
- package/dist/types/excalidraw/utils/referenceBudget.d.ts +12 -23
- package/dist/types/excalidraw/utils/videoApi.d.ts +21 -1
- package/dist/types/excalidraw/wysiwyg/richTextEditable.d.ts +36 -0
- package/dist/types/excalidraw/wysiwyg/textSelectionStyles.d.ts +32 -0
- package/dist/types/excalidraw/wysiwyg/textWysiwyg.d.ts +6 -2
- package/package.json +10 -4
- package/dist/dev/chunk-5XP2WJAQ.js.map +0 -7
- package/dist/dev/chunk-LIELFD5T.js.map +0 -7
- package/dist/prod/chunk-T3ZLBHX4.js +0 -4
- package/dist/prod/data/image-AG3CTRSJ.js +0 -1
- /package/dist/dev/data/{image-PUPXCX7M.js.map → image-MTDE3WXB.js.map} +0 -0
- /package/dist/dev/locales/{en-VCZQBJ2N.js.map → en-3S2H3QGO.js.map} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../components/auto-resize/frameSource.ts", "../../utils/imageCompression.ts", "../../utils/referenceBudget.ts", "../../utils/imageApi.ts", "../../components/auto-resize/autoResizeStore.ts", "../../components/auto-resize/backgroundExtend.ts", "../../components/auto-resize/autoResizeEngine.ts", "../../components/ai-chat/htmlToExcalidraw.ts", "../../components/ai-chat/imagePayload.ts", "../../components/ai-chat/reviewerAgent.ts"],
|
|
4
|
+
"sourcesContent": ["// ---------------------------------------------------------------------------\n// Structured-frame detection for Auto Resize\n//\n// Auto Resize re-lays-out an ad by repositioning its individual layers. That\n// only works on a frame that HAS individual layers. A freshly generated ad is\n// a single flat image filling its frame \u2014 there is nothing to re-lay-out, and\n// asking an image model to repaint it is what produced letterboxed output.\n//\n// An editable frame can arrive two ways, and this deliberately cannot tell\n// them apart:\n// 1. image generation \u2192 \"Make editable\" (decompose)\n// 2. the user composing or editing a frame by hand\n//\n// Frame membership is a `frameId` back-reference on each child, so both routes\n// look identical here. That is the point: the signal is about the frame's\n// CURRENT structure, not its provenance.\n// ---------------------------------------------------------------------------\n\nimport { getFrameChildren } from \"@orangecatai/element\";\n\nimport type {\n ExcalidrawElement,\n ExcalidrawFrameLikeElement,\n} from \"@orangecatai/element/types\";\n\n/** Live (non-deleted) children of a frame. */\nexport function getLiveFrameChildren(\n elements: readonly ExcalidrawElement[],\n frame: ExcalidrawFrameLikeElement,\n): readonly ExcalidrawElement[] {\n return getFrameChildren(elements, frame.id).filter((el) => !el.isDeleted);\n}\n\n/**\n * True when this frame holds separable layers Auto Resize can re-arrange.\n *\n * Any ONE of these is sufficient:\n * - more than one live child \u2192 there is something to arrange\n * - a child tagged `decomposeType` \u2192 came from \"Make editable\"\n * - a text child \u2192 real editable copy, so the frame is not a flat render\n *\n * False for the flat case: zero or one child (typically one full-bleed image).\n */\nexport function isStructuredFrame(\n elements: readonly ExcalidrawElement[],\n frame: ExcalidrawFrameLikeElement,\n): boolean {\n const children = getLiveFrameChildren(elements, frame);\n\n if (children.length > 1) {\n return true;\n }\n\n return children.some((el) => {\n if (el.type === \"text\") {\n return true;\n }\n const customData = (\n el as ExcalidrawElement & {\n customData?: Record<string, unknown>;\n }\n ).customData;\n return typeof customData?.decomposeType === \"string\";\n });\n}\n\n/** Human-readable reason shown when Auto Resize is unavailable. */\nexport const AUTO_RESIZE_FLAT_FRAME_REASON =\n \"Make this ad editable first \u2014 Auto Resize needs separate layers to re-lay-out.\";\n", "/**\n * Shared image-compression primitives.\n *\n * Extracted from `components/ai-chat/imagePayload` so the image-generation\n * surfaces (canvas chat, generator panel, quick edit, auto-resize) can use the\n * same downscaling the chat request path has always used. Previously only chat\n * shrank its images, which is why an image-gen request could still carry a raw\n * 1 MB PNG straight at the host proxy's 4.5 MB platform cap.\n *\n * Everything here is best-effort and never throws: on any failure the original\n * data URL is returned unchanged, and the caller's budget pass is the backstop.\n */\n\n/** Long-edge cap in CSS pixels. Well above what vision models resolve. */\nexport const MAX_EDGE_PX = 1280;\n/** JPEG quality for re-encoded images. */\nexport const JPEG_QUALITY = 0.78;\n/**\n * Images already smaller than this are passed through untouched \u2014 re-encoding\n * a 40KB thumbnail costs a canvas round-trip and can make it *larger*.\n */\nexport const PASSTHROUGH_BYTES = 200 * 1024;\n/** Decode ceiling. A stalled decode must not hang the send indefinitely. */\nconst DECODE_TIMEOUT_MS = 10_000;\n\n/** Estimated wire size of a data URL: the base64 payload is what gets sent. */\nexport function dataUrlWireBytes(dataUrl: string): number {\n const commaIdx = dataUrl.indexOf(\",\");\n return commaIdx >= 0 ? dataUrl.length - commaIdx - 1 : dataUrl.length;\n}\n\n/**\n * Whether a data URL is a raster format safe to send as an `image_url` block.\n * SVG is commonly rejected outright by vision models, and canvas re-encoding\n * of an SVG is unreliable across browsers, so it is neither shrunk nor sent.\n */\nexport function isRasterImageDataUrl(dataUrl: string): boolean {\n return /^data:image\\/(png|jpe?g|gif|webp);base64,/i.test(dataUrl);\n}\n\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n const timer = setTimeout(() => {\n img.onload = null;\n img.onerror = null;\n reject(new Error(\"image decode timed out\"));\n }, DECODE_TIMEOUT_MS);\n img.onload = () => {\n clearTimeout(timer);\n resolve(img);\n };\n img.onerror = () => {\n clearTimeout(timer);\n reject(new Error(\"image decode failed\"));\n };\n img.src = src;\n });\n}\n\n/** Draw `img` onto a white-flattened canvas at `w`\u00D7`h` and encode as JPEG. */\nfunction encodeAt(\n img: HTMLImageElement,\n w: number,\n h: number,\n quality: number,\n): string | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n return null;\n }\n // JPEG has no alpha; flatten onto white so transparent PNGs (logos, cutout\n // product shots) don't come out with black backgrounds.\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, w, h);\n ctx.drawImage(img, 0, 0, w, h);\n return canvas.toDataURL(\"image/jpeg\", quality);\n}\n\n/**\n * Downscale + re-encode a raster data URL so it is safe to embed in a request.\n *\n * Never throws: on any failure (decode error, no canvas context, non-raster\n * input) the original data URL is returned unchanged.\n */\nexport async function shrinkImageDataUrl(dataUrl: string): Promise<string> {\n if (!isRasterImageDataUrl(dataUrl)) {\n return dataUrl;\n }\n if (dataUrlWireBytes(dataUrl) <= PASSTHROUGH_BYTES) {\n return dataUrl;\n }\n\n try {\n const img = await loadImage(dataUrl);\n const { width, height } = img;\n if (!width || !height) {\n return dataUrl;\n }\n\n const scale = Math.min(1, MAX_EDGE_PX / Math.max(width, height));\n const shrunk = encodeAt(\n img,\n Math.max(1, Math.round(width * scale)),\n Math.max(1, Math.round(height * scale)),\n JPEG_QUALITY,\n );\n if (!shrunk) {\n return dataUrl;\n }\n // Re-encoding can lose to the original (already-optimised JPEGs at low\n // resolution); keep whichever is actually smaller.\n return dataUrlWireBytes(shrunk) < dataUrlWireBytes(dataUrl)\n ? shrunk\n : dataUrl;\n } catch {\n return dataUrl;\n }\n}\n\n/** Shrink a batch in parallel, preserving order. */\nexport function shrinkImageDataUrls(dataUrls: string[]): Promise<string[]> {\n return Promise.all(dataUrls.map(shrinkImageDataUrl));\n}\n\n/**\n * Force a re-encode at a specific long-edge cap, ignoring the passthrough rule.\n * Used to escalate compression when a request is still over budget after the\n * standard downscale \u2014 always preferable to dropping a reference outright.\n */\nexport async function reencodeAtEdge(\n dataUrl: string,\n edge: number,\n quality = 0.7,\n): Promise<string> {\n if (!isRasterImageDataUrl(dataUrl)) {\n return dataUrl;\n }\n try {\n const img = await loadImage(dataUrl);\n if (!img.width || !img.height) {\n return dataUrl;\n }\n const scale = Math.min(1, edge / Math.max(img.width, img.height));\n const out = encodeAt(\n img,\n Math.max(1, Math.round(img.width * scale)),\n Math.max(1, Math.round(img.height * scale)),\n quality,\n );\n if (!out) {\n return dataUrl;\n }\n return dataUrlWireBytes(out) < dataUrlWireBytes(dataUrl) ? out : dataUrl;\n } catch {\n return dataUrl;\n }\n}\n\n/** Escalation ladder used when the standard downscale is not enough. */\nexport const ESCALATION_EDGES = [960, 720, 512] as const;\n", "/**\n * Ranked slot allocation for the reference images attached to an image\n * generation request.\n *\n * ## Why this exists\n *\n * The previous rule was a single boolean: if the user attached ANY reference,\n * the brand logo, both font specimens, and every host-injected brand asset were\n * deleted. Unconditionally \u2014 no size check, no count check. That made\n * \"replace their logo with ours\" unsatisfiable, because the logo the prompt\n * asked for was never attached.\n *\n * The real constraint was never \"brand assets compete with the user\" \u2014 it is\n * the host proxy's platform request cap (Vercel rejects bodies over 4.5 MB at\n * the edge with an opaque 413). Competition is solved by ORDER and by the\n * prompt's precedence rules, not by deletion.\n *\n * ## The contract\n *\n * Priority, highest first:\n *\n * 1. `user` \u2014 references the user attached themselves (max 6)\n * 2. `source` \u2014 the image being edited or resized (quick edit, resize)\n * 3. `logo` \u2014 the brand logo\n * 4. `brandAsset` \u2014 product / lifestyle photos from the brand bank\n * 5. `adReference` \u2014 existing ad creatives and template thumbnails\n *\n * Everything is sent when it fits. Truncation happens ONLY when the count or\n * byte budget is exceeded, and always from the bottom up \u2014 never touching\n * `user`, and never touching `logo` unless the caller marks it droppable\n * (quick edit and resize, where the source image matters more).\n *\n * Brand assets and ad references degrade gracefully: surplus copies go first,\n * so one of each survives as long as possible, and only then is a whole tier\n * given up \u2014 ad references before brand assets, per the priority order.\n *\n * The floor is therefore 6 user references + the logo.\n *\n * ## Compression first, always\n *\n * Dropping a reference is the last resort, not the first. `prepareReferences`\n * downscales everything, then escalates compression on the largest offenders,\n * and only starts shedding slots if the payload is *still* over budget.\n */\n\nimport {\n HOST_ASSET_BYTE_RESERVATION,\n MAX_REFERENCE_IMAGES,\n MAX_USER_REFERENCE_IMAGES,\n REFERENCE_WIRE_BUDGET_BYTES,\n} from \"../modelCapabilities/types\";\n\nimport {\n ESCALATION_EDGES,\n dataUrlWireBytes,\n reencodeAtEdge,\n shrinkImageDataUrl,\n} from \"./imageCompression\";\n\n/** Reference tiers in descending priority. Index === priority rank. */\nexport const REFERENCE_TIERS = [\n \"user\",\n \"source\",\n \"logo\",\n \"brandAsset\",\n \"adReference\",\n] as const;\n\nexport type ReferenceTier = typeof REFERENCE_TIERS[number];\n\nexport interface ReferenceSlot {\n tier: ReferenceTier;\n /**\n * The image itself. Empty string marks a *placeholder* \u2014 a slot the host\n * proxy will fill from the brand database, which the SDK can only reserve\n * room for because it never sees the bytes. `bytes` carries the reservation.\n */\n dataUrl: string;\n /** Human label used in the reference manifest. */\n label?: string;\n /** Wire-byte estimate. Derived from `dataUrl` when omitted. */\n bytes?: number;\n}\n\nexport interface DroppedSlot {\n tier: ReferenceTier;\n label?: string;\n reason: \"count\" | \"bytes\";\n}\n\n/**\n * These four are TRANSPORT guards \u2014 our own limits, not any provider's \u2014 and\n * they now live alongside the capability contract so both enforcement layers\n * read the same numbers. Re-exported here because this module has been their\n * import site for a long time.\n *\n * A transport guard and a provider limit are different things and either may\n * bind. Most providers accept far more than we send (Seedance allows 30\n * references and a 64 MB body), but two accept fewer \u2014 MAI Image takes one\n * input image, Grok Imagine Image five \u2014 so a count that clears these guards\n * can still be invalid. Use `effectiveUserReferenceLimit` from\n * ../modelCapabilities, which takes the smaller of the two.\n */\nexport {\n HOST_ASSET_BYTE_RESERVATION,\n MAX_REFERENCE_IMAGES,\n MAX_USER_REFERENCE_IMAGES,\n REFERENCE_WIRE_BUDGET_BYTES,\n};\n\n/** Minimum number of each degradable tier to preserve while any survive. */\nconst TIER_FLOOR: Partial<Record<ReferenceTier, number>> = {\n brandAsset: 1,\n adReference: 1,\n};\n\nconst slotBytes = (slot: ReferenceSlot): number =>\n slot.bytes ?? (slot.dataUrl ? dataUrlWireBytes(slot.dataUrl) : 0);\n\nconst totalBytes = (slots: ReferenceSlot[]): number =>\n slots.reduce((sum, slot) => sum + slotBytes(slot), 0);\n\n/** Sort into priority order, stable within a tier. */\nexport function orderByPriority(slots: ReferenceSlot[]): ReferenceSlot[] {\n return [...slots].sort(\n (a, b) => REFERENCE_TIERS.indexOf(a.tier) - REFERENCE_TIERS.indexOf(b.tier),\n );\n}\n\n/**\n * The order in which slots may be shed, first to go listed first.\n *\n * `user` and `source` never appear \u2014 they are the request. `logo` appears only\n * when `protectLogo` is false, and always last, so it outlives every brand\n * asset.\n */\nexport function shedOrder(\n slots: ReferenceSlot[],\n protectLogo: boolean,\n): ReferenceSlot[] {\n const of = (tier: ReferenceTier) => slots.filter((s) => s.tier === tier);\n const brandAssets = of(\"brandAsset\");\n const adReferences = of(\"adReference\");\n\n const surplus = (tier: ReferenceTier, list: ReferenceSlot[]) =>\n list.slice(TIER_FLOOR[tier] ?? 0).reverse();\n\n return [\n // Extras first, tail-first within each tier, lowest priority tier first.\n ...surplus(\"adReference\", adReferences),\n ...surplus(\"brandAsset\", brandAssets),\n // Then whole tiers, still lowest priority first.\n ...adReferences.slice(0, TIER_FLOOR.adReference ?? 0),\n ...brandAssets.slice(0, TIER_FLOOR.brandAsset ?? 0),\n // The logo outlives every brand asset, and on generate flows is never shed.\n ...(protectLogo ? [] : of(\"logo\")),\n ];\n}\n\nexport interface AllocateOptions {\n /** Count ceiling. Defaults to {@link MAX_REFERENCE_IMAGES}. */\n maxImages?: number;\n /** Byte ceiling for images. Defaults to {@link REFERENCE_WIRE_BUDGET_BYTES}. */\n maxBytes?: number;\n /** Non-image payload (prompt, schemas) to charge against `maxBytes`. */\n reservedBytes?: number;\n /**\n * False on surfaces where the source image outranks the logo \u2014 quick edit and\n * resize. On generate flows the logo is part of the protected core.\n */\n protectLogo?: boolean;\n}\n\nexport interface Allocation {\n kept: ReferenceSlot[];\n dropped: DroppedSlot[];\n bytes: number;\n /**\n * True when the protected core alone still exceeds the budget. The caller\n * should send anyway \u2014 a 413 with the user's own images is a better failure\n * than silently generating without them.\n */\n overBudget: boolean;\n}\n\n/**\n * Pure allocation: order, cap the user tier, then shed bottom-up until the\n * count and byte budgets are met. No compression \u2014 see `prepareReferences`.\n */\nexport function allocateReferences(\n slots: ReferenceSlot[],\n options: AllocateOptions = {},\n): Allocation {\n const maxImages = options.maxImages ?? MAX_REFERENCE_IMAGES;\n const maxBytes = options.maxBytes ?? REFERENCE_WIRE_BUDGET_BYTES;\n const budget = maxBytes - (options.reservedBytes ?? 0);\n const protectLogo = options.protectLogo ?? true;\n\n const dropped: DroppedSlot[] = [];\n\n // The user tier has its own hard cap, applied before anything else so a user\n // who attaches ten images doesn't consume the whole request.\n let kept: ReferenceSlot[] = [];\n let userSeen = 0;\n for (const slot of orderByPriority(slots)) {\n if (slot.tier === \"user\") {\n userSeen += 1;\n if (userSeen > MAX_USER_REFERENCE_IMAGES) {\n dropped.push({ tier: slot.tier, label: slot.label, reason: \"count\" });\n continue;\n }\n }\n kept.push(slot);\n }\n\n const shed = (reason: \"count\" | \"bytes\", stillOver: () => boolean) => {\n for (const victim of shedOrder(kept, protectLogo)) {\n if (!stillOver()) {\n return;\n }\n kept = kept.filter((s) => s !== victim);\n dropped.push({ tier: victim.tier, label: victim.label, reason });\n }\n };\n\n shed(\"count\", () => kept.length > maxImages);\n shed(\"bytes\", () => totalBytes(kept) > budget);\n\n return {\n kept,\n dropped,\n bytes: totalBytes(kept),\n overBudget: kept.length > maxImages || totalBytes(kept) > budget,\n };\n}\n\n/**\n * Compress everything, escalate on the largest offenders, and only then shed\n * slots. This is the entry point every image-generation surface should use.\n *\n * Placeholder slots (`dataUrl === \"\"`) are carried through untouched \u2014 the host\n * compresses those on its side against {@link HOST_ASSET_BYTE_RESERVATION}.\n */\nexport async function prepareReferences(\n slots: ReferenceSlot[],\n options: AllocateOptions = {},\n): Promise<Allocation> {\n const maxBytes = options.maxBytes ?? REFERENCE_WIRE_BUDGET_BYTES;\n const budget = maxBytes - (options.reservedBytes ?? 0);\n\n // Pass 1 \u2014 standard downscale of everything the SDK actually holds.\n const compressed: ReferenceSlot[] = await Promise.all(\n slots.map(async (slot) =>\n slot.dataUrl\n ? {\n ...slot,\n dataUrl: await shrinkImageDataUrl(slot.dataUrl),\n bytes: undefined,\n }\n : slot,\n ),\n );\n\n // Pass 2 \u2014 escalate on the biggest images until the whole set fits. Cheaper\n // for the user than losing a reference, so it runs before any shedding.\n for (const edge of ESCALATION_EDGES) {\n if (totalBytes(compressed) <= budget) {\n break;\n }\n const largestFirst = compressed\n .map((slot, index) => ({ slot, index }))\n .filter(({ slot }) => Boolean(slot.dataUrl))\n .sort((a, b) => slotBytes(b.slot) - slotBytes(a.slot));\n\n for (const { slot, index } of largestFirst) {\n if (totalBytes(compressed) <= budget) {\n break;\n }\n compressed[index] = {\n ...slot,\n dataUrl: await reencodeAtEdge(slot.dataUrl, edge),\n bytes: undefined,\n };\n }\n }\n\n // Pass 3 \u2014 still over: shed slots, bottom-up.\n return allocateReferences(compressed, options);\n}\n", "import { getCapabilities } from \"../modelCapabilities\";\n\nimport {\n HOST_ASSET_BYTE_RESERVATION,\n MAX_REFERENCE_IMAGES,\n MAX_USER_REFERENCE_IMAGES,\n prepareReferences,\n} from \"./referenceBudget\";\n\nimport type { ReferenceSlot, ReferenceTier } from \"./referenceBudget\";\n\n// Re-exported for the panels, which cap their own upload UI against it.\nexport { MAX_USER_REFERENCE_IMAGES };\n\n/**\n * Byte length of a string on the wire. `String.length` counts UTF-16 code\n * units, which under-reports every non-ASCII character \u2014 and the budget this\n * feeds is what keeps requests under Vercel's 4.5 MB limit.\n */\nconst utf8Bytes = (value: string | null | undefined): number => {\n if (!value) {\n return 0;\n }\n if (typeof TextEncoder !== \"undefined\") {\n return new TextEncoder().encode(value).length;\n }\n // eslint-disable-next-line no-control-regex\n return unescape(encodeURIComponent(value)).length;\n};\n\n/**\n * An exact `WIDTHxHEIGHT` request, as opposed to a tier like \"1K\"/\"2K\".\n * The two go in different request fields \u2014 see the Images API body below.\n */\nconst EXACT_PIXEL_SIZE = /^\\d+x\\d+$/;\n\n/** True for OpenAI's Images-API models, which are routed and shaped differently. */\nexport const isOpenAIImageModel = (modelId: string): boolean =>\n modelId.startsWith(\"openai/gpt-image\");\n\n/**\n * gpt-image-2 accepts an arbitrary `WIDTHxHEIGHT` resolution, not just the\n * handful of preset aspect ratios the Gemini-era models were limited to. Its\n * constraints (via OpenRouter's unified image API):\n *\n * - both edges a multiple of 16\n * - longest edge <= 3840px\n * - long edge / short edge <= 3\n * - total pixels between 655,360 and 8,294,400\n *\n * @see https://openrouter.ai/docs/guides/overview/multimodal/image-generation\n */\nconst PIXEL_STEP = 16;\nconst MAX_EDGE = 3840;\nconst MAX_EDGE_RATIO = 3;\nconst MIN_PIXELS = 655_360;\nconst MAX_PIXELS = 8_294_400;\n\n/** Aspect ratios the non-OpenAI image models accept. */\nconst PRESET_ASPECT_RATIOS = [\n { label: \"16:9\", value: 16 / 9 },\n { label: \"4:3\", value: 4 / 3 },\n { label: \"1:1\", value: 1 },\n { label: \"3:4\", value: 3 / 4 },\n { label: \"9:16\", value: 9 / 16 },\n] as const;\n\n/** Nearest preset ratio to `frameW / frameH`. */\nexport function nearestPresetAspectRatio(\n frameW: number,\n frameH: number,\n): string {\n const target = frameW / frameH;\n return PRESET_ASPECT_RATIOS.reduce((best, candidate) =>\n Math.abs(candidate.value - target) < Math.abs(best.value - target)\n ? candidate\n : best,\n ).label;\n}\n\n/**\n * Picks the output size for a frame.\n *\n * For gpt-image-* this returns an exact `\"1088x1360\"` size matching the frame's\n * real aspect ratio, so a 1080\u00D71350 (4:5) frame no longer comes back as a 3:4\n * image that has to be letterboxed to 1013\u00D71350. It travels in the request's\n * `size` field, NOT `resolution` \u2014 see the Images API body below. `aspectRatio`\n * is null in that case: an exact size is authoritative and OpenRouter 400s if a\n * ratio is sent alongside it (4:5 is not a valid preset anyway).\n *\n * Every other model keeps the preset-snapping behaviour it has always had.\n */\nexport function resolveImageOutputSize(\n modelId: string,\n frameW: number,\n frameH: number,\n): { aspectRatio: string | null; imageSize: string | null } {\n const usable =\n Number.isFinite(frameW) &&\n Number.isFinite(frameH) &&\n frameW > 0 &&\n frameH > 0;\n\n if (!usable) {\n return { aspectRatio: \"1:1\", imageSize: null };\n }\n\n if (!isOpenAIImageModel(modelId)) {\n return {\n aspectRatio: nearestPresetAspectRatio(frameW, frameH),\n imageSize: null,\n };\n }\n\n const target = frameW / frameH;\n\n // Ratios beyond 3:1 can't be expressed at all, so clamp to 3:1 \u2014 the widest\n // legal image for that frame. `target` below stays the frame's true ratio, so\n // the snap search still leans toward the widest candidate.\n const ratio = Math.max(frameW, frameH) / Math.min(frameW, frameH);\n const clamp = ratio > MAX_EDGE_RATIO ? MAX_EDGE_RATIO / ratio : 1;\n let w = frameW > frameH ? frameW * clamp : frameW;\n let h = frameW > frameH ? frameH : frameH * clamp;\n\n // Scale into the legal pixel/edge envelope, preserving the ratio.\n const edgeScale = Math.min(1, MAX_EDGE / Math.max(w, h));\n w *= edgeScale;\n h *= edgeScale;\n const pixels = w * h;\n const pixelScale =\n pixels < MIN_PIXELS\n ? Math.sqrt(MIN_PIXELS / pixels)\n : pixels > MAX_PIXELS\n ? Math.sqrt(MAX_PIXELS / pixels)\n : 1;\n w *= pixelScale;\n h *= pixelScale;\n\n // Both edges must land on a multiple of 16. Rounding each independently can\n // skew the ratio (1080\u00D71350 \u2192 1088\u00D71344, 1.2% off); trying all four\n // floor/ceil combinations finds 1088\u00D71360, which is exactly 4:5.\n const snap = (v: number, up: boolean) =>\n Math.max(\n PIXEL_STEP,\n (up ? Math.ceil(v / PIXEL_STEP) : Math.floor(v / PIXEL_STEP)) *\n PIXEL_STEP,\n );\n\n let best: { w: number; h: number } | null = null;\n let bestError = Infinity;\n for (const upW of [false, true]) {\n for (const upH of [false, true]) {\n const cw = snap(w, upW);\n const ch = snap(h, upH);\n const area = cw * ch;\n if (\n Math.max(cw, ch) > MAX_EDGE ||\n Math.max(cw, ch) / Math.min(cw, ch) > MAX_EDGE_RATIO ||\n area < MIN_PIXELS ||\n area > MAX_PIXELS\n ) {\n continue;\n }\n const error = Math.abs(cw / ch - target);\n if (error < bestError) {\n bestError = error;\n best = { w: cw, h: ch };\n }\n }\n }\n\n if (!best) {\n // No legal snap \u2014 let the API pick from the ratio presets instead of\n // sending a resolution it would reject.\n return {\n aspectRatio: nearestPresetAspectRatio(frameW, frameH),\n imageSize: null,\n };\n }\n\n return { aspectRatio: null, imageSize: `${best.w}x${best.h}` };\n}\n\n/**\n * Marks the brand block as advisory once the user has supplied references.\n * Without it the palette hex in that block outranks the reference image.\n */\nconst DEMOTED_BRAND_HEADER =\n \"BRAND DIRECTION \u2014 OPTIONAL DEFAULTS. Everything in this section is \" +\n \"subordinate to the user's instruction and the USER REFERENCE images above. \" +\n \"Apply it only where they are silent, and discard any part of it that \" +\n \"contradicts them \u2014 including colour values, typography, and layout.\";\n\n/**\n * Describes each attached reference image by its real 1-based position, and\n * states the precedence rule between them.\n *\n * This exists because attachment order alone does not steer the model \u2014 it\n * follows whichever image the prompt names. Previously only the brand assets\n * were named (\"the first reference image is the exact brand logo\"), so a\n * user-supplied reference sat in the array unmentioned and was ignored.\n *\n * When the user attaches anything, their references are the authority: the\n * brand kit, stored logo, and reference ads are explicitly demoted so a\n * contradiction resolves in the user's favour rather than the brand's.\n *\n * Returns \"\" when there is nothing worth numbering, so prompts that carry no\n * attachments are left byte-identical to before.\n */\nexport function buildReferenceManifest(\n userReferenceCount: number,\n brandLabels: string[],\n /** The brand logo survived the budget and is attached. */\n logoIsAttached = false,\n /** A source image (quick edit / resize) is attached below the user's refs. */\n hasSourceImage = false,\n): string {\n if (userReferenceCount === 0 && !hasSourceImage && brandLabels.length === 0) {\n return \"\";\n }\n\n const lines: string[] = [\n \"ATTACHED REFERENCE IMAGES (in the order supplied):\",\n ];\n\n let slot = 0;\n for (let i = 0; i < userReferenceCount; i++) {\n lines.push(\n ` ${++slot}. USER REFERENCE \u2014 supplied by the user for this specific request.`,\n );\n }\n if (hasSourceImage) {\n lines.push(\n ` ${++slot}. SOURCE IMAGE \u2014 the existing image being edited. Preserve it exactly except for the change the user asked for.`,\n );\n }\n brandLabels.forEach((label) => {\n lines.push(` ${++slot}. Brand kit asset \u2014 ${label}.`);\n });\n\n const hasUserAuthority = userReferenceCount > 0 || hasSourceImage;\n if (!hasUserAuthority) {\n return lines.join(\"\\n\");\n }\n\n const refWord =\n userReferenceCount === 0\n ? \"SOURCE IMAGE\"\n : userReferenceCount === 1\n ? \"USER REFERENCE image\"\n : \"USER REFERENCE images\";\n\n lines.push(\n \"\",\n `PRIORITY \u2014 ABSOLUTE: the user's written instruction and the ${refWord} above outrank every other input. Follow the user's instruction exactly and treat ${\n userReferenceCount === 0 ? \"it\" : \"them\"\n } as the governing reference for subject, composition, style, colour, and typography. Where they contradict the brand kit, the brand guidelines, or any brand reference ad \u2014 including instructions given earlier in this prompt \u2014 the user's instruction and references WIN and the brand direction is overruled. Apply brand direction only where the user's instruction and references are silent.`,\n );\n\n if (logoIsAttached) {\n lines.push(\n \"\",\n `LOGO \u2014 EXACT ARTWORK: the brand logo listed above is the ONLY logo permitted in this image. Wherever a logo appears, it must be that artwork reproduced pixel-for-pixel \u2014 no redrawing, simplification, restyling, or recolouring \u2014 and it replaces any logo, emblem, or wordmark visible in the other reference images. This is not overruled by the precedence rule above; the precedence rule governs subject, composition, and style, not the identity of the logo. If the user explicitly asked for no logo, honour that instead.`,\n );\n }\n\n return lines.join(\"\\n\");\n}\n\nexport async function callOpenRouterImageAPI(\n prompt: string,\n modelId: string,\n /**\n * Preset ratio label (\"4:5\" is NOT one). Pass null when `imageSize` carries an\n * exact `WIDTHxHEIGHT` resolution \u2014 that is authoritative, and sending a\n * conflicting (or unsupported) ratio alongside it only muddies the request.\n */\n aspectRatio: string | null,\n imageSize: string | null,\n textOutput: boolean,\n imageGenUrl: string,\n referenceImageDataUrl?: string | string[],\n signal?: AbortSignal,\n brandImages?: {\n logo?: string;\n fontPreview?: string;\n /** Optional second font preview (brand body typeface) shown as a reference. */\n fontPreviewBody?: string;\n /**\n * Fetchable URL of the real headline font file (.ttf/.otf). Only Sourceful\n * Riverflow v2+ consumes this \u2014 sent via `image_config.font_inputs` so the\n * model renders text in the actual brand typeface (not just a preview img).\n */\n fontUrl?: string;\n /** Text to render with `fontUrl` for the font_inputs entry. */\n fontText?: string;\n /**\n * Full brand identity context text (colors, typography, visual style, etc.)\n * prepended to the prompt so the model applies brand guidelines to the image.\n */\n brandContextText?: string;\n },\n options?: {\n /**\n * How to treat `referenceImageDataUrl`.\n *\n * \"user-priority\" \u2014 the references came from the end user. They are\n * attached FIRST, named individually in the reference manifest, and\n * explicitly overrule the brand kit, stored logo, and brand reference ads\n * on any contradiction. Used by the surfaces where the user picks the\n * reference themselves: image generation, quick edit, and canvas chat.\n *\n * \"legacy-positional\" (default) \u2014 the reference is a pipeline input rather\n * than a user choice (e.g. the auto-resize source ad), and the caller has\n * already written its own numbered reference list into the prompt assuming\n * brand assets come first. Preserves the original ordering and emits no\n * manifest, so those callers are unaffected by this option.\n */\n referenceMode?: \"user-priority\" | \"legacy-positional\";\n /**\n * The image being edited or resized. Ranks directly below the user's own\n * references and above the logo \u2014 on those surfaces the source IS the\n * subject, so it must never be shed, and the logo becomes droppable.\n */\n sourceImageDataUrl?: string;\n /**\n * Whether the logo is part of the protected core. True on generate flows\n * (the floor is 6 user references + the logo); false on quick edit and\n * resize, where the source image outranks it.\n */\n protectLogo?: boolean;\n /**\n * How many brand-bank assets / ad references the host proxy may append.\n * The SDK cannot see those images, so it reserves\n * {@link HOST_ASSET_BYTE_RESERVATION} of budget per slot and tells the host\n * how many survived; the host compresses each one to fit its reservation.\n */\n hostBrandAssetSlots?: number;\n hostAdReferenceSlots?: number;\n quality?: \"auto\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n background?: \"auto\" | \"opaque\" | \"transparent\";\n outputFormat?: \"png\" | \"webp\";\n },\n): Promise<string> {\n const referenceMode = options?.referenceMode ?? \"legacy-positional\";\n\n const userReferences = (\n Array.isArray(referenceImageDataUrl)\n ? referenceImageDataUrl\n : referenceImageDataUrl\n ? [referenceImageDataUrl]\n : []\n ).filter((url): url is string => Boolean(url && url.trim()));\n\n // \u2500\u2500 Ranked slots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // Everything the request could carry, in priority order. Nothing is dropped\n // for competing with anything else \u2014 competition is settled by this ordering\n // and by the precedence rules the manifest states. Slots are shed only when\n // the count or byte budget demands it, bottom-up, and only after compression\n // has been escalated. See utils/referenceBudget.\n const hostBrandAssetSlots = options?.hostBrandAssetSlots ?? 0;\n const hostAdReferenceSlots = options?.hostAdReferenceSlots ?? 0;\n\n const placeholder = (tier: ReferenceTier, label: string): ReferenceSlot => ({\n tier,\n dataUrl: \"\",\n label,\n bytes: HOST_ASSET_BYTE_RESERVATION,\n });\n\n const slots: ReferenceSlot[] = [\n ...userReferences.map(\n (dataUrl): ReferenceSlot => ({ tier: \"user\", dataUrl }),\n ),\n ...(options?.sourceImageDataUrl\n ? [\n {\n tier: \"source\" as const,\n dataUrl: options.sourceImageDataUrl,\n label: \"the image being edited\",\n },\n ]\n : []),\n ...(brandImages?.logo\n ? [\n {\n tier: \"logo\" as const,\n dataUrl: brandImages.logo,\n label: \"the exact brand logo graphic\",\n },\n ]\n : []),\n ...(brandImages?.fontPreview\n ? [\n {\n tier: \"brandAsset\" as const,\n dataUrl: brandImages.fontPreview,\n label: \"a brand font specimen (headline typeface)\",\n },\n ]\n : []),\n ...(brandImages?.fontPreviewBody\n ? [\n {\n tier: \"brandAsset\" as const,\n dataUrl: brandImages.fontPreviewBody,\n label: \"a brand font specimen (body typeface)\",\n },\n ]\n : []),\n ...Array.from({ length: hostBrandAssetSlots }, () =>\n placeholder(\"brandAsset\", \"a brand product/lifestyle photo\"),\n ),\n ...Array.from({ length: hostAdReferenceSlots }, () =>\n placeholder(\"adReference\", \"an existing ad creative or template\"),\n ),\n ];\n\n const allocation = await prepareReferences(slots, {\n protectLogo: options?.protectLogo ?? true,\n // The prompt, the brand context block, and the JSON envelope all ride along\n // in the same body, so they are charged against the image budget.\n reservedBytes: prompt.length + (brandImages?.brandContextText?.length ?? 0),\n });\n\n if (allocation.dropped.length > 0) {\n console.warn(\n `[imageApi] reference budget \u2014 dropped ${allocation.dropped\n .map((d) => `${d.tier}${d.label ? ` (${d.label})` : \"\"}:${d.reason}`)\n .join(\", \")}; kept ${allocation.kept.length} slot(s), ${Math.round(\n allocation.bytes / 1024,\n )} KB`,\n );\n }\n\n const keptOfTier = (tier: ReferenceTier) =>\n allocation.kept.filter((s) => s.tier === tier);\n\n // Placeholders stay in the array as *counts* only \u2014 the host fills them.\n const attached = allocation.kept.filter((s) => Boolean(s.dataUrl));\n const keptHostBrandAssets = keptOfTier(\"brandAsset\").filter(\n (s) => !s.dataUrl,\n ).length;\n const keptHostAdReferences = keptOfTier(\"adReference\").filter(\n (s) => !s.dataUrl,\n ).length;\n\n const keptUserReferenceCount = keptOfTier(\"user\").length;\n const keptSourceCount = keptOfTier(\"source\").length;\n\n // Legacy positional callers wrote their own numbered list into the prompt\n // assuming brand assets come first, so their ordering is preserved verbatim.\n const orderedReferences: string[] =\n referenceMode === \"legacy-positional\"\n ? [\n ...attached\n .filter((s) => s.tier !== \"user\" && s.tier !== \"source\")\n .map((s) => s.dataUrl),\n ...attached\n .filter((s) => s.tier === \"user\" || s.tier === \"source\")\n .map((s) => s.dataUrl),\n ]\n : attached.map((s) => s.dataUrl);\n\n const brandLabels =\n referenceMode === \"user-priority\"\n ? attached\n .filter((s) => s.tier !== \"user\" && s.tier !== \"source\")\n .map((s) => s.label)\n .filter((label): label is string => Boolean(label))\n : [];\n\n const logoIsAttached = keptOfTier(\"logo\").length > 0;\n\n const referenceManifest =\n referenceMode === \"user-priority\"\n ? buildReferenceManifest(\n keptUserReferenceCount,\n brandLabels,\n logoIsAttached,\n keptSourceCount > 0,\n )\n : \"\";\n\n // Drives the brand-block demotion below. A source image counts: on a quick\n // edit the existing artwork is the authority, not the brand palette.\n const userSuppliedReferences =\n referenceMode === \"user-priority\" &&\n (keptUserReferenceCount > 0 || keptSourceCount > 0);\n\n const referenceParts = orderedReferences.map((url) => ({\n type: \"image_url\",\n image_url: { url },\n }));\n\n const isGrok = modelId.startsWith(\"x-ai/\");\n // Sourceful Riverflow (v2+) \u2014 Gemini-style multimodal body, but additionally\n // accepts the real font file through image_config.font_inputs.\n const isSourceful = modelId.startsWith(\"sourceful/\");\n // OpenAI's dedicated Images API models (gpt-image-2, gpt-image-2.5-sunburst) 400\n // on /chat/completions \u2014 OpenRouter requires POSTing to /api/v1/images with\n // a { prompt, input_references } body instead of { messages }. The host\n // proxy (imageGenUrl) inspects `model` and routes to the right upstream URL;\n // this branch only needs to build the matching body shape.\n const isOpenAIImagesAPI = isOpenAIImageModel(modelId);\n\n // If the caller supplies brand identity context text, prepend it to the prompt\n // so the model applies brand colors, visual style, etc. The caller is\n // responsible for also appending any brand asset instructions (logo, font) to\n // the prompt itself \u2014 see brandContextUtils.buildImageGenSafePrompt.\n // Brand direction normally frames the prompt. But when the user supplied\n // references it moves BELOW their instruction and is relabelled as optional \u2014\n // leading with \"Colors: #7a1fa2\" made the model emit that exact hex instead of\n // the colour in the user's reference image.\n const brandContextText = brandImages?.brandContextText;\n const promptWithBrandContext = !brandContextText\n ? prompt\n : userSuppliedReferences\n ? `${prompt}\\n\\n---\\n\\n${DEMOTED_BRAND_HEADER}\\n\\n${brandContextText}`\n : `${brandContextText}\\n\\n---\\n\\n${prompt}`;\n\n // Manifest goes last: it is the authoritative statement of which attachment is\n // which, and it must be able to overrule the brand-asset instructions that\n // buildImageGenSafePrompt appended earlier in the prompt.\n const effectivePrompt = referenceManifest\n ? `${promptWithBrandContext}\\n\\n${referenceManifest}`\n : promptWithBrandContext;\n\n let body: Record<string, unknown>;\n\n if (isOpenAIImagesAPI) {\n // Images API takes reference images in `input_references`, not message\n // content parts, and has no `messages` array at all \u2014 text lives in a\n // top-level `prompt` string.\n body = {\n model: modelId,\n prompt: effectivePrompt,\n // Two mutually exclusive ways to ask for a size:\n //\n // \u2022 `size: \"1088x1360\"` \u2014 exact pixels. Authoritative, and OpenRouter\n // 400s if `resolution` or `aspect_ratio` is sent alongside it.\n // \u2022 `resolution: \"1K\" | \"2K\" | \u2026` \u2014 a tier, which combines with\n // `aspect_ratio`. `resolution` accepts ONLY that enum; passing\n // pixels to it is a ZodError.\n ...(imageSize && EXACT_PIXEL_SIZE.test(imageSize)\n ? { size: imageSize }\n : {\n ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),\n ...(imageSize ? { resolution: imageSize } : {}),\n }),\n ...(referenceParts.length > 0\n ? { input_references: referenceParts }\n : {}),\n ...(options?.quality ? { quality: options.quality } : {}),\n ...(options?.background ? { background: options.background } : {}),\n ...(options?.background === \"transparent\"\n ? { output_format: options.outputFormat ?? \"png\" }\n : {}),\n };\n } else if (isGrok) {\n // Grok image models use plain chat completions \u2014 no modalities/image_config.\n // For text-only prompts, content must be a plain string (Grok rejects single-item arrays).\n // When brand images are provided, build an array: [logo, fontPreview, userRef, text].\n const content: unknown =\n referenceParts.length > 0\n ? [...referenceParts, { type: \"text\", text: effectivePrompt }]\n : effectivePrompt;\n\n body = {\n model: modelId,\n messages: [{ role: \"user\", content }],\n stream: false,\n };\n } else {\n // Gemini / SeedDream via OpenRouter \u2014 requires modalities + image_config.\n const contentParts: unknown[] = [\n ...referenceParts,\n { type: \"text\", text: effectivePrompt },\n ];\n\n const imageConfig: Record<string, unknown> = {\n // These models only take preset ratios, so callers always supply one.\n aspect_ratio: aspectRatio ?? \"1:1\",\n };\n if (imageSize) {\n imageConfig.image_size = imageSize;\n }\n\n // Sourceful Riverflow v2+ \u2014 pass the actual font file so on-image text is\n // rendered in the real brand typeface. OpenRouter accepts up to 2 font\n // inputs ($0.03 each); we send the headline font only. font_url must be a\n // URL the provider can fetch directly (e.g. a presigned S3 URL).\n if (isSourceful && brandImages?.fontUrl) {\n const fontSampleText =\n brandImages.fontText ||\n \"AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz 0123456789\";\n imageConfig.font_inputs = [\n {\n font_url: brandImages.fontUrl,\n text: fontSampleText,\n },\n ];\n }\n\n body = {\n model: modelId,\n messages: [{ role: \"user\", content: contentParts }],\n modalities: textOutput ? [\"image\", \"text\"] : [\"image\"],\n stream: false,\n image_config: imageConfig,\n };\n }\n\n // Tell the host proxy how the reference array is laid out. The proxy appends\n // its own brand assets and numbers them in the prompt; without this it restarts\n // numbering at 1 and mislabels the user's references. Stripped by the proxy\n // before forwarding \u2014 OpenRouter validates its body strictly and would reject\n // an unknown top-level field.\n if (referenceMode === \"user-priority\" && referenceParts.length > 0) {\n body.x_reference_manifest = {\n userReferenceCount: keptUserReferenceCount,\n sourceReferenceCount: keptSourceCount,\n totalReferenceCount: referenceParts.length,\n logoAttached: logoIsAttached,\n // How many of its own assets the host may append, decided by the budget\n // above. The host compresses each one to maxHostAssetBytes so the\n // reservation the SDK made for it stays honest.\n brandAssetSlots: keptHostBrandAssets,\n adReferenceSlots: keptHostAdReferences,\n maxHostAssetBytes: HOST_ASSET_BYTE_RESERVATION,\n };\n }\n\n // Routed through the host proxy (imageGenUrl) so the provider key never lives\n // in the browser. The proxy injects the key, gates credits, and forwards to\n // OpenRouter transparently \u2014 the response shape is unchanged.\n const response = await fetch(imageGenUrl, {\n method: \"POST\",\n signal,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n let message = `OpenRouter API error ${response.status}`;\n try {\n const err = await response.json();\n if (err?.error?.message) {\n message = err.error.message;\n }\n } catch {\n /* ignore */\n }\n throw new Error(message);\n }\n\n const data = await response.json();\n\n // OpenAI Images API \u2192 image as base64 in data.data[0].b64_json\n const imagesApiResult = data?.data?.[0];\n if (imagesApiResult?.b64_json) {\n const mediaType = imagesApiResult.media_type ?? \"image/png\";\n return `data:${mediaType};base64,${imagesApiResult.b64_json}`;\n }\n\n // Gemini / SeedDream \u2192 image in message.images[].image_url.url\n const images = data?.choices?.[0]?.message?.images;\n if (images?.[0]?.image_url?.url) {\n return images[0].image_url.url;\n }\n\n // Grok \u2192 image as base64 data URL or https URL in message.content\n const content = data?.choices?.[0]?.message?.content;\n if (typeof content === \"string\" && content.length > 0) {\n return content;\n }\n if (Array.isArray(content)) {\n const imgPart = content.find(\n (p: { type?: string; image_url?: { url?: string } }) =>\n p?.type === \"image_url\" && p?.image_url?.url,\n );\n if (imgPart?.image_url?.url) {\n return imgPart.image_url.url;\n }\n }\n\n throw new Error(\"No image was returned by the API.\");\n}\n", "// ---------------------------------------------------------------------------\n// Module-level store: tracks which frames are currently being auto-resized\n// so AutoResizeShimmerLayer (rendered in App.tsx) can react to it.\n// ---------------------------------------------------------------------------\n\nexport type LoadingFrameInfo = {\n frameId: string;\n x: number;\n y: number;\n width: number;\n height: number;\n};\n\nexport type ResizeStep = {\n id: number;\n kind: \"agent\" | \"tool\";\n name: string;\n model?: string;\n status: \"running\" | \"done\" | \"error\";\n startedAt: number;\n finishedAt?: number;\n detail?: string;\n};\nexport type ResizeActivity = LoadingFrameInfo & {\n status: \"running\" | \"done\" | \"error\" | \"cancelled\";\n steps: ResizeStep[];\n cancel?: () => void;\n};\nlet activities: ResizeActivity[] = [];\nlet stepSequence = 0;\n\ntype Listener = () => void;\n\nlet loadingFrames: LoadingFrameInfo[] = [];\nconst listeners = new Set<Listener>();\n\nfunction notify() {\n listeners.forEach((fn) => fn());\n}\n\nexport const autoResizeStore = {\n startActivity(frame: LoadingFrameInfo, cancel?: () => void) {\n activities = [\n ...activities.filter((a) => a.frameId !== frame.frameId),\n { ...frame, status: \"running\", steps: [], cancel },\n ];\n // Keep active jobs, and a bounded history of completed output cards.\n const completed = activities\n .filter((a) => a.status !== \"running\")\n .slice(-20);\n activities = activities.filter(\n (a) => a.status === \"running\" || completed.includes(a),\n );\n notify();\n },\n startStep(\n frameId: string,\n kind: ResizeStep[\"kind\"],\n name: string,\n model?: string,\n ) {\n const id = ++stepSequence;\n const activity = activities.find((a) => a.frameId === frameId);\n activity?.steps.push({\n id,\n kind,\n name,\n model,\n status: \"running\",\n startedAt: Date.now(),\n });\n notify();\n return id;\n },\n endStep(\n frameId: string,\n id: number,\n status: \"done\" | \"error\",\n detail?: string,\n ) {\n const step = activities\n .find((a) => a.frameId === frameId)\n ?.steps.find((s) => s.id === id);\n if (step) {\n Object.assign(step, { status, detail, finishedAt: Date.now() });\n }\n notify();\n },\n finishActivity(frameId: string, status: ResizeActivity[\"status\"]) {\n const activity = activities.find((a) => a.frameId === frameId);\n if (activity) {\n activity.status = status;\n activity.cancel = undefined;\n }\n notify();\n },\n cancelActivity(frameId: string) {\n activities.find((a) => a.frameId === frameId)?.cancel?.();\n },\n dismissActivity(frameId: string) {\n activities = activities.filter(\n (a) => a.frameId !== frameId || a.status === \"running\",\n );\n notify();\n },\n getActivities(): ResizeActivity[] {\n return activities;\n },\n resetActivity() {\n activities.forEach((a) => a.cancel?.());\n activities = [];\n notify();\n },\n setLoadingFrames(frames: LoadingFrameInfo[]) {\n loadingFrames = [...frames];\n notify();\n },\n\n markComplete(frameId: string) {\n loadingFrames = loadingFrames.filter((f) => f.frameId !== frameId);\n notify();\n },\n\n clear() {\n loadingFrames = [];\n notify();\n },\n\n getLoadingFrames(): LoadingFrameInfo[] {\n return loadingFrames;\n },\n\n subscribe(fn: Listener): () => void {\n listeners.add(fn);\n return () => listeners.delete(fn);\n },\n};\n", "// ---------------------------------------------------------------------------\n// Background extension for Auto Resize\n//\n// When an ad is re-laid-out into a new aspect ratio, the foreground elements\n// are repositioned by the HTML path \u2014 but the background plate still has the\n// source's shape. Stretching it distorts; cover-cropping it throws pixels away.\n//\n// The right treatment depends on whether the plate is STATIONARY \u2014 whether\n// every crop of it looks statistically like every other crop:\n//\n// solid \u2192 emit a colour, no bitmap at all\n// texture \u2192 stationary (halftone, grain, noise, hatch): mirror-tile it.\n// Tiles to any dimension seamlessly and pixel-exactly. Sending\n// this through an image model is strictly worse \u2014 the model\n// redraws the pattern and gets the spacing subtly wrong.\n// panels \u2192 two regions split by one hard edge (the extremely common\n// \"dark half | brand-colour half\" ad layout), each stationary in\n// its own right. Re-proportion the split, then tile each side.\n// gradient \u2192 directional falloff: extend the edge rows/columns.\n// photo \u2192 NOT stationary (a scene with subjects). Tiling would repeat the\n// subject. This is the only case that needs generative outpaint.\n//\n// Classification is pure, local and cheap \u2014 no vision call. Everything up to\n// `photo` runs entirely on a canvas with zero network calls.\n//\n// The pure analysis functions take a plain {data,width,height} buffer so they\n// are unit-testable without a DOM; canvas access is confined to the thin I/O\n// helpers at the bottom.\n// ---------------------------------------------------------------------------\n\nexport type BgKind = \"solid\" | \"texture\" | \"panels\" | \"gradient\" | \"photo\";\n\nexport type PixelBuffer = {\n data: Uint8ClampedArray;\n width: number;\n height: number;\n};\n\nexport type Rect = { x: number; y: number; w: number; h: number };\n\n/** Where a panel boundary sits, as a fraction (0-1) along `axis`. */\nexport type BgSplit = { axis: \"x\" | \"y\"; at: number };\n\nexport type BgClassification = {\n kind: BgKind;\n /** Present only for `kind: \"panels\"`. */\n split?: BgSplit;\n /** Present for `kind: \"solid\"` \u2014 the plate's average colour as hex. */\n solidColor?: string;\n /** Per-region verdicts; length 2 when a split was found, else 1. */\n regions: RegionStats[];\n};\n\nexport type RegionStats = {\n rect: Rect;\n /** Mean luminance stdev within patches \u2014 how \"busy\" the region is. */\n textureEnergy: number;\n /** Spread of patch MEANS \u2014 how much the region changes across itself. */\n meanSpread: number;\n /** True when patch means march in one direction along x or y. */\n monotonic: boolean;\n meanColor: { r: number; g: number; b: number };\n verdict: \"solid\" | \"texture\" | \"gradient\" | \"photo\";\n};\n\n// --- Tunables --------------------------------------------------------------\n// All on a 0-255 scale. Chosen so a halftone dot field reads as `texture`\n// (its patches differ only in dot phase, so their MEANS are nearly identical\n// even though each patch is internally busy) while a photographic scene reads\n// as `photo` (patch means differ wildly between sky, water and subject).\n\n/** Below this patch-internal stdev the region has no texture worth keeping. */\nconst SOLID_TEXTURE_ENERGY = 4;\n/** Below this spread of patch means, a low-energy region is flat colour. */\nconst SOLID_MEAN_SPREAD = 6;\n/** Below this spread of patch means the region is stationary \u2192 tileable. */\nconst STATIONARY_MEAN_SPREAD = 20;\n/** A gradient must span at least this much to be worth calling a gradient. */\nconst GRADIENT_MIN_RANGE = 18;\n/** Fraction of steps that must move the same way to count as monotonic. */\nconst MONOTONIC_RATIO = 0.8;\n/** Adjacent-line colour distance that marks a hard panel boundary. */\nconst SPLIT_MIN_DELTA = 55;\n/** Ignore candidate splits closer than this to either end. */\nconst SPLIT_EDGE_MARGIN = 0.12;\n/** Patch grid resolution used for all region statistics. */\nconst PATCH_GRID = 8;\n\n// ---------------------------------------------------------------------------\n// Pure analysis\n// ---------------------------------------------------------------------------\n\nfunction clampInt(v: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, Math.round(v)));\n}\n\nfunction colorDistance(\n a: { r: number; g: number; b: number },\n b: { r: number; g: number; b: number },\n): number {\n return Math.sqrt((a.r - b.r) ** 2 + (a.g - b.g) ** 2 + (a.b - b.b) ** 2);\n}\n\nexport function toHex({\n r,\n g,\n b,\n}: {\n r: number;\n g: number;\n b: number;\n}): string {\n const h = (v: number) => clampInt(v, 0, 255).toString(16).padStart(2, \"0\");\n return `#${h(r)}${h(g)}${h(b)}`;\n}\n\n/** Mean RGB + mean luminance stdev over an axis-aligned rect. */\nfunction regionMeans(\n buf: PixelBuffer,\n rect: Rect,\n): { mean: { r: number; g: number; b: number }; lumaStdev: number } {\n const x0 = clampInt(rect.x, 0, buf.width);\n const y0 = clampInt(rect.y, 0, buf.height);\n const x1 = clampInt(rect.x + rect.w, 0, buf.width);\n const y1 = clampInt(rect.y + rect.h, 0, buf.height);\n\n let r = 0;\n let g = 0;\n let b = 0;\n let n = 0;\n const lumas: number[] = [];\n\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = (y * buf.width + x) * 4;\n const pr = buf.data[i];\n const pg = buf.data[i + 1];\n const pb = buf.data[i + 2];\n r += pr;\n g += pg;\n b += pb;\n lumas.push(0.299 * pr + 0.587 * pg + 0.114 * pb);\n n++;\n }\n }\n\n if (n === 0) {\n return { mean: { r: 0, g: 0, b: 0 }, lumaStdev: 0 };\n }\n\n const mean = { r: r / n, g: g / n, b: b / n };\n const lumaMean = lumas.reduce((s, v) => s + v, 0) / lumas.length;\n const variance =\n lumas.reduce((s, v) => s + (v - lumaMean) ** 2, 0) / lumas.length;\n\n return { mean, lumaStdev: Math.sqrt(variance) };\n}\n\n/** Mean colour of every column (axis \"x\") or row (axis \"y\"). */\nfunction lineMeans(\n buf: PixelBuffer,\n axis: \"x\" | \"y\",\n): { r: number; g: number; b: number }[] {\n const outer = axis === \"x\" ? buf.width : buf.height;\n const inner = axis === \"x\" ? buf.height : buf.width;\n const out: { r: number; g: number; b: number }[] = [];\n\n for (let o = 0; o < outer; o++) {\n let r = 0;\n let g = 0;\n let b = 0;\n for (let i = 0; i < inner; i++) {\n const x = axis === \"x\" ? o : i;\n const y = axis === \"x\" ? i : o;\n const idx = (y * buf.width + x) * 4;\n r += buf.data[idx];\n g += buf.data[idx + 1];\n b += buf.data[idx + 2];\n }\n out.push({ r: r / inner, g: g / inner, b: b / inner });\n }\n\n return out;\n}\n\n/**\n * Finds the single strongest panel boundary, if any. Deliberately detects at\n * most ONE split \u2014 that covers the dominant real-world ad layout (a two-panel\n * composition) without overfitting to noise in busier plates.\n */\nexport function detectSplit(buf: PixelBuffer): BgSplit | null {\n let best: { axis: \"x\" | \"y\"; index: number; delta: number } | null = null;\n\n for (const axis of [\"x\", \"y\"] as const) {\n const means = lineMeans(buf, axis);\n const len = means.length;\n if (len < 8) {\n continue;\n }\n\n const lo = Math.floor(len * SPLIT_EDGE_MARGIN);\n const hi = Math.ceil(len * (1 - SPLIT_EDGE_MARGIN));\n\n for (let i = lo; i < hi - 1; i++) {\n const delta = colorDistance(means[i], means[i + 1]);\n if (delta >= SPLIT_MIN_DELTA && (!best || delta > best.delta)) {\n best = { axis, index: i + 1, delta };\n }\n }\n }\n\n if (!best) {\n return null;\n }\n\n const len = best.axis === \"x\" ? buf.width : buf.height;\n return { axis: best.axis, at: best.index / len };\n}\n\n/** Patch-grid statistics for one region, and the verdict they imply. */\nexport function analyzeRegion(buf: PixelBuffer, rect: Rect): RegionStats {\n const patchW = rect.w / PATCH_GRID;\n const patchH = rect.h / PATCH_GRID;\n\n const means: { r: number; g: number; b: number }[] = [];\n const energies: number[] = [];\n\n for (let gy = 0; gy < PATCH_GRID; gy++) {\n for (let gx = 0; gx < PATCH_GRID; gx++) {\n const { mean, lumaStdev } = regionMeans(buf, {\n x: rect.x + gx * patchW,\n y: rect.y + gy * patchH,\n w: patchW,\n h: patchH,\n });\n means.push(mean);\n energies.push(lumaStdev);\n }\n }\n\n const textureEnergy = energies.reduce((s, v) => s + v, 0) / energies.length;\n\n const overall = means.reduce(\n (acc, m) => ({ r: acc.r + m.r, g: acc.g + m.g, b: acc.b + m.b }),\n { r: 0, g: 0, b: 0 },\n );\n const meanColor = {\n r: overall.r / means.length,\n g: overall.g / means.length,\n b: overall.b / means.length,\n };\n\n // How far the busiest patch mean sits from the region mean. Stationary\n // textures score low here even when textureEnergy is high \u2014 that is exactly\n // the signal that separates a halftone field from a photograph.\n const meanSpread = Math.max(...means.map((m) => colorDistance(m, meanColor)));\n\n const monotonic = isMonotonic(means);\n\n let verdict: RegionStats[\"verdict\"];\n if (textureEnergy < SOLID_TEXTURE_ENERGY && meanSpread < SOLID_MEAN_SPREAD) {\n verdict = \"solid\";\n } else if (meanSpread < STATIONARY_MEAN_SPREAD) {\n verdict = \"texture\";\n } else if (monotonic) {\n verdict = \"gradient\";\n } else {\n verdict = \"photo\";\n }\n\n return { rect, textureEnergy, meanSpread, monotonic, meanColor, verdict };\n}\n\n/**\n * True when patch luminance marches consistently in one direction across the\n * grid, along either axis, over a meaningful range. Strict on purpose: a\n * photograph that happens to darken downward should still read as `photo`,\n * because edge-extending a scene looks worse than outpainting it.\n */\nfunction isMonotonic(means: { r: number; g: number; b: number }[]): boolean {\n const luma = (m: { r: number; g: number; b: number }) =>\n 0.299 * m.r + 0.587 * m.g + 0.114 * m.b;\n\n for (const axis of [\"x\", \"y\"] as const) {\n const series: number[] = [];\n for (let a = 0; a < PATCH_GRID; a++) {\n let sum = 0;\n for (let b = 0; b < PATCH_GRID; b++) {\n const idx = axis === \"x\" ? b * PATCH_GRID + a : a * PATCH_GRID + b;\n sum += luma(means[idx]);\n }\n series.push(sum / PATCH_GRID);\n }\n\n const range = Math.max(...series) - Math.min(...series);\n if (range < GRADIENT_MIN_RANGE) {\n continue;\n }\n\n let up = 0;\n let down = 0;\n for (let i = 1; i < series.length; i++) {\n if (series[i] > series[i - 1]) {\n up++;\n } else if (series[i] < series[i - 1]) {\n down++;\n }\n }\n const steps = series.length - 1;\n if (Math.max(up, down) / steps >= MONOTONIC_RATIO) {\n return true;\n }\n }\n\n return false;\n}\n\n/** Full classification: split detection, then per-region analysis. */\nexport function classifyBackground(buf: PixelBuffer): BgClassification {\n const split = detectSplit(buf);\n\n if (!split) {\n const stats = analyzeRegion(buf, {\n x: 0,\n y: 0,\n w: buf.width,\n h: buf.height,\n });\n return {\n kind: stats.verdict,\n solidColor:\n stats.verdict === \"solid\" ? toHex(stats.meanColor) : undefined,\n regions: [stats],\n };\n }\n\n const rects: Rect[] =\n split.axis === \"x\"\n ? [\n { x: 0, y: 0, w: buf.width * split.at, h: buf.height },\n {\n x: buf.width * split.at,\n y: 0,\n w: buf.width * (1 - split.at),\n h: buf.height,\n },\n ]\n : [\n { x: 0, y: 0, w: buf.width, h: buf.height * split.at },\n {\n x: 0,\n y: buf.height * split.at,\n w: buf.width,\n h: buf.height * (1 - split.at),\n },\n ];\n\n const regions = rects.map((r) => analyzeRegion(buf, r));\n\n // A split only helps if BOTH sides can be extended without a model. If\n // either half is photographic, treat the whole plate as a photo \u2014 half a\n // tiled background next to half an outpainted one looks worse than either.\n const extendable = regions.every(\n (r) => r.verdict === \"solid\" || r.verdict === \"texture\",\n );\n\n if (!extendable) {\n const whole = analyzeRegion(buf, {\n x: 0,\n y: 0,\n w: buf.width,\n h: buf.height,\n });\n return {\n kind: whole.verdict === \"solid\" ? \"texture\" : whole.verdict,\n regions,\n };\n }\n\n return { kind: \"panels\", split, regions };\n}\n\n// ---------------------------------------------------------------------------\n// Canvas I/O\n// ---------------------------------------------------------------------------\n\nfunction makeCanvas(w: number, h: number): HTMLCanvasElement {\n const c = document.createElement(\"canvas\");\n c.width = Math.max(1, Math.round(w));\n c.height = Math.max(1, Math.round(h));\n return c;\n}\n\nexport async function loadImageElement(\n dataUrl: string,\n): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = () => resolve(img);\n img.onerror = () => reject(new Error(\"Failed to load background image\"));\n img.src = dataUrl;\n });\n}\n\n/**\n * Reads an image into a pixel buffer, downscaled to at most `maxSide` on its\n * long edge. Classification only needs coarse statistics, and a 4K plate would\n * otherwise cost tens of megabytes of ImageData for no extra accuracy.\n */\nexport async function imageToPixelBuffer(\n dataUrl: string,\n maxSide = 512,\n): Promise<PixelBuffer> {\n const img = await loadImageElement(dataUrl);\n const scale = Math.min(1, maxSide / Math.max(img.width, img.height));\n const w = Math.max(1, Math.round(img.width * scale));\n const h = Math.max(1, Math.round(img.height * scale));\n\n const canvas = makeCanvas(w, h);\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) {\n throw new Error(\"2D canvas context unavailable\");\n }\n ctx.drawImage(img, 0, 0, w, h);\n\n const imageData = ctx.getImageData(0, 0, w, h);\n return { data: imageData.data, width: w, height: h };\n}\n\n/**\n * Fills `dst` by repeating `srcRect` with alternating mirror flips, so tile\n * seams always meet their own reflection and disappear. Correct for any\n * stationary texture regardless of its period.\n */\nfunction mirrorTile(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n srcRect: Rect,\n dst: Rect,\n): void {\n const tw = Math.max(1, srcRect.w);\n const th = Math.max(1, srcRect.h);\n\n const cols = Math.ceil(dst.w / tw);\n const rows = Math.ceil(dst.h / th);\n\n ctx.save();\n ctx.beginPath();\n ctx.rect(dst.x, dst.y, dst.w, dst.h);\n ctx.clip();\n\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const flipX = col % 2 === 1;\n const flipY = row % 2 === 1;\n\n ctx.save();\n ctx.translate(\n dst.x + col * tw + (flipX ? tw : 0),\n dst.y + row * th + (flipY ? th : 0),\n );\n ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);\n ctx.drawImage(\n img,\n srcRect.x,\n srcRect.y,\n srcRect.w,\n srcRect.h,\n 0,\n 0,\n tw,\n th,\n );\n ctx.restore();\n }\n }\n\n ctx.restore();\n}\n\n/**\n * Draws the plate scaled to cover the target's SHORT axis, then stretches its\n * outermost edge strips to fill the remaining space. Keeps a directional\n * gradient reading correctly instead of squashing it.\n */\nfunction edgeExtend(\n ctx: CanvasRenderingContext2D,\n img: HTMLImageElement,\n tgtW: number,\n tgtH: number,\n): void {\n const scale = Math.min(tgtW / img.width, tgtH / img.height);\n const coreW = img.width * scale;\n const coreH = img.height * scale;\n const coreX = (tgtW - coreW) / 2;\n const coreY = (tgtH - coreH) / 2;\n\n const STRIP = Math.max(1, Math.round(Math.min(img.width, img.height) * 0.04));\n\n // Left / right\n if (coreX > 0) {\n ctx.drawImage(img, 0, 0, STRIP, img.height, 0, coreY, coreX, coreH);\n ctx.drawImage(\n img,\n img.width - STRIP,\n 0,\n STRIP,\n img.height,\n coreX + coreW,\n coreY,\n tgtW - (coreX + coreW),\n coreH,\n );\n }\n // Top / bottom (full width, so the corners get covered too)\n if (coreY > 0) {\n ctx.drawImage(img, 0, 0, img.width, STRIP, 0, 0, tgtW, coreY);\n ctx.drawImage(\n img,\n 0,\n img.height - STRIP,\n img.width,\n STRIP,\n 0,\n coreY + coreH,\n tgtW,\n tgtH - (coreY + coreH),\n );\n }\n\n ctx.drawImage(img, coreX, coreY, coreW, coreH);\n}\n\n/** Cover-scale: fill the target completely, cropping the overflow. */\nfunction drawCover(\n ctx: CanvasRenderingContext2D,\n img: HTMLImageElement,\n tgtW: number,\n tgtH: number,\n): void {\n const scale = Math.max(tgtW / img.width, tgtH / img.height);\n const w = img.width * scale;\n const h = img.height * scale;\n ctx.drawImage(img, (tgtW - w) / 2, (tgtH - h) / 2, w, h);\n}\n\n// ---------------------------------------------------------------------------\n// Entry point\n// ---------------------------------------------------------------------------\n\nexport type BackgroundSource =\n | { type: \"solid\"; color: string; elementId?: string }\n | { type: \"image\"; dataUrl: string; elementId?: string }\n | { type: \"none\" };\n\nexport type BackgroundResult =\n | { kind: \"none\" }\n | { kind: \"solid\"; color: string }\n | { kind: \"image\"; dataUrl: string; strategy: BgKind | \"scaled\" };\n\nexport type BuildBackgroundOpts = {\n /**\n * Called ONLY for photographic plates that cannot be extended locally.\n * Supplied by the caller so this module stays free of network concerns and\n * fully unit-testable. Returning null falls back to edge-extension.\n */\n outpaint?: (args: {\n dataUrl: string;\n targetWidth: number;\n targetHeight: number;\n }) => Promise<string | null>;\n /** When false, skip analysis and just cover-scale (small AR delta). */\n needsExtension: boolean;\n onProgress?: (phase: string) => void;\n};\n\n/**\n * Produces a background sized EXACTLY to the target frame. The caller inserts\n * the result 1:1 \u2014 no cover-crop afterwards, unlike the old image-gen path\n * where the model returned approximate dimensions.\n */\nexport async function buildTargetBackground(\n bg: BackgroundSource,\n tgtW: number,\n tgtH: number,\n opts: BuildBackgroundOpts,\n): Promise<BackgroundResult> {\n if (bg.type === \"none\") {\n return { kind: \"none\" };\n }\n if (bg.type === \"solid\") {\n return { kind: \"solid\", color: bg.color };\n }\n\n const img = await loadImageElement(bg.dataUrl);\n\n // Aspect delta is small enough that a plain cover-scale loses nothing.\n if (!opts.needsExtension) {\n const canvas = makeCanvas(tgtW, tgtH);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"2D canvas context unavailable\");\n }\n drawCover(ctx, img, tgtW, tgtH);\n return {\n kind: \"image\",\n dataUrl: canvas.toDataURL(\"image/png\"),\n strategy: \"scaled\",\n };\n }\n\n opts.onProgress?.(\"Analysing background\u2026\");\n const buf = await imageToPixelBuffer(bg.dataUrl);\n const classification = classifyBackground(buf);\n\n // Scale factor from the downscaled analysis buffer back to real pixels.\n const sx = img.width / buf.width;\n const sy = img.height / buf.height;\n\n if (classification.kind === \"solid\" && classification.solidColor) {\n return { kind: \"solid\", color: classification.solidColor };\n }\n\n const canvas = makeCanvas(tgtW, tgtH);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"2D canvas context unavailable\");\n }\n\n if (classification.kind === \"texture\") {\n opts.onProgress?.(\"Tiling background texture\u2026\");\n // Sample from the middle of the plate \u2014 edges often carry vignetting or\n // a stray sliver of an adjacent element that would then repeat.\n const patch: Rect = {\n x: img.width * 0.25,\n y: img.height * 0.25,\n w: img.width * 0.5,\n h: img.height * 0.5,\n };\n mirrorTile(ctx, img, patch, { x: 0, y: 0, w: tgtW, h: tgtH });\n return {\n kind: \"image\",\n dataUrl: canvas.toDataURL(\"image/png\"),\n strategy: \"texture\",\n };\n }\n\n if (classification.kind === \"panels\" && classification.split) {\n opts.onProgress?.(\"Rebuilding background panels\u2026\");\n const { axis, at } = classification.split;\n\n for (let i = 0; i < 2; i++) {\n const region = classification.regions[i];\n // Region rect is in analysis-buffer space; lift it to real pixels.\n const srcRect: Rect = {\n x: region.rect.x * sx,\n y: region.rect.y * sy,\n w: region.rect.w * sx,\n h: region.rect.h * sy,\n };\n\n const dst: Rect =\n axis === \"x\"\n ? {\n x: i === 0 ? 0 : tgtW * at,\n y: 0,\n w: i === 0 ? tgtW * at : tgtW * (1 - at),\n h: tgtH,\n }\n : {\n x: 0,\n y: i === 0 ? 0 : tgtH * at,\n w: tgtW,\n h: i === 0 ? tgtH * at : tgtH * (1 - at),\n };\n\n if (region.verdict === \"solid\") {\n ctx.fillStyle = toHex(region.meanColor);\n ctx.fillRect(dst.x, dst.y, dst.w, dst.h);\n } else {\n // Inset the sample so the hard panel boundary itself never tiles.\n const inset = 0.15;\n mirrorTile(\n ctx,\n img,\n {\n x: srcRect.x + srcRect.w * inset,\n y: srcRect.y + srcRect.h * inset,\n w: srcRect.w * (1 - inset * 2),\n h: srcRect.h * (1 - inset * 2),\n },\n dst,\n );\n }\n }\n\n return {\n kind: \"image\",\n dataUrl: canvas.toDataURL(\"image/png\"),\n strategy: \"panels\",\n };\n }\n\n if (classification.kind === \"photo\") {\n // Before spending a generation call: if the plate already has more pixels\n // than the target box needs, the new aspect ratio can often be satisfied\n // by re-cropping real pixels instead of inventing any.\n if (img.width >= tgtW && img.height >= tgtH) {\n opts.onProgress?.(\"Re-cropping background\u2026\");\n drawCover(ctx, img, tgtW, tgtH);\n return {\n kind: \"image\",\n dataUrl: canvas.toDataURL(\"image/png\"),\n strategy: \"scaled\",\n };\n }\n\n if (opts.outpaint) {\n opts.onProgress?.(\"Extending background\u2026\");\n try {\n const out = await opts.outpaint({\n dataUrl: bg.dataUrl,\n targetWidth: tgtW,\n targetHeight: tgtH,\n });\n if (out) {\n const outImg = await loadImageElement(out);\n drawCover(ctx, outImg, tgtW, tgtH);\n return {\n kind: \"image\",\n dataUrl: canvas.toDataURL(\"image/png\"),\n strategy: \"photo\",\n };\n }\n } catch {\n // Non-fatal \u2014 fall through to edge-extension below.\n }\n }\n }\n\n opts.onProgress?.(\"Extending background edges\u2026\");\n edgeExtend(ctx, img, tgtW, tgtH);\n return {\n kind: \"image\",\n dataUrl: canvas.toDataURL(\"image/png\"),\n strategy: \"gradient\",\n };\n}\n", "// Auto Resize public entry point and legacy geometry helpers.\n// runAutoResize delegates to nativeResizeAgent: Grok vision labels, native\n// ID-based patches, deterministic fitting, staged rendering and final review.\n// Historical HTML/image generation helpers below remain unwired.\n\nimport { nanoid } from \"nanoid\";\n\nimport {\n newFrameElement,\n newElement,\n newTextElement,\n newImageElement,\n getFrameChildren,\n syncMovedIndices,\n styledTextToHtml,\n} from \"@orangecatai/element\";\n\nimport { FRAME_STYLE, arrayToMap } from \"@orangecatai/common\";\n\nimport { getMimeTypeFromDataURL } from \"../../data/blob\";\nimport {\n callOpenRouterImageAPI,\n resolveImageOutputSize,\n} from \"../../utils/imageApi\";\nimport { htmlToExcalidrawElements } from \"../ai-chat/htmlToExcalidraw\";\n\nimport {\n runReviewerAgent,\n type ReviewerFeedback,\n} from \"../ai-chat/reviewerAgent\";\nimport { exportToCanvas } from \"../../scene/export\";\n\nimport {\n buildTargetBackground,\n type BackgroundResult,\n} from \"./backgroundExtend\";\n\nimport type { IMAGE_MIME_TYPES } from \"@orangecatai/common\";\nimport type { ValueOf } from \"@orangecatai/common/utility-types\";\nimport type {\n ExcalidrawElement,\n ExcalidrawFrameLikeElement,\n FileId,\n OrderedExcalidrawElement,\n} from \"@orangecatai/element/types\";\nimport type { AppClassProperties, DataURL } from \"../../types\";\nimport type { BinaryFiles } from \"../../types\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEBUG_AUTO_RESIZE = false;\nconst debug = (...args: unknown[]) => {\n if (DEBUG_AUTO_RESIZE) {\n // eslint-disable-next-line no-console\n console.log(...args);\n }\n};\n\n// Fallback image model for every auto-resize generation call when the host does\n// not pass `agentImageModel`. Hosts should pass the prop \u2014 this only keeps the\n// SDK usable standalone.\n// Fallback background-outpaint model when the host does not pass\n// `outpaintModel`. Deliberately Gemini, NOT gpt-image-2: gpt-image-2 is\n// generation-only on OpenRouter (404s on chat/completions) and its ratio enum\n// stops at 3:1 / 1:3, so it cannot produce the 4:1 and 1:8 strip formats this\n// path exists to serve. Hosts should still pass the prop.\nconst DEFAULT_BG_REGEN_MODEL = \"google/gemini-3.1-flash-lite-image\";\n\n// Fallback layout model when the host does not pass `chatModel`.\nconst DEFAULT_CHAT_MODEL = \"x-ai/grok-4.6\";\n\n// Max reviewer critique rounds per dimension\nconst MAX_REVIEW_ROUNDS = 2;\n\n// All aspect ratios supported by Gemini 3.1 Flash Image\nconst GEMINI_RATIOS: Record<string, number> = {\n \"21:9\": 21 / 9,\n \"16:9\": 16 / 9,\n \"4:3\": 4 / 3,\n \"3:2\": 3 / 2,\n \"1:1\": 1,\n \"9:16\": 9 / 16,\n \"3:4\": 3 / 4,\n \"2:3\": 2 / 3,\n \"5:4\": 5 / 4,\n \"4:5\": 4 / 5,\n \"4:1\": 4 / 1,\n \"1:4\": 1 / 4,\n \"8:1\": 8 / 1,\n \"1:8\": 1 / 8,\n};\n\n// AR diff tolerance below which bg regen is skipped\nconst AR_DIFF_TOLERANCE = 0.12;\n// Crop-loss threshold above which bg regen triggers\nconst CROP_LOSS_THRESHOLD = 0.35;\n\n// Log-space AR delta above which a target dimension is routed to the host's\n// Tier-2 custom-resize pipeline (`customResizeUrl`) instead of the standard\n// generate+cover-crop flow (`fitImageToFrame`), which risks cutting\n// foreground elements at extreme deltas. Mirrors BIG_DELTA_THRESHOLD in the\n// host's lib/custom-resize/planner.ts.\nconst CUSTOM_RESIZE_DELTA_THRESHOLD = 0.2;\n\n// Absolute AR beyond which we always route to Tier-2, even for ratios the\n// model \"natively supports\" (4:1, 1:4, 8:1, 1:8). In practice the single-shot\n// generate+cover-crop flow duplicates/tiles elements to fill the extra space\n// at these extremes regardless of native support \u2014 only the verified Tier-2\n// pipeline (detect \u2192 generate \u2192 verify, with a duplication check) catches it.\nconst EXTREME_AR_THRESHOLD = 4;\n\nfunction ratioLogDelta(a: number, b: number): number {\n return Math.abs(Math.log(a / b));\n}\n\n/**\n * True when (targetW, targetH)'s aspect ratio is far from every ratio the\n * generation model natively supports, OR is an extreme ultra-wide/tall ratio\n * prone to element duplication regardless of native support \u2014 routes this\n * dimension to `customResizeUrl` when the host has provided one.\n */\nexport function isBigAspectRatioDelta(\n targetW: number,\n targetH: number,\n): boolean {\n const targetAR = targetW / targetH;\n if (\n targetAR >= EXTREME_AR_THRESHOLD ||\n targetAR <= 1 / EXTREME_AR_THRESHOLD\n ) {\n return true;\n }\n let minDelta = Infinity;\n for (const value of Object.values(GEMINI_RATIOS)) {\n const delta = ratioLogDelta(value, targetAR);\n if (delta < minDelta) {\n minDelta = delta;\n }\n }\n return minDelta > CUSTOM_RESIZE_DELTA_THRESHOLD;\n}\n\n/** Calls the host's Tier-2 custom-resize proxy for a single big-delta dimension. */\nasync function callCustomResizeProxy(\n customResizeUrl: string,\n sourceImageDataUrl: string,\n targetWidth: number,\n targetHeight: number,\n signal?: AbortSignal,\n): Promise<string> {\n const res = await fetch(customResizeUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n imageDataUrl: sourceImageDataUrl,\n targetWidth,\n targetHeight,\n }),\n signal,\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(\n `Custom resize failed ${res.status}: ${body.slice(0, 300)}`,\n );\n }\n\n const data = await res.json();\n if (typeof data?.dataUrl !== \"string\") {\n throw new Error(\"Custom resize response missing dataUrl\");\n }\n return data.dataUrl;\n}\n\n/**\n * Calls the host's Tier-1 element-aware crop proxy: given the image already\n * generated at the nearest native ratio, the host detects element positions\n * and picks a crop window that avoids cutting critical/important content\n * (escalating to Tier 2 internally if no safe crop exists).\n */\nasync function callTier1CropProxy(\n tier1CropUrl: string,\n generatedImageDataUrl: string,\n sourceImageDataUrl: string,\n targetWidth: number,\n targetHeight: number,\n signal?: AbortSignal,\n): Promise<string> {\n const res = await fetch(tier1CropUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n generatedImageDataUrl,\n sourceImageDataUrl,\n targetWidth,\n targetHeight,\n }),\n signal,\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`Tier-1 crop failed ${res.status}: ${body.slice(0, 300)}`);\n }\n\n const data = await res.json();\n if (typeof data?.dataUrl !== \"string\") {\n throw new Error(\"Tier-1 crop response missing dataUrl\");\n }\n return data.dataUrl;\n}\n\n// Cover tolerance for \"does this rect cover the full frame?\"\nconst COVER_TOLERANCE = 12;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type TargetDimension = {\n width: number;\n height: number;\n label?: string;\n};\n\nexport type DimensionResult =\n | { status: \"ok\"; frameId: string }\n | { status: \"error\"; error: string };\n\nexport type ProgressCallback = (\n dimensionIndex: number,\n total: number,\n phase: \"bg-regen\" | \"layout\" | \"inserting\" | \"done\" | \"error\",\n label: string,\n) => void;\n\ntype ForegroundEl =\n | {\n kind: \"text\";\n elementId: string;\n slotType?: string;\n relX: number;\n relY: number;\n width: number;\n height: number;\n text: string;\n /** inline markup for per-run styles, when the text carries any */\n contentHtml?: string;\n fontSize: number;\n color: string;\n textAlign: string;\n fontWeight: string;\n opacity: number;\n }\n | {\n kind: \"image\";\n elementId: string;\n slotType?: string;\n relX: number;\n relY: number;\n width: number;\n height: number;\n dataUrl: string;\n opacity: number;\n }\n | {\n kind: \"rect\";\n elementId: string;\n slotType?: string;\n relX: number;\n relY: number;\n width: number;\n height: number;\n backgroundColor: string;\n borderRadius: number;\n opacity: number;\n };\n\ntype SourceFrameInfo = {\n frame: ExcalidrawFrameLikeElement;\n background:\n | { type: \"solid\"; color: string; elementId?: string }\n | { type: \"image\"; dataUrl: string; elementId?: string }\n | { type: \"none\" };\n foreground: ForegroundEl[];\n};\n\n// ---------------------------------------------------------------------------\n// Frame analysis\n// ---------------------------------------------------------------------------\n\nfunction coversFrame(\n el: ExcalidrawElement,\n frame: ExcalidrawFrameLikeElement,\n): boolean {\n return (\n Math.abs(el.x - frame.x) < COVER_TOLERANCE &&\n Math.abs(el.y - frame.y) < COVER_TOLERANCE &&\n Math.abs(el.width - frame.width) < COVER_TOLERANCE &&\n Math.abs(el.height - frame.height) < COVER_TOLERANCE\n );\n}\n\nexport function extractFrameInfo(\n elements: readonly ExcalidrawElement[],\n frame: ExcalidrawFrameLikeElement,\n files: BinaryFiles,\n): SourceFrameInfo {\n const children = getFrameChildren(elements, frame.id);\n\n // Sort by z-order (position in elements array \u2014 lower index = further back)\n const sorted = [...children].sort(\n (a, b) =>\n elements.indexOf(a as ExcalidrawElement) -\n elements.indexOf(b as ExcalidrawElement),\n );\n\n let background: SourceFrameInfo[\"background\"] = { type: \"none\" };\n const foreground: ForegroundEl[] = [];\n\n for (let i = 0; i < sorted.length; i++) {\n const el = sorted[i];\n const isFirst = i === 0;\n // \"Make editable\" writes `decomposeType`; the (now-retired) vision tagger\n // wrote `slotType`. Both name the same slots \u2014 `product`, `logo`, `cta` \u2014\n // and downstream logic (enforceMinProductBox, product preservation) keys\n // off `slotType`. Without the fallback, every decomposed frame reports\n // `undefined` and products shrink to nothing on strip formats.\n const customData = (\n el as ExcalidrawElement & { customData?: Record<string, unknown> }\n ).customData;\n const slotType = (customData?.slotType ?? customData?.decomposeType) as\n | string\n | undefined;\n\n // Determine background: bottom element that covers the whole frame\n if (isFirst && coversFrame(el, frame)) {\n if (el.type === \"image\") {\n const fileData = files[el.fileId as string];\n if (fileData?.dataURL) {\n background = {\n type: \"image\",\n dataUrl: fileData.dataURL as string,\n elementId: el.id,\n };\n continue;\n }\n // File data is missing for the full-frame image \u2014 log so it's visible\n // in DevTools without requiring DEBUG_AUTO_RESIZE.\n console.warn(\n `[AutoResize] extractFrameInfo: full-frame image element has no file data (fileId=${el.fileId}). Will fall back to frame.backgroundColor.`,\n );\n } else if (\n el.type === \"rectangle\" &&\n el.backgroundColor &&\n el.backgroundColor !== \"transparent\"\n ) {\n background = {\n type: \"solid\",\n color: el.backgroundColor,\n elementId: el.id,\n };\n continue;\n }\n }\n\n // Foreground elements\n const relX = el.x - frame.x;\n const relY = el.y - frame.y;\n\n if (el.type === \"text\") {\n foreground.push({\n kind: \"text\",\n elementId: el.id,\n slotType,\n relX,\n relY,\n width: el.width,\n height: el.height,\n text: (el as any).text ?? \"\",\n contentHtml: (el as any).styleRanges?.length\n ? styledTextToHtml(\n (el as any).originalText ?? (el as any).text ?? \"\",\n (el as any).styleRanges,\n )\n : undefined,\n fontSize: (el as any).fontSize ?? 16,\n color: (el as any).strokeColor ?? \"#000000\",\n textAlign: (el as any).textAlign ?? \"left\",\n fontWeight: \"normal\",\n opacity: el.opacity ?? 100,\n });\n } else if (el.type === \"image\") {\n const fileData = files[el.fileId as string];\n if (fileData?.dataURL) {\n foreground.push({\n kind: \"image\",\n elementId: el.id,\n slotType,\n relX,\n relY,\n width: el.width,\n height: el.height,\n dataUrl: fileData.dataURL as string,\n opacity: el.opacity ?? 100,\n });\n }\n } else if (el.type === \"rectangle\" || el.type === \"ellipse\") {\n if (el.backgroundColor && el.backgroundColor !== \"transparent\") {\n foreground.push({\n kind: \"rect\",\n elementId: el.id,\n slotType,\n relX,\n relY,\n width: el.width,\n height: el.height,\n backgroundColor: el.backgroundColor,\n borderRadius:\n (el as any).roundness?.value ??\n (el.type === \"ellipse\" ? Math.min(el.width, el.height) / 2 : 0),\n opacity: el.opacity ?? 100,\n });\n }\n }\n }\n\n // Fallback: if no child element was identified as the background, check the\n // frame element's own backgroundColor. This is the common case for ads\n // generated by the AI pipeline where the base colour is set on the frame.\n if (background.type === \"none\") {\n const fc = (frame as any).backgroundColor as string | undefined;\n const isBlankOrWhite =\n !fc ||\n fc === \"transparent\" ||\n fc === \"#ffffff\" ||\n fc === \"#ffffffff\" ||\n fc === \"#fff\" ||\n fc === \"#FFFFFF\";\n if (!isBlankOrWhite) {\n background = { type: \"solid\", color: fc! };\n debug(\n `[AutoResize] extractFrameInfo: using frame.backgroundColor as fallback background: ${fc}`,\n );\n }\n }\n\n return { frame, background, foreground };\n}\n\n// ---------------------------------------------------------------------------\n// Scene element type alias\n// ---------------------------------------------------------------------------\n\ntype SceneElement = OrderedExcalidrawElement;\n\n// ---------------------------------------------------------------------------\n// Background regeneration helpers\n// ---------------------------------------------------------------------------\n\nexport function computeCropLoss(sourceAR: number, targetAR: number): number {\n // Simulate center-crop from source to target\n if (sourceAR >= targetAR) {\n // Source is wider \u2014 crop sides\n const usedWidth = targetAR * 1;\n return 1 - usedWidth / sourceAR;\n }\n // Source is taller \u2014 crop top/bottom\n const usedHeight = sourceAR / targetAR;\n return 1 - usedHeight;\n}\n\nexport function needsBackgroundRegeneration(\n srcW: number,\n srcH: number,\n tgtW: number,\n tgtH: number,\n): boolean {\n const srcAR = srcW / srcH;\n const tgtAR = tgtW / tgtH;\n const arDiff = Math.abs(srcAR - tgtAR);\n if (arDiff <= AR_DIFF_TOLERANCE) {\n return false;\n }\n return computeCropLoss(srcAR, tgtAR) > CROP_LOSS_THRESHOLD;\n}\n\nexport function nearestGeminiAspectRatio(w: number, h: number): string {\n const target = w / h;\n let best = \"1:1\";\n let bestDiff = Infinity;\n for (const [label, ratio] of Object.entries(GEMINI_RATIOS)) {\n const diff = Math.abs(target - ratio);\n if (diff < bestDiff) {\n bestDiff = diff;\n best = label;\n }\n }\n return best;\n}\n\n/**\n * Resolution dimensions available for each aspect ratio in Gemini 3.1 Flash\n * Image (Nano Banana 2). Maps aspect ratio \u2192 resolution level \u2192 [width, height].\n * Exported so the Auto Resize panel can build its dimension presets from the\n * exact model-supported sizes instead of duplicating the numbers.\n */\nexport const GEMINI_RESOLUTION_TABLE: Record<\n string,\n Record<string, [number, number]>\n> = {\n \"1:1\": {\n \"0.5K\": [512, 512],\n \"1K\": [1024, 1024],\n \"2K\": [2048, 2048],\n \"4K\": [4096, 4096],\n },\n \"1:4\": {\n \"0.5K\": [256, 1024],\n \"1K\": [512, 2048],\n \"2K\": [1024, 4096],\n \"4K\": [2048, 8192],\n },\n \"1:8\": {\n \"0.5K\": [192, 1536],\n \"1K\": [384, 3072],\n \"2K\": [768, 6144],\n \"4K\": [1536, 12288],\n },\n \"2:3\": {\n \"0.5K\": [424, 632],\n \"1K\": [848, 1264],\n \"2K\": [1696, 2528],\n \"4K\": [3392, 5056],\n },\n \"3:2\": {\n \"0.5K\": [632, 424],\n \"1K\": [1264, 848],\n \"2K\": [2528, 1696],\n \"4K\": [5056, 3392],\n },\n \"3:4\": {\n \"0.5K\": [448, 600],\n \"1K\": [896, 1200],\n \"2K\": [1792, 2400],\n \"4K\": [3584, 4800],\n },\n \"4:1\": {\n \"0.5K\": [1024, 256],\n \"1K\": [2048, 512],\n \"2K\": [4096, 1024],\n \"4K\": [8192, 2048],\n },\n \"4:3\": {\n \"0.5K\": [600, 448],\n \"1K\": [1200, 896],\n \"2K\": [2400, 1792],\n \"4K\": [4800, 3584],\n },\n \"4:5\": {\n \"0.5K\": [464, 576],\n \"1K\": [928, 1152],\n \"2K\": [1856, 2304],\n \"4K\": [3712, 4608],\n },\n \"5:4\": {\n \"0.5K\": [576, 464],\n \"1K\": [1152, 928],\n \"2K\": [2304, 1856],\n \"4K\": [4608, 3712],\n },\n \"8:1\": {\n \"0.5K\": [1536, 192],\n \"1K\": [3072, 384],\n \"2K\": [6144, 768],\n \"4K\": [12288, 1536],\n },\n \"9:16\": {\n \"0.5K\": [384, 688],\n \"1K\": [768, 1376],\n \"2K\": [1536, 2752],\n \"4K\": [3072, 5504],\n },\n \"16:9\": {\n \"0.5K\": [688, 384],\n \"1K\": [1376, 768],\n \"2K\": [2752, 1536],\n \"4K\": [5504, 3072],\n },\n \"21:9\": {\n \"0.5K\": [792, 168],\n \"1K\": [1584, 672],\n \"2K\": [3168, 1344],\n \"4K\": [6336, 2688],\n },\n};\n\n/**\n * Resolution levels offered by the Auto Resize panel.\n * Nano Banana (gemini-3.1-flash-image) and Nano Banana 2 (gemini-3.1-flash-lite-image)\n * both support 0.5K\u20134K. 0.5K is the fastest/cheapest tier.\n */\nexport const AUTO_RESIZE_RESOLUTIONS = [\"0.5K\", \"1K\", \"2K\", \"4K\"] as const;\nexport type AutoResizeResolution = typeof AUTO_RESIZE_RESOLUTIONS[number];\n\n/**\n * Aspect ratios the Nano Banana 2 / Gemini 3.1 Flash Image model supports,\n * ordered for display (common first, then the ultrawide / extreme-narrow ones).\n * Each carries a human label for the panel.\n */\nexport const AUTO_RESIZE_ASPECT_RATIOS: { ratio: string; desc: string }[] = [\n { ratio: \"1:1\", desc: \"Square\" },\n { ratio: \"16:9\", desc: \"Landscape\" },\n { ratio: \"9:16\", desc: \"Portrait / Story\" },\n { ratio: \"4:3\", desc: \"Landscape\" },\n { ratio: \"3:4\", desc: \"Portrait\" },\n { ratio: \"3:2\", desc: \"Landscape\" },\n { ratio: \"2:3\", desc: \"Portrait\" },\n { ratio: \"5:4\", desc: \"Landscape\" },\n { ratio: \"4:5\", desc: \"Portrait\" },\n { ratio: \"21:9\", desc: \"Ultrawide\" },\n { ratio: \"4:1\", desc: \"Wide strip\" },\n { ratio: \"1:4\", desc: \"Tall strip\" },\n { ratio: \"8:1\", desc: \"Banner\" },\n { ratio: \"1:8\", desc: \"Skyscraper\" },\n];\n\n/**\n * Concrete pixel dimensions for a given aspect ratio + resolution, straight from\n * the Nano Banana 2 resolution table. Falls back to 1:1 / 1K if unknown.\n */\nexport function geminiDimsFor(\n ratio: string,\n resolution: string,\n): { width: number; height: number } {\n const row = GEMINI_RESOLUTION_TABLE[ratio] ?? GEMINI_RESOLUTION_TABLE[\"1:1\"];\n const dims = row[resolution] ?? row[\"1K\"] ?? [1024, 1024];\n return { width: dims[0], height: dims[1] };\n}\n\n/**\n * Selects the optimal resolution for a given target dimension and aspect ratio.\n * Chooses the resolution level that is closest to (but preferably >= ) the target size.\n */\nexport function selectOptimalResolution(\n targetW: number,\n targetH: number,\n aspectRatio: string,\n): \"0.5K\" | \"1K\" | \"2K\" | \"4K\" {\n const resolutions = GEMINI_RESOLUTION_TABLE[aspectRatio];\n if (!resolutions) {\n // Fallback to 1K if aspect ratio not in table\n return \"1K\";\n }\n\n const targetArea = targetW * targetH;\n let best: \"0.5K\" | \"1K\" | \"2K\" | \"4K\" = \"1K\";\n let bestDiff = Infinity;\n\n for (const [resLevel, [w, h]] of Object.entries(resolutions)) {\n const resArea = w * h;\n const diff = Math.abs(resArea - targetArea);\n\n // Prefer resolutions that are >= target size (to avoid upscaling)\n // But if nothing is larger, pick the closest\n if (resArea >= targetArea) {\n if (diff < bestDiff) {\n bestDiff = diff;\n best = resLevel as \"0.5K\" | \"1K\" | \"2K\" | \"4K\";\n }\n } else if (bestDiff === Infinity || diff < bestDiff) {\n // No resolution >= target found yet, or this is closer\n bestDiff = diff;\n best = resLevel as \"0.5K\" | \"1K\" | \"2K\" | \"4K\";\n }\n }\n\n return best;\n}\n\n// ---------------------------------------------------------------------------\n// Image dimension utilities\n// ---------------------------------------------------------------------------\n\nfunction getImageNaturalDimensions(\n dataUrl: string,\n): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () =>\n resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.onerror = () => resolve({ width: 0, height: 0 });\n img.src = dataUrl;\n });\n}\n\n/**\n * Resizes a dataURL image down so its longest side is at most `maxDim` pixels,\n * then encodes it as JPEG at the given quality.\n *\n * Gemini rejects very large inline images and degrades in quality as payload\n * size grows. Keeping the reference image at \u2264800px is sufficient for\n * outpainting guidance while keeping the request well under API limits.\n */\nfunction downscaleForGemini(\n dataUrl: string,\n maxDim = 800,\n quality = 0.85,\n): Promise<string> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => {\n const { naturalWidth: w, naturalHeight: h } = img;\n if (w <= maxDim && h <= maxDim) {\n // Already small enough \u2014 but still re-encode as JPEG to normalise format\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n canvas.getContext(\"2d\")!.drawImage(img, 0, 0);\n resolve(canvas.toDataURL(\"image/jpeg\", quality));\n return;\n }\n const scale = maxDim / Math.max(w, h);\n const dw = Math.round(w * scale);\n const dh = Math.round(h * scale);\n const canvas = document.createElement(\"canvas\");\n canvas.width = dw;\n canvas.height = dh;\n canvas.getContext(\"2d\")!.drawImage(img, 0, 0, dw, dh);\n resolve(canvas.toDataURL(\"image/jpeg\", quality));\n };\n img.onerror = () => resolve(dataUrl); // If loading fails, fall through with original\n img.src = dataUrl;\n });\n}\n\n/**\n * Returns the x, y, width, height (relative to frame top-left) to place an\n * image such that:\n * - it covers the entire frame (scale up if needed)\n * - the centres of the image and frame coincide\n */\nexport function fitImageToFrame(\n imgW: number,\n imgH: number,\n frameW: number,\n frameH: number,\n): { relX: number; relY: number; width: number; height: number } {\n if (imgW <= 0 || imgH <= 0) {\n return { relX: 0, relY: 0, width: frameW, height: frameH };\n }\n // Scale factor needed to cover the frame\n const scale = Math.max(frameW / imgW, frameH / imgH);\n const fittedW = imgW * scale;\n const fittedH = imgH * scale;\n // Centre the image over the frame\n const relX = (frameW - fittedW) / 2;\n const relY = (frameH - fittedH) / 2;\n return { relX, relY, width: fittedW, height: fittedH };\n}\n\n/**\n * Fit-contain: scale image to fit entirely within the bbox, preserving aspect\n * ratio, centred. Use for foreground elements (logos, product images) where\n * cropping or stretching is not acceptable.\n */\nexport function containImageInBbox(\n imgW: number,\n imgH: number,\n bboxW: number,\n bboxH: number,\n): { relX: number; relY: number; width: number; height: number } {\n if (imgW <= 0 || imgH <= 0) {\n return { relX: 0, relY: 0, width: bboxW, height: bboxH };\n }\n const scale = Math.min(bboxW / imgW, bboxH / imgH);\n const fittedW = imgW * scale;\n const fittedH = imgH * scale;\n const relX = (bboxW - fittedW) / 2;\n const relY = (bboxH - fittedH) / 2;\n return { relX, relY, width: fittedW, height: fittedH };\n}\n\nexport function getProductMinBox(\n targetW: number,\n targetH: number,\n): { minWidth: number; minHeight: number } {\n const ar = targetW / targetH;\n\n if (ar >= 3.5) {\n return {\n minWidth: Math.min(targetW, Math.max(Math.round(targetW * 0.28), 300)),\n minHeight: Math.round(targetH * 0.72),\n };\n }\n\n if (ar <= 1 / 3.5) {\n return {\n minWidth: Math.round(targetW * 0.82),\n minHeight: Math.round(targetH * 0.34),\n };\n }\n\n if (ar <= 0.65) {\n return {\n minWidth: Math.round(targetW * 0.76),\n minHeight: Math.round(targetH * 0.32),\n };\n }\n\n if (ar >= 1.6) {\n return {\n minWidth: Math.round(targetW * 0.36),\n minHeight: Math.round(targetH * 0.58),\n };\n }\n\n return {\n minWidth: Math.round(targetW * 0.54),\n minHeight: Math.round(targetH * 0.42),\n };\n}\n\nexport function enforceMinProductBox({\n x,\n y,\n width,\n height,\n targetW,\n targetH,\n}: {\n x: number;\n y: number;\n width: number;\n height: number;\n targetW: number;\n targetH: number;\n}): { x: number; y: number; width: number; height: number } {\n const { minWidth, minHeight } = getProductMinBox(targetW, targetH);\n const nextWidth = Math.min(targetW, Math.max(width, minWidth));\n const nextHeight = Math.min(targetH, Math.max(height, minHeight));\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n\n return {\n x: Math.min(Math.max(centerX - nextWidth / 2, 0), targetW - nextWidth),\n y: Math.min(Math.max(centerY - nextHeight / 2, 0), targetH - nextHeight),\n width: nextWidth,\n height: nextHeight,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Frame screenshot (for AI reviewer)\n// ---------------------------------------------------------------------------\n\nasync function captureFrameScreenshotFromApp(\n app: AppClassProperties,\n frameId: string,\n): Promise<string | null> {\n const elements = app.getSceneElements();\n const frame = elements.find((el) => el.id === frameId) as\n | ExcalidrawFrameLikeElement\n | undefined;\n if (!frame) {\n return null;\n }\n\n const children = getFrameChildren(elements, frameId);\n try {\n const canvas = await exportToCanvas(\n [...children, frame] as any,\n app.state as any,\n app.files,\n {\n exportBackground: true,\n viewBackgroundColor:\n (app.state as any).viewBackgroundColor ?? \"#ffffff\",\n exportingFrame: frame,\n exportPadding: 0,\n },\n );\n return canvas.toDataURL(\"image/jpeg\", 0.7);\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// OpenRouter HTML generation\n// ---------------------------------------------------------------------------\n\nfunction classifyBannerType(\n w: number,\n h: number,\n): \"horizontal-strip\" | \"vertical-strip\" | \"square\" | \"portrait\" | \"landscape\" {\n const ar = w / h;\n if (ar >= 3.5) {\n return \"horizontal-strip\";\n } // 728x90, 970x90 etc.\n if (ar <= 1 / 3.5) {\n return \"vertical-strip\";\n } // 160x600, 120x600\n if (ar >= 1.6) {\n return \"landscape\";\n } // 300x250 is not this, 1200x628, 16:9\n if (ar <= 0.65) {\n return \"portrait\";\n } // 300x600, 160x600 (caught above)\n return \"square\"; // 1:1 or close\n}\n\nfunction getBannerLayoutGuidance(\n srcW: number,\n srcH: number,\n tgtW: number,\n tgtH: number,\n): string {\n const type = classifyBannerType(tgtW, tgtH);\n const srcType = classifyBannerType(srcW, srcH);\n\n // These describe the GEOMETRIC constraints of each target format only \u2014 what\n // physically fits and what must shrink. They deliberately do NOT prescribe\n // where the logo, headline, or CTA go. The source ad already encodes the\n // brand's own art direction, and this pass re-fits that design rather than\n // replacing it; naming corners here overrode the brand's own placement on\n // every resize. Preserve the source's arrangement and reading order wherever\n // the target geometry allows.\n const guidance: Record<string, string> = {\n \"horizontal-strip\": `\nHORIZONTAL STRIP BANNER (${tgtW}\u00D7${tgtH}) \u2014 VERY WIDE AND SHORT:\n\u2022 This is an extreme format. Height is tiny so you must be ruthless with vertical space.\n\u2022 A vertical stack cannot survive here \u2014 flow the source's elements along the horizontal axis instead, keeping the source's reading order (whatever leads in the original leads here too).\n\u2022 Text: REDUCE font size aggressively (aim for ~${Math.max(\n 10,\n Math.round(tgtH * 0.35),\n )}\u2013${Math.max(\n 14,\n Math.round(tgtH * 0.5),\n )}px). Single line only \u2014 no wrapping.\n\u2022 Product image (if present): height = 70\u201390% of canvas height (${Math.round(\n tgtH * 0.7,\n )}\u2013${Math.round(tgtH * 0.9)}px) so it stays legible at this scale.\n\u2022 NOTHING should be taller than the canvas. All elements must fit within ${tgtH}px height.\n\u2022 Remove or collapse any decorative shapes that cannot fit at this scale.`,\n\n \"vertical-strip\": `\nVERTICAL STRIP BANNER (${tgtW}\u00D7${tgtH}) \u2014 NARROW AND TALL:\n\u2022 A side-by-side arrangement cannot survive here \u2014 stack the source's elements along the vertical axis instead, preserving the source's reading order top to bottom.\n\u2022 Width is the binding constraint: scale elements to fit ${tgtW}px without distorting their aspect ratios.\n\u2022 Text: font size \u2248 ${Math.max(12, Math.round(tgtW * 0.1))}\u2013${Math.max(\n 16,\n Math.round(tgtW * 0.14),\n )}px so it stays readable at this width.\n\u2022 Leave \u22658px horizontal padding on each side \u2014 nothing should touch the edges.`,\n\n square: `\nSQUARE FORMAT (${tgtW}\u00D7${tgtH}):\n\u2022 Neither axis is binding \u2014 the source layout can usually be re-fitted close to its original arrangement.\n\u2022 Keep the source's relative positions and proportions; adjust only what the changed aspect ratio actually forces.`,\n\n portrait: `\nPORTRAIT FORMAT (${tgtW}\u00D7${tgtH}):\n\u2022 Taller than wide, but less extreme than a vertical strip \u2014 there is room for the source's structure.\n\u2022 Width is the tighter axis: scale to fit without distorting aspect ratios, and preserve the source's arrangement where it still fits.\n\u2022 Text must be large enough to read at display size.`,\n\n landscape: `\nLANDSCAPE FORMAT (${tgtW}\u00D7${tgtH}):\n\u2022 Wider than tall \u2014 height is the tighter axis.\n\u2022 Preserve the source's arrangement and reading order; adjust only what the changed aspect ratio forces.\n\u2022 Scale font sizes to \u2265${Math.max(\n 16,\n Math.round(tgtH * 0.05),\n )}px for readability.`,\n };\n\n let result = guidance[type] ?? guidance.square;\n\n // Add cross-format transition hint\n if (srcType !== type) {\n result += `\n\nSOURCE FORMAT WAS: ${srcType.toUpperCase()} (${srcW}\u00D7${srcH}) \u2192 you are adapting to ${type.toUpperCase()}.\nRe-fit the layout to the new geometry \u2014 a linear scale of the original positions will not work, but neither should you redesign it. Keep the source's arrangement, reading order, and relative emphasis, and change only what the format change forces.\nPrioritise: (1) readability of text, (2) prominence of the primary CTA, (3) brand identity.`;\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Vision-based semantic element tagger\n// ---------------------------------------------------------------------------\n\nconst VALID_SLOT_TYPES = new Set([\"logo\", \"product\", \"decoration\", \"cta\"]);\n\n/**\n * Sends a screenshot of the source frame to a vision LLM and asks it to\n * classify each foreground image as logo / product / decoration / cta.\n * Tags are written back onto the ForegroundEl objects in-place.\n * Best-effort \u2014 silently skips on any error.\n */\nasync function tagImageElementsWithVision(\n info: SourceFrameInfo,\n screenshotDataUrl: string,\n autoResizeUrl: string,\n model: string,\n signal?: AbortSignal,\n): Promise<void> {\n const imageEls = info.foreground.filter(\n (el) => el.kind === \"image\" && !el.slotType,\n );\n if (imageEls.length === 0) {\n return;\n }\n\n const srcW = info.frame.width;\n const srcH = info.frame.height;\n\n const elementList = imageEls\n .map((el, i) => {\n const pctX = ((el.relX / srcW) * 100).toFixed(1);\n const pctY = ((el.relY / srcH) * 100).toFixed(1);\n const pctW = ((el.width / srcW) * 100).toFixed(1);\n const pctH = ((el.height / srcH) * 100).toFixed(1);\n return `[${\n i + 1\n }] IMAGE \u2014 x=${pctX}% from left, y=${pctY}% from top, w=${pctW}% of canvas, h=${pctH}% of canvas`;\n })\n .join(\"\\n\");\n\n const prompt = `You are given a screenshot of an advertisement creative. Identify the semantic role of each numbered image element listed below.\n\nImage elements:\n${elementList}\n\nAssign each element exactly one role:\n- \"logo\": brand logo, wordmark, icon, or brand mark\n- \"product\": main product, hero image, or featured item\n- \"decoration\": decorative graphic, background texture, badge, or pattern\n- \"cta\": call-to-action button image or badge\n\nReturn ONLY a JSON object mapping the element number (string key) to its role. Example: {\"1\": \"logo\", \"2\": \"product\"}`;\n\n try {\n // Routed through the host proxy (autoResizeUrl); the key stays server-side.\n const response = await fetch(autoResizeUrl, {\n method: \"POST\",\n signal,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model,\n messages: [\n {\n role: \"user\",\n content: [\n { type: \"image_url\", image_url: { url: screenshotDataUrl } },\n { type: \"text\", text: prompt },\n ],\n },\n ],\n response_format: { type: \"json_object\" },\n temperature: 0.1,\n stream: false,\n }),\n });\n\n if (!response.ok) {\n return;\n }\n\n const data = await response.json();\n const raw: string = data?.choices?.[0]?.message?.content ?? \"\";\n const parsed = JSON.parse(raw) as Record<string, string>;\n\n imageEls.forEach((el, i) => {\n const role = parsed[String(i + 1)];\n if (role && VALID_SLOT_TYPES.has(role)) {\n el.slotType = role;\n debug(`[AutoResize] vision tagger: element ${i + 1} \u2192 ${role}`);\n }\n });\n } catch {\n // Best-effort \u2014 layout continues without tags\n }\n}\n\n/**\n * Builds the layout prompt for the LLM.\n *\n * Foreground images are referenced by placeholder tokens (IMG_PLACEHOLDER_N)\n * rather than inlining their data URLs. This prevents multi-megabyte base64\n * strings from blowing up the prompt and hitting token limits. The returned\n * `imagePlaceholders` map is used to substitute the real data URLs back into\n * the HTML after the LLM responds.\n */\nfunction buildAutoResizePrompt(\n info: SourceFrameInfo,\n targetW: number,\n targetH: number,\n reviewerFeedback?: ReviewerFeedback,\n): { prompt: string; imagePlaceholders: Map<string, string> } {\n const { frame, background, foreground } = info;\n const srcW = Math.round(frame.width);\n const srcH = Math.round(frame.height);\n\n const imagePlaceholders = new Map<string, string>();\n\n const bgNote =\n background.type === \"solid\"\n ? `Background: solid colour ${background.color} \u2014 set this as background-color on .canvas`\n : background.type === \"image\"\n ? `Background: image \u2014 DO NOT include in HTML (it is placed separately as an image layer)`\n : `Background: none / transparent`;\n\n const elementsDesc = foreground\n .map((el, i) => {\n const pctX = ((el.relX / srcW) * 100).toFixed(1);\n const pctY = ((el.relY / srcH) * 100).toFixed(1);\n const pctW = ((el.width / srcW) * 100).toFixed(1);\n const pctH = ((el.height / srcH) * 100).toFixed(1);\n\n const roleTag = el.slotType ? ` (role: ${el.slotType})` : \"\";\n\n if (el.kind === \"text\") {\n return `[${i + 1}] TEXT${roleTag} \u2014 source pos: x=${Math.round(\n el.relX,\n )}px (${pctX}% of W), y=${Math.round(\n el.relY,\n )}px (${pctY}% of H), size: ${Math.round(el.width)}\u00D7${Math.round(\n el.height,\n )}px (${pctW}%\u00D7${pctH}% of canvas)\n content: \"${el.text.replace(/\"/g, '\\\\\"').slice(0, 200)}\"${\n el.contentHtml\n ? `\n content markup (parts of this text are individually styled \u2014 reproduce this inline markup verbatim inside the layer): ${el.contentHtml.slice(\n 0,\n 600,\n )}`\n : \"\"\n }\n style: font-size: ${el.fontSize}px, color: ${el.color}, text-align: ${\n el.textAlign\n }, opacity: ${Math.round(el.opacity)}%`;\n } else if (el.kind === \"image\") {\n const placeholder = `IMG_PLACEHOLDER_${i}`;\n imagePlaceholders.set(placeholder, el.dataUrl);\n return `[${i + 1}] IMAGE${roleTag} \u2014 source pos: x=${Math.round(\n el.relX,\n )}px (${pctX}% of W), y=${Math.round(\n el.relY,\n )}px (${pctY}% of H), size: ${Math.round(el.width)}\u00D7${Math.round(\n el.height,\n )}px (${pctW}%\u00D7${pctH}% of canvas)\n Place this image as: background-image: url(${placeholder}); background-size: contain; background-repeat: no-repeat; background-position: center; opacity: ${(\n el.opacity / 100\n ).toFixed(2)}`;\n }\n return `[${i + 1}] SHAPE/RECT${roleTag} \u2014 source pos: x=${Math.round(\n el.relX,\n )}px (${pctX}% of W), y=${Math.round(\n el.relY,\n )}px (${pctY}% of H), size: ${Math.round(el.width)}\u00D7${Math.round(\n el.height,\n )}px (${pctW}%\u00D7${pctH}% of canvas)\n style: background-color: ${el.backgroundColor}, border-radius: ${\n el.borderRadius\n }px, opacity: ${Math.round(el.opacity)}%`;\n })\n .join(\"\\n\\n\");\n\n const layoutGuidance = getBannerLayoutGuidance(srcW, srcH, targetW, targetH);\n\n const bgCss =\n background.type === \"solid\" ? `background-color: ${background.color};` : \"\";\n\n // Build reviewer feedback section if a prior round had issues\n const reviewerSection =\n reviewerFeedback &&\n !reviewerFeedback.approved &&\n reviewerFeedback.issues.length > 0\n ? `\n## REVIEWER FEEDBACK \u2014 fix these issues in this revision:\n${reviewerFeedback.issues\n .map(\n (issue, n) =>\n `${n + 1}. [${issue.severity.toUpperCase()}] ${issue.element}: ${\n issue.problem\n }\n Fix: ${issue.preciseInstruction}`,\n )\n .join(\"\\n\")}\n\n`\n : \"\";\n\n const prompt = `You are an expert ad layout designer. Re-layout the following ad creative from ${srcW}\u00D7${srcH} to ${targetW}\u00D7${targetH}.\n${reviewerSection}\n## Source creative: ${srcW}\u00D7${srcH}\n${bgNote}\n\n## Source elements (${foreground.length} total):\n${elementsDesc || \"(no foreground elements)\"}\n\n## Target canvas: ${targetW}\u00D7${targetH}\n${layoutGuidance}\n\n## Non-negotiable element count rule\nThe transformed frame must contain EXACTLY ${\n foreground.length\n } component(s), matching the ${\n foreground.length\n } source element(s) listed above.\nCreate a strict one-to-one mapping: source element [1] becomes output child [1], source element [2] becomes output child [2], and so on.\nDo NOT add any new elements, components, headlines, subheadlines, CTA buttons, badges, logos, captions, decorative shapes, overlays, or background layers.\nDo NOT remove any source elements.\nDo NOT split one source element into multiple output elements.\nDo NOT merge multiple source elements into one output element.\nIf the source has zero text elements, the output must have zero text elements.\nIf the source does not include a headline or CTA, the output must not include a headline or CTA.\nThe layout guidance is only for repositioning and resizing existing elements; it never permits adding new content.\n\n## Task\nProduce complete, self-contained HTML that represents this ad at exactly ${targetW}\u00D7${targetH} pixels.\nPreserve the original text content, image assets, colors, and brand identity.\nOnly change positions and sizes.\n\n## STRICT HTML CONTRACT (parser requirements \u2014 do NOT deviate):\n1. Root element MUST be: <div class=\"canvas\" style=\"position: relative; width: ${targetW}px; height: ${targetH}px; overflow: hidden; ${bgCss}\">\n2. The .canvas root MUST contain exactly ${\n foreground.length\n } direct child <div> element(s), one for each source element listed above, in the same order.\n3. Every child element MUST have: position: absolute; left: Xpx; top: Ypx; width: Wpx; height: Hpx\n4. Use ONLY <div> elements. NO <span>, <p>, <img>, <h1>\u2013<h6>, <button>, etc.\n5. Text: inline style with color, font-size (px), font-weight, text-align, line-height, white-space: pre-wrap\n6. Images: background-image: url(IMG_PLACEHOLDER_N); background-size: contain; background-repeat: no-repeat; background-position: center\n7. Shapes: background-color in hex (#rrggbb). Gradients allowed (linear-gradient only).\n8. NO flexbox. NO grid. NO transform. NO animation. NO filter. NO clip-path.\n9. Use the exact IMG_PLACEHOLDER_N tokens provided above \u2014 do NOT substitute real URLs.\n10. All numeric values in px. No %, em, rem, vw, vh.\n11. Opacity via CSS opacity property (0\u20131 decimal).\n12. border-radius in px if needed.\n13. One purpose per div: background fill OR image OR text (never mixed).\n\nReturn ONLY the raw HTML. No markdown, no code fences, no explanation.`;\n\n return { prompt, imagePlaceholders };\n}\n\n/**\n * Checks generated HTML against the strict contract in `buildAutoResizePrompt`\n * BEFORE handing it to the parser.\n *\n * The parser is deliberately narrow \u2014 absolutely-positioned divs, no flex, no\n * grid, no transforms \u2014 because that is what maps cleanly onto Excalidraw\n * elements. Models drift off strict DOM contracts, and a violation shows up as\n * silently mangled geometry rather than an error. Catching it here buys one\n * targeted retry instead of a wrong layout.\n *\n * Returns the list of violations; empty means the HTML conforms.\n */\nexport function validateHtmlContract(\n html: string,\n expectedChildCount: number,\n): string[] {\n const violations: string[] = [];\n\n if (!/class\\s*=\\s*[\"'][^\"']*\\bcanvas\\b/.test(html)) {\n violations.push(\n 'The root element is missing class=\"canvas\". Wrap everything in <div class=\"canvas\" style=\"position: relative; ...\">.',\n );\n }\n\n const bannedTags = [\n \"span\",\n \"p\",\n \"img\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"button\",\n \"section\",\n \"ul\",\n \"li\",\n ];\n const usedBanned = bannedTags.filter((t) =>\n new RegExp(`<${t}[\\\\s>]`, \"i\").test(html),\n );\n if (usedBanned.length > 0) {\n violations.push(\n `Used forbidden tag(s): ${usedBanned\n .map((t) => `<${t}>`)\n .join(\", \")}. Use ONLY <div> elements.`,\n );\n }\n\n const bannedCss: [RegExp, string][] = [\n [/display\\s*:\\s*flex/i, \"display: flex\"],\n [/display\\s*:\\s*grid/i, \"display: grid\"],\n [/(^|[;\\s\"'])transform\\s*:/i, \"transform\"],\n [/animation\\s*:/i, \"animation\"],\n [/\\bfilter\\s*:/i, \"filter\"],\n [/clip-path\\s*:/i, \"clip-path\"],\n ];\n const usedCss = bannedCss\n .filter(([re]) => re.test(html))\n .map(([, name]) => name);\n if (usedCss.length > 0) {\n violations.push(\n `Used forbidden CSS: ${usedCss.join(\n \", \",\n )}. Position every layer with absolute left/top/width/height in px.`,\n );\n }\n\n // Count only DIRECT children of .canvas by depth-tracking the div tree.\n const canvasIdx = html.search(/<div[^>]*class\\s*=\\s*[\"'][^\"']*\\bcanvas\\b/);\n if (canvasIdx >= 0) {\n const rest = html.slice(canvasIdx);\n const openClose = rest.match(/<div\\b|<\\/div>/gi) ?? [];\n let depth = 0;\n let directChildren = 0;\n for (const tok of openClose) {\n if (tok.toLowerCase() === \"</div>\") {\n depth--;\n if (depth <= 0) {\n break;\n }\n } else {\n depth++;\n if (depth === 2) {\n directChildren++;\n }\n }\n }\n if (expectedChildCount > 0 && directChildren !== expectedChildCount) {\n violations.push(\n `.canvas must contain exactly ${expectedChildCount} direct child <div> element(s), but ${directChildren} were found. One per layer, no wrappers.`,\n );\n }\n }\n\n return violations;\n}\n\nasync function callOpenRouterForHtml(\n prompt: string,\n autoResizeUrl: string,\n model: string,\n signal?: AbortSignal,\n): Promise<string> {\n // Routed through the host proxy (autoResizeUrl); the key stays server-side.\n const response = await fetch(autoResizeUrl, {\n method: \"POST\",\n signal,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"user\", content: prompt }],\n stream: false,\n temperature: 0.3,\n }),\n });\n\n if (!response.ok) {\n let msg = `OpenRouter API error ${response.status}`;\n try {\n const err = await response.json();\n if (err?.error?.message) {\n msg = err.error.message;\n }\n } catch {\n /* ignore */\n }\n throw new Error(msg);\n }\n\n const data = await response.json();\n const content: string = data?.choices?.[0]?.message?.content ?? \"\";\n\n // Strip markdown code fences if present\n const stripped = content\n .replace(/^```(?:html)?\\s*/i, \"\")\n .replace(/\\s*```\\s*$/i, \"\")\n .trim();\n\n return stripped;\n}\n\n// ---------------------------------------------------------------------------\n// Scene insertion helpers\n// ---------------------------------------------------------------------------\n\nfunction findInsertAfterFrame(\n elements: readonly ExcalidrawElement[],\n frameId: string,\n): number {\n // Find the frame element index\n const frameIdx = elements.findIndex((el) => el.id === frameId);\n if (frameIdx < 0) {\n return elements.length;\n }\n // Walk forward past all children of this frame\n let end = frameIdx + 1;\n for (let i = frameIdx + 1; i < elements.length; i++) {\n const el = elements[i] as ExcalidrawElement & { frameId?: string };\n if (el.frameId === frameId) {\n end = i + 1;\n }\n }\n return end;\n}\n\n// ===========================================================================\n// AI-HTML re-layout path \u2014 the Auto Resize implementation\n// ---------------------------------------------------------------------------\n// extract frame \u2192 LLM HTML re-layout \u2192 contract validation \u2192 parse via\n// htmlToExcalidrawElements \u2192 insert \u2192 reviewer critique loop.\n//\n// Output is native Excalidraw elements: real text, real image layers, exact\n// source colours. Nothing is rasterised, so the result stays editable and no\n// image model has to \"support\" the target aspect ratio \u2014 a 4:1 strip is a\n// layout problem, not a generation problem.\n//\n// The background is handled separately by `buildTargetBackground`\n// (./backgroundExtend), which extends stationary plates locally and only\n// reaches for a generative outpaint on genuinely photographic ones.\n//\n// `tagImageElementsWithVision` is intentionally NOT called: \"Make editable\"\n// already tags every layer with `decomposeType`, which `extractFrameInfo` maps\n// onto `slotType`. That saves a vision call per run.\n// ===========================================================================\n\nasync function runAutoResizeForDimensionHtml(opts: {\n info: SourceFrameInfo;\n targetWidth: number;\n targetHeight: number;\n placementX: number; // absolute canvas x for new frame (INITIAL position only)\n placementY: number; // absolute canvas y for new frame (INITIAL position only)\n app: AppClassProperties;\n autoResizeUrl: string;\n chatModel: string;\n reviewerModel?: string;\n outpaintModel?: string;\n customFontMap?: Record<string, number>;\n onProgress?: (phase: string) => void;\n signal?: AbortSignal;\n debugLabel?: string; // e.g. \"[1/7] 1200\u00D7628\" for console logging\n existingFrameId?: string; // if provided, skip frame creation and use this frame\n}): Promise<{ frameId: string }> {\n const {\n info,\n targetWidth: tgtW,\n targetHeight: tgtH,\n placementX,\n placementY,\n app,\n autoResizeUrl,\n chatModel,\n reviewerModel,\n outpaintModel,\n customFontMap = {},\n onProgress,\n signal,\n debugLabel = `${tgtW}\u00D7${tgtH}`,\n existingFrameId,\n } = opts;\n\n const srcFrame = info.frame;\n\n // Helper: Get current frame position from scene (may differ from initial if user dragged it)\n const getCurrentFramePosition = (): { x: number; y: number } => {\n const currentEls = app.getSceneElements();\n const currentFrame = currentEls.find((el) => el.id === frameId);\n if (currentFrame) {\n return { x: currentFrame.x, y: currentFrame.y };\n }\n // Fallback to initial position if frame not found (shouldn't happen)\n return { x: placementX, y: placementY };\n };\n\n // \u2500\u2500 1. Create the new frame (or reuse pre-created one) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let frameId: string;\n\n if (existingFrameId) {\n // Frame was pre-created upfront \u2014 skip insertion\n frameId = existingFrameId;\n } else {\n const newFrame = newFrameElement({\n ...FRAME_STYLE,\n x: placementX,\n y: placementY,\n width: tgtW,\n height: tgtH,\n opacity: 100,\n locked: false,\n name: `${tgtW}\u00D7${tgtH}`,\n });\n\n const currentEls = app.getSceneElements() as SceneElement[];\n const withFrame = [...currentEls, newFrame as SceneElement];\n syncMovedIndices(withFrame, arrayToMap([newFrame as SceneElement]));\n app.updateScene({ elements: withFrame });\n\n frameId = newFrame.id;\n }\n\n // \u2500\u2500 2. Handle background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The gate is unchanged: below the crop-loss threshold the source plate is\n // simply cover-scaled; above it the plate has to be genuinely EXTENDED,\n // because a 1:1 background cannot sit in a vertical strip without either\n // distorting or letterboxing.\n //\n // What changed is HOW it is extended. `buildTargetBackground` classifies the\n // plate and picks the cheapest correct treatment \u2014 mirror-tiling a halftone\n // or a two-panel composition costs nothing and reproduces the pattern\n // exactly, where an image model would redraw it and get the spacing wrong.\n // Only a genuinely photographic plate reaches a generation call.\n const needsRegen = needsBackgroundRegeneration(\n srcFrame.width,\n srcFrame.height,\n tgtW,\n tgtH,\n );\n debug(\n `[AutoResize] ${debugLabel} \u2014 background: type=${info.background.type}, needsRegen=${needsRegen}`,\n );\n\n let bgResult: BackgroundResult = { kind: \"none\" };\n try {\n bgResult = await buildTargetBackground(info.background, tgtW, tgtH, {\n needsExtension: needsRegen,\n onProgress,\n outpaint: async ({ dataUrl, targetWidth, targetHeight }) => {\n const ar = nearestGeminiAspectRatio(targetWidth, targetHeight);\n const resolution = selectOptimalResolution(\n targetWidth,\n targetHeight,\n ar,\n );\n const model = outpaintModel || DEFAULT_BG_REGEN_MODEL;\n debug(\n `[AutoResize] ${debugLabel} \u2014 photographic plate, outpainting (model=${model}, ar=${ar}, res=${resolution})`,\n );\n // Downscale the reference before sending \u2014 prevents large-payload\n // failures and speeds up the call. 800px is enough for outpainting.\n const compressed = await downscaleForGemini(dataUrl);\n return callOpenRouterImageAPI(\n `Expand this image to fill the full canvas dimensions.\nPreserve the original scene exactly \u2014 same lighting, color palette, style, and mood.\nFill any new areas with content that naturally continues the existing scene,\nas if the camera simply zoomed out. The original image content must appear embedded naturally within the larger scene,\nwith no visible boundary or transition between old and new areas.\nNo blurring, softening, or bokeh.\nNo visible seams, tiling, repeated patterns, or artifacts.\nNo new text, logos, watermarks, or unrelated objects added.`,\n model,\n ar,\n resolution,\n true,\n autoResizeUrl,\n compressed,\n signal,\n );\n },\n });\n debug(\n `[AutoResize] ${debugLabel} \u2014 background resolved: kind=${bgResult.kind}${\n bgResult.kind === \"image\" ? ` strategy=${bgResult.strategy}` : \"\"\n }`,\n );\n } catch (bgErr) {\n // Non-fatal: the layout is still worth producing without a background.\n console.error(\n `[AutoResize] background build failed for ${debugLabel}:`,\n bgErr instanceof Error ? bgErr.message : String(bgErr),\n );\n }\n\n const bgImageDataUrl = bgResult.kind === \"image\" ? bgResult.dataUrl : null;\n\n // A solid background becomes a plain rect rather than a bitmap \u2014 cheaper,\n // sharper, and it stays editable as a colour rather than pixels.\n if (bgResult.kind === \"solid\") {\n const currentPos = getCurrentFramePosition();\n const rectEl = newElement({\n type: \"rectangle\",\n x: currentPos.x,\n y: currentPos.y,\n width: tgtW,\n height: tgtH,\n backgroundColor: bgResult.color,\n strokeColor: \"transparent\",\n fillStyle: \"solid\" as const,\n roughness: 0,\n customData: { autoResizeBg: true },\n });\n const elsForRect = app.getSceneElements() as SceneElement[];\n const insertAt = findInsertAfterFrame(elsForRect, frameId);\n const withRect = [\n ...elsForRect.slice(0, insertAt),\n { ...rectEl, frameId } as SceneElement,\n ...elsForRect.slice(insertAt),\n ];\n syncMovedIndices(withRect, arrayToMap([rectEl as SceneElement]));\n app.updateScene({ elements: withRect });\n }\n\n // Insert background image if we have one\n if (bgImageDataUrl) {\n onProgress?.(\"Placing background\u2026\");\n const { width: imgNatW, height: imgNatH } = await getImageNaturalDimensions(\n bgImageDataUrl,\n );\n\n const {\n relX,\n relY,\n width: imgW,\n height: imgH,\n } = fitImageToFrame(imgNatW, imgNatH, tgtW, tgtH);\n\n // Use CURRENT frame position (may have moved since generation started)\n const currentPos = getCurrentFramePosition();\n\n const bgFileId = nanoid() as FileId;\n const bgEl = newImageElement({\n type: \"image\",\n x: currentPos.x + relX,\n y: currentPos.y + relY,\n width: imgW,\n height: imgH,\n fileId: bgFileId,\n status: \"pending\",\n scale: [1, 1],\n customData: { autoResizeBg: true },\n });\n\n const elsAfterFrame = app.getSceneElements() as SceneElement[];\n const insertPos = findInsertAfterFrame(elsAfterFrame, frameId);\n const withBg = [\n ...elsAfterFrame.slice(0, insertPos),\n { ...bgEl, frameId } as SceneElement,\n ...elsAfterFrame.slice(insertPos),\n ];\n syncMovedIndices(withBg, arrayToMap([bgEl as SceneElement]));\n app.updateScene({ elements: withBg });\n\n app.addFiles([\n {\n id: bgFileId,\n dataURL: bgImageDataUrl as DataURL,\n mimeType: getMimeTypeFromDataURL(bgImageDataUrl) as ValueOf<\n typeof IMAGE_MIME_TYPES\n >,\n created: Date.now(),\n lastRetrieved: Date.now(),\n },\n ]);\n }\n\n // \u2500\u2500 3 + 4 + 5. HTML generation with reviewer loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.(\"Generating layout\u2026\");\n\n let reviewRounds = 0;\n let lastFeedback: ReviewerFeedback | undefined;\n\n // We loop: generate HTML \u2192 parse \u2192 insert \u2192 screenshot \u2192 review \u2192 maybe regenerate\n // The loop body always replaces any previously-inserted elements for this frame.\n while (true) {\n debug(\n `[AutoResize] ${debugLabel} \u2014 calling OpenRouter for HTML layout (model=${chatModel}, round=${\n reviewRounds + 1\n })`,\n );\n\n const { prompt, imagePlaceholders } = buildAutoResizePrompt(\n info,\n tgtW,\n tgtH,\n lastFeedback,\n );\n let html = await callOpenRouterForHtml(\n prompt,\n autoResizeUrl,\n chatModel,\n signal,\n );\n\n if (!html || html.length < 20) {\n throw new Error(\"LLM returned empty HTML\");\n }\n\n // Contract check + one targeted repair. A violation here would otherwise\n // surface as silently mangled geometry rather than an error, because the\n // parser only understands absolutely-positioned divs.\n const violations = validateHtmlContract(html, info.foreground.length);\n if (violations.length > 0) {\n debug(\n `[AutoResize] ${debugLabel} \u2014 HTML contract violated: ${violations.join(\n \" | \",\n )}`,\n );\n const repairPrompt = `${prompt}\n\n## CONTRACT VIOLATIONS IN YOUR PREVIOUS ANSWER \u2014 fix these exactly:\n${violations.map((v) => `- ${v}`).join(\"\\n\")}\n\nReturn ONLY the corrected raw HTML. No markdown, no code fences, no explanation.`;\n html = await callOpenRouterForHtml(\n repairPrompt,\n autoResizeUrl,\n chatModel,\n signal,\n );\n\n const stillBroken = validateHtmlContract(html, info.foreground.length);\n if (!html || html.length < 20 || stillBroken.length > 0) {\n throw new Error(\n `HTML contract violated after repair: ${stillBroken.join(\" | \")}`,\n );\n }\n debug(`[AutoResize] ${debugLabel} \u2014 HTML contract repaired`);\n }\n\n debug(\n `[AutoResize] ${debugLabel} \u2014 HTML received (${html.length} chars), substituting ${imagePlaceholders.size} image placeholder(s)\u2026`,\n );\n\n // Substitute image placeholders with their actual data URLs\n for (const [placeholder, dataUrl] of imagePlaceholders) {\n html = html.split(`url(${placeholder})`).join(`url(${dataUrl})`);\n }\n\n // \u2500\u2500 4. Parse HTML \u2192 Excalidraw elements \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.(\"Parsing layout\u2026\");\n\n // Use CURRENT frame position (may have moved since generation started)\n const currentPos = getCurrentFramePosition();\n\n let parsedEls;\n try {\n parsedEls = await htmlToExcalidrawElements(\n html,\n currentPos.x,\n currentPos.y,\n tgtW,\n tgtH,\n customFontMap,\n );\n } catch (err) {\n throw new Error(\n `HTML parse failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n\n if (!parsedEls || parsedEls.length === 0) {\n throw new Error(\"HTML parser produced no elements\");\n }\n\n const productDataUrls = new Set(\n info.foreground\n .filter(\n (el): el is Extract<ForegroundEl, { kind: \"image\" }> =>\n el.kind === \"image\" && el.slotType === \"product\",\n )\n .map((el) => el.dataUrl),\n );\n\n // \u2500\u2500 5. Convert parsed elements to Excalidraw elements \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.(\"Inserting elements\u2026\");\n\n const newEls: SceneElement[] = [];\n const imageFiles: Parameters<typeof app.addFiles>[0] = [];\n\n for (const pel of parsedEls) {\n if (pel.kind === \"rectangle\") {\n const el = newElement({\n type: \"rectangle\",\n x: pel.x,\n y: pel.y,\n width: pel.width,\n height: pel.height,\n backgroundColor: pel.backgroundColor,\n fillStyle: \"solid\" as const,\n strokeColor: pel.strokeColor,\n strokeWidth: pel.strokeWidth,\n opacity: pel.opacity,\n roughness: 0,\n ...(pel.borderRadius > 0\n ? { roundness: { type: 3, value: pel.borderRadius } }\n : {}),\n customData: { generatedByHtml: true },\n });\n newEls.push({ ...el, frameId } as SceneElement);\n } else if (pel.kind === \"ellipse\") {\n const el = newElement({\n type: \"ellipse\",\n x: pel.x,\n y: pel.y,\n width: pel.width,\n height: pel.height,\n backgroundColor: pel.backgroundColor,\n fillStyle: \"solid\" as const,\n strokeColor: pel.strokeColor,\n strokeWidth: pel.strokeWidth,\n opacity: pel.opacity,\n roughness: 0,\n customData: { generatedByHtml: true },\n });\n newEls.push({ ...el, frameId } as SceneElement);\n } else if (pel.kind === \"text\") {\n const el = newTextElement({\n x: pel.x,\n y: pel.y,\n text: pel.text,\n styleRanges: pel.styleRanges,\n fontSize: pel.fontSize,\n fontFamily: pel.fontFamily,\n strokeColor: pel.color,\n textAlign: pel.textAlign,\n width: pel.width,\n opacity: pel.opacity,\n roughness: 0,\n customData: { generatedByHtml: true },\n });\n newEls.push({ ...el, frameId } as SceneElement);\n } else if (pel.kind === \"image\") {\n const isProduct = productDataUrls.has(pel.dataUrl);\n const adjusted = isProduct\n ? enforceMinProductBox({\n x: pel.x - currentPos.x,\n y: pel.y - currentPos.y,\n width: pel.width,\n height: pel.height,\n targetW: tgtW,\n targetH: tgtH,\n })\n : {\n x: pel.x - currentPos.x,\n y: pel.y - currentPos.y,\n width: pel.width,\n height: pel.height,\n };\n const fileId2 = nanoid() as FileId;\n // Preserve natural aspect ratio \u2014 products get a target-aware bbox so\n // they stay prominent without regenerating the image asset.\n const { width: natW, height: natH } = await getImageNaturalDimensions(\n pel.dataUrl,\n );\n const {\n relX,\n relY,\n width: imgW,\n height: imgH,\n } = containImageInBbox(natW, natH, adjusted.width, adjusted.height);\n const el = newImageElement({\n type: \"image\",\n x: currentPos.x + adjusted.x + relX,\n y: currentPos.y + adjusted.y + relY,\n width: imgW,\n height: imgH,\n fileId: fileId2,\n status: \"pending\",\n scale: [1, 1],\n customData: {\n generatedByHtml: true,\n ...(isProduct ? { slotType: \"product\" } : {}),\n },\n });\n newEls.push({ ...el, frameId } as SceneElement);\n imageFiles.push({\n id: fileId2,\n dataURL: pel.dataUrl as DataURL,\n mimeType: getMimeTypeFromDataURL(pel.dataUrl) as ValueOf<\n typeof IMAGE_MIME_TYPES\n >,\n created: Date.now(),\n lastRetrieved: Date.now(),\n });\n }\n }\n\n // \u2500\u2500 6. Remove any previously-inserted HTML elements for this frame \u2500\u2500\u2500\u2500\u2500\u2500\n // (on reviewer loop rounds we replace the prior attempt)\n if (reviewRounds > 0) {\n const beforeReplace = app.getSceneElements() as SceneElement[];\n const withoutOldHtml = beforeReplace.filter((el) => {\n const elAny = el as ExcalidrawElement & {\n frameId?: string;\n customData?: Record<string, unknown>;\n };\n return !(\n elAny.frameId === frameId && elAny.customData?.generatedByHtml\n );\n });\n app.updateScene({ elements: withoutOldHtml });\n }\n\n // \u2500\u2500 7. Z-order insertion: bg rects before bg image, fg after \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const frameArea = tgtW * tgtH;\n const bgEls: SceneElement[] = [];\n const fgEls: SceneElement[] = [];\n\n for (const el of newEls) {\n const isLargeRect =\n el.type === \"rectangle\" && el.width * el.height >= frameArea * 0.25;\n if (isLargeRect) {\n bgEls.push(el);\n } else {\n fgEls.push(el);\n }\n }\n\n const finalEls = app.getSceneElements() as SceneElement[];\n const frameIdx2 = finalEls.findIndex((el) => el.id === frameId);\n\n const existingChildren = getFrameChildren(finalEls, frameId);\n const firstChildIdx =\n existingChildren.length > 0\n ? finalEls.findIndex((el) => el.id === existingChildren[0].id)\n : frameIdx2 + 1;\n const afterLastChildIdx =\n existingChildren.length > 0\n ? finalEls.findIndex(\n (el) => el.id === existingChildren[existingChildren.length - 1].id,\n ) + 1\n : frameIdx2 + 1;\n\n // Layout: [frame] [bgEls (solid rects)] [bg image] [fgEls (text/images/shapes)]\n const withAll = [\n ...finalEls.slice(0, firstChildIdx),\n ...bgEls,\n ...finalEls.slice(firstChildIdx, afterLastChildIdx),\n ...fgEls,\n ...finalEls.slice(afterLastChildIdx),\n ];\n\n const allNew = [...bgEls, ...fgEls];\n syncMovedIndices(withAll, arrayToMap(allNew));\n app.updateScene({ elements: withAll });\n\n if (imageFiles.length > 0) {\n app.addFiles(imageFiles);\n }\n\n debug(\n `[AutoResize] ${debugLabel} \u2014 inserted ${newEls.length} elements (${bgEls.length} bg, ${fgEls.length} fg)`,\n );\n\n // \u2500\u2500 8. Reviewer loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (reviewRounds >= MAX_REVIEW_ROUNDS) {\n debug(`[AutoResize] ${debugLabel} \u2014 reviewer cap reached, finalising`);\n if (lastFeedback && lastFeedback.issues.length > 0) {\n debug(\n `[AutoResize] ${debugLabel} \u2014 WARNING: Giving up with ${lastFeedback.issues.length} unresolved issue(s)`,\n );\n lastFeedback.issues.forEach((issue, i) => {\n debug(\n `[AutoResize] ${debugLabel} \u2014 Unresolved ${i + 1}: [${\n issue.severity\n }] ${issue.element}: ${issue.problem}`,\n );\n });\n }\n break;\n }\n\n const screenshot = await captureFrameScreenshotFromApp(app, frameId);\n if (!screenshot) {\n debug(\n `[AutoResize] ${debugLabel} \u2014 screenshot unavailable, skipping reviewer`,\n );\n break;\n }\n\n onProgress?.(\"Reviewing layout\u2026\");\n try {\n const feedback = await runReviewerAgent({\n screenshotDataUrl: screenshot,\n htmlSource: html,\n frame: { id: frameId, width: tgtW, height: tgtH },\n userBrief: `Auto-resize ad from ${Math.round(\n srcFrame.width,\n )}\u00D7${Math.round(srcFrame.height)} to ${tgtW}\u00D7${tgtH}`,\n chatUrl: autoResizeUrl,\n reviewerModel: reviewerModel ?? chatModel,\n signal,\n iteration: reviewRounds + 1,\n });\n\n reviewRounds++;\n\n if (feedback.approved) {\n debug(\n `[AutoResize] ${debugLabel} \u2014 reviewer approved (round ${reviewRounds})`,\n );\n break;\n }\n\n debug(\n `[AutoResize] ${debugLabel} \u2014 reviewer found ${feedback.issues.length} issue(s), regenerating\u2026`,\n );\n // Log each issue for debugging\n feedback.issues.forEach((issue, i) => {\n debug(\n `[AutoResize] ${debugLabel} \u2014 Issue ${\n i + 1\n }: [${issue.severity.toUpperCase()}] ${issue.element}: ${\n issue.problem\n }`,\n );\n debug(\n `[AutoResize] ${debugLabel} \u2014 Fix: ${issue.preciseInstruction}`,\n );\n });\n lastFeedback = feedback;\n onProgress?.(\"Refining layout\u2026\");\n } catch {\n // Reviewer unavailable \u2014 accept current output\n debug(`[AutoResize] ${debugLabel} \u2014 reviewer error, accepting output`);\n break;\n }\n }\n\n return { frameId };\n}\n\n// ===========================================================================\n// RETIRED \u2014 image-generation resize path\n// ---------------------------------------------------------------------------\n// `runAutoResize` no longer calls this. It recomposed the whole ad as a single\n// flat image per dimension, which loses editability, re-renders text (often\n// re-wording it), drifts brand colours, and \u2014 for strip formats \u2014 cannot work\n// at all, because no image model supports 4:1 / 1:8.\n//\n// It is also currently non-functional: it routes through `AGENT_IMAGE_MODEL`\n// (`openai/gpt-image-2.5-flare`), which OpenRouter serves only from `/api/v1/images`\n// and 404s on `chat/completions`. Every call failed, and the Tier-2 fallback\n// shipped `blurExtendLetterbox` output \u2014 the source ad pasted at native size\n// into a blurred field.\n//\n// Kept (unwired) only so its unit tests keep documenting the old behaviour and\n// so the switch is reversible. Do NOT re-wire it without first fixing the\n// endpoint. `callCustomResizeProxy`, `callTier1CropProxy`,\n// `buildAutoResizeImagePrompt` and `isBigAspectRatioDelta` belong to it.\n// ===========================================================================\n// ---------------------------------------------------------------------------\n// Per-dimension orchestrator (image-generation path)\n// ---------------------------------------------------------------------------\n\n// Grok Imagine supports only 1K/2K. Gemini and Sourceful support 0.5K\u20132K.\n// Map the area-optimal resolution down to what the active model can produce.\nfunction clampResolutionForModel(\n res: \"0.5K\" | \"1K\" | \"2K\" | \"4K\",\n modelId: string,\n): \"0.5K\" | \"1K\" | \"2K\" {\n const isGrok = modelId.startsWith(\"x-ai/\");\n if (isGrok) {\n // Grok only supports 1K and 2K\n return res === \"4K\" || res === \"2K\" ? \"2K\" : \"1K\";\n }\n // Gemini/Sourceful support 0.5K, 1K, 2K; cap 4K at 2K\n return res === \"4K\" ? \"2K\" : (res as \"0.5K\" | \"1K\" | \"2K\");\n}\n\nexport function buildAutoResizeImagePrompt(\n srcW: number,\n srcH: number,\n tgtW: number,\n tgtH: number,\n aspectRatio: string,\n brandImages?: {\n logo?: string;\n fontPreview?: string;\n fontUrl?: string;\n name?: string;\n },\n sourceTextColors?: string[],\n): string {\n // The reference images are attached in this order (see callOpenRouterImageAPI):\n // [sourceAd] [logo?] [text]\n //\n // The source ad leads: it is the design being re-fit, and the reference\n // budget never sheds it. The numbering below matches the manifest the SDK\n // appends, so the two lists always agree.\n //\n // The brand FONT PREVIEW is deliberately not referenced here, and the resize\n // path does not attach it (see runAutoResizeForDimension). That preview is a\n // coloured, bold-italic type sample; instructing an image model to render all\n // on-image text \"in this exact font\" hands it a colour and a weight along\n // with the letterforms, and resized ads came back with recoloured copy. On a\n // resize the source ad already carries the brand's real typeface at its real\n // colour, so the source is the better \u2014 and only \u2014 type reference.\n const refs: string[] = [\n \"the SOURCE AD CREATIVE that you must recompose (this is the design to adapt)\",\n ];\n if (brandImages?.logo) {\n refs.push(\n \"the brand LOGO (reproduce it exactly \u2014 do not distort, recolour, or omit it)\",\n );\n }\n\n // Exact hexes, when the source frame still has live text elements to read\n // them from. A flat generated ad has none, in which case the prose rule below\n // carries the contract on its own.\n const uniqueTextColors = Array.from(\n new Set(\n (sourceTextColors ?? [])\n .map((c) => c.trim().toLowerCase())\n .filter((c) => c.length > 0),\n ),\n );\n const textColorLock = uniqueTextColors.length\n ? `\\n\u2022 EXACT TEXT COLOURS: the copy in the source is set in ${uniqueTextColors.join(\n \", \",\n )}. Reproduce these exact values. Do not substitute a brand colour, a \"close enough\" shade, or a higher-contrast alternative for any of them.`\n : \"\";\n\n const refList = refs.map((r, i) => ` ${i + 1}. ${r}`).join(\"\\n\");\n\n const brandNameRule = brandImages?.name\n ? `\\nBRAND: This ad belongs to the brand \"${brandImages.name}\". All visual elements, copy, and style must reflect this brand exclusively \u2014 do not introduce any other brand identity.`\n : \"\";\n\n return `You are an expert ad designer. You are given the following reference images, in order:\n${refList}\n\nTASK: Recreate the SOURCE AD CREATIVE as a single, polished advertisement recomposed for a ${tgtW}\u00D7${tgtH} pixel canvas (aspect ratio ${aspectRatio}). The source was designed at ${srcW}\u00D7${srcH}.${brandNameRule}\n\nTHIS IS A RE-FIT, NOT A RE-DESIGN. The only thing you are allowed to change is where elements sit and how big they are. Everything else must survive untouched.\n\nCOLOUR AND STYLE LOCK \u2014 the SOURCE AD CREATIVE is the sole authority for every colour, typeface, and treatment in the output:\n\u2022 Do NOT recolour anything. Every colour in the output \u2014 text, background, buttons, shapes, product, logo \u2014 must be sampled from the source ad and reproduced exactly. Do not \"improve\", harmonise, saturate, brighten, darken, or increase the contrast of any colour.\n\u2022 Typography comes from the SOURCE AD, not from any other reference: match the letterforms, weight, style, and colour of the copy exactly as it appears in the source.${textColorLock}\n\u2022 Do NOT restyle elements: no new shadows, gradients, outlines, glows, rounded corners, or texture that the source does not already have.\n\u2022 If you are unsure what a colour should be, take it from the source ad. Never invent one.\n\nPLACEMENT LOCK \u2014 the SOURCE AD CREATIVE is also the authority for where things sit. The source encodes the brand's own art direction; your job is to re-fit it, not to re-art-direct it:\n\u2022 Keep every element in the same region of the canvas it occupies in the source, and keep the source's relative arrangement and reading order. If an element sits in a given corner, edge, or side in the source, it belongs in that same corner, edge, or side of the output.\n\u2022 THE LOGO GOES WHERE THE SOURCE PUTS IT. Do not relocate it to a corner you consider more conventional, and do not resize it to some standard share of the canvas \u2014 keep its position and its visual prominence relative to the rest of the ad.\n\u2022 Keep the relative size hierarchy of the source: whatever is dominant there stays dominant here, and whatever is secondary stays secondary.\n\u2022 Change an element's position or size ONLY where the new aspect ratio genuinely forces it \u2014 because the source arrangement no longer physically fits. When it is forced, make the smallest change that fits, and preserve the source's ordering along the axis that still has room.\n\nHARD REQUIREMENTS:\n\u2022 Preserve the original message: keep the SAME headline, subheadline, CTA and any other copy, spelled exactly as in the source. Do not invent new text or drop existing text.\n\u2022 Preserve the brand exactly as the source renders it: same colours, same logo, same typeface. Render all text crisply and legibly.\n\u2022 Preserve ALL visual elements from the source without exception \u2014 every text element, image, logo, product shot, CTA, and graphic must appear in the recomposed output. Do NOT drop, merge, or omit any element. The number of distinct visual components in the output must exactly match the source.\n\u2022 NEVER DUPLICATE OR REPEAT ANY ELEMENT: Each element from the source must appear EXACTLY ONCE in the output \u2014 one logo, one headline, one product image, one CTA. If the new canvas is taller or wider and space remains after placing all elements, leave that space as part of the background. Do NOT fill empty space by repeating, tiling, or copying any element a second time. Duplicating content to fill the canvas is strictly forbidden.\n\u2022 LOGO INTEGRITY: The brand logo must appear exactly as supplied \u2014 same shape, colours, and proportions. Do NOT alter, recolour, distort, or replace it. Do NOT squish or stretch the logo horizontally or vertically.\n\u2022 Do NOT squish, stretch, or distort any element horizontally or vertically \u2014 maintain the natural aspect ratio of every visual, logo, product image, and text block.\n\u2022 RE-FIT the composition to the new aspect ratio \u2014 rescale and, only where the geometry forces it, reposition elements so the layout sits comfortably in ${tgtW}\u00D7${tgtH} while still reading as the same ad. Do NOT simply stretch, squash, or letterbox the original, and do NOT take the change of format as licence to redesign the layout. If the new aspect ratio leaves empty areas after placing all elements, those areas should be filled with the background colour or a natural extension of the background \u2014 never with repeated ad content.\n\u2022 NO BORDER OR FRAME: the ad's background must extend all the way to every edge of the ${tgtW}\u00D7${tgtH} canvas. Do NOT add a decorative border, outline, frame, vignette, or dim band around the ad \u2014 even as a stylistic flourish. The output should look like a single continuous ad filling the canvas, not a smaller ad pasted onto a bordered/framed backdrop.\n\u2022 Maintain professional advertising quality: balanced layout, clear visual hierarchy, generous but purposeful spacing.\n\nOutput ONLY the final recomposed ad image at ${tgtW}\u00D7${tgtH}.`;\n}\n\nasync function runAutoResizeForDimension(opts: {\n targetWidth: number;\n targetHeight: number;\n placementX: number;\n placementY: number;\n app: AppClassProperties;\n autoResizeUrl: string;\n customResizeUrl?: string;\n tier1CropUrl?: string;\n agentImageModel?: string;\n sourceFrame: ExcalidrawFrameLikeElement;\n sourceScreenshot: string | null;\n brandImages?: {\n logo?: string;\n fontPreview?: string;\n fontUrl?: string;\n name?: string;\n };\n onProgress?: (phase: string) => void;\n signal?: AbortSignal;\n debugLabel?: string;\n existingFrameId?: string;\n}): Promise<{ frameId: string }> {\n const {\n targetWidth: tgtW,\n targetHeight: tgtH,\n placementX,\n placementY,\n app,\n autoResizeUrl,\n customResizeUrl,\n tier1CropUrl,\n agentImageModel,\n sourceFrame,\n sourceScreenshot,\n brandImages,\n onProgress,\n signal,\n debugLabel = `${tgtW}\u00D7${tgtH}`,\n existingFrameId,\n } = opts;\n\n const imageModel = agentImageModel || DEFAULT_BG_REGEN_MODEL;\n\n // \u2500\u2500 1. Create the new frame (or reuse the pre-created one) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let frameId: string;\n if (existingFrameId) {\n frameId = existingFrameId;\n } else {\n const newFrame = newFrameElement({\n ...FRAME_STYLE,\n x: placementX,\n y: placementY,\n width: tgtW,\n height: tgtH,\n opacity: 100,\n locked: false,\n name: `${tgtW}\u00D7${tgtH}`,\n });\n const currentEls = app.getSceneElements() as SceneElement[];\n const withFrame = [...currentEls, newFrame as SceneElement];\n syncMovedIndices(withFrame, arrayToMap([newFrame as SceneElement]));\n app.updateScene({ elements: withFrame });\n frameId = newFrame.id;\n }\n\n const getCurrentFramePosition = (): { x: number; y: number } => {\n const currentEls = app.getSceneElements();\n const currentFrame = currentEls.find((el) => el.id === frameId);\n return currentFrame\n ? { x: currentFrame.x, y: currentFrame.y }\n : { x: placementX, y: placementY };\n };\n\n if (!sourceScreenshot) {\n throw new Error(\"Source frame screenshot unavailable\");\n }\n\n // \u2500\u2500 2. Generate the recomposed ad via image generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Extreme aspect-ratio deltas risk the standard cover+crop flow below\n // cutting foreground elements \u2014 route those to the host's Tier-2\n // custom-resize pipeline instead, when configured.\n const useBigDeltaPipeline =\n !!customResizeUrl && isBigAspectRatioDelta(tgtW, tgtH);\n\n let generatedDataUrl: string;\n let placedAtExactTargetSize = false;\n\n if (useBigDeltaPipeline) {\n debug(\n `[AutoResize] ${debugLabel} \u2014 big AR delta, routing to Tier-2 custom-resize`,\n );\n onProgress?.(\"Recomposing for extreme aspect ratio\u2026\");\n generatedDataUrl = await callCustomResizeProxy(\n customResizeUrl!,\n sourceScreenshot,\n tgtW,\n tgtH,\n signal,\n );\n // The host pipeline already returns an image cropped/scaled to the exact\n // target pixels \u2014 no further fit/crop needed.\n placedAtExactTargetSize = true;\n } else {\n const aspectRatio = nearestGeminiAspectRatio(tgtW, tgtH);\n // Same enum trap as the background path above: ask gpt-image-* for the\n // target's real pixels rather than a ratio label it may not accept.\n const exactSize = resolveImageOutputSize(imageModel, tgtW, tgtH);\n const resolution = clampResolutionForModel(\n selectOptimalResolution(tgtW, tgtH, aspectRatio),\n imageModel,\n );\n\n // The brand font preview is withheld on the resize path. It is a coloured\n // (#1a3a3a), bold, italic type sample, and the instruction that goes with\n // it \u2014 \"render every piece of on-image text in this exact font\" \u2014 cannot\n // separate letterforms from the colour and weight the model also sees. The\n // source ad already contains the brand's real typeface at its real colour,\n // so it is the correct type reference for a re-fit. The logo is still\n // passed: its instruction is scoped to the logo alone.\n const resizeBrandImages = brandImages\n ? { ...brandImages, fontPreview: undefined, fontPreviewBody: undefined }\n : undefined;\n\n // Exact hexes when the frame still has live text elements (a decomposed\n // \"editable\" ad). A flat generated ad yields none and the prompt falls back\n // to its prose colour lock.\n const sourceTextColors = extractFrameInfo(\n app.getSceneElements() as readonly ExcalidrawElement[],\n sourceFrame,\n app.files,\n ).foreground.flatMap((el) => (el.kind === \"text\" ? [el.color] : []));\n\n debug(\n `[AutoResize] ${debugLabel} \u2014 image-gen recompose (model=${imageModel}, ar=${aspectRatio}, res=${resolution}, logo=${!!resizeBrandImages?.logo}, textColors=${\n sourceTextColors.length\n })`,\n );\n\n onProgress?.(\"Generating ad\u2026\");\n const prompt = buildAutoResizeImagePrompt(\n Math.round(sourceFrame.width),\n Math.round(sourceFrame.height),\n tgtW,\n tgtH,\n aspectRatio,\n resizeBrandImages,\n sourceTextColors,\n );\n\n generatedDataUrl = await callOpenRouterImageAPI(\n prompt,\n imageModel,\n exactSize.imageSize ? null : aspectRatio,\n exactSize.imageSize ?? resolution,\n false,\n autoResizeUrl,\n undefined,\n signal,\n resizeBrandImages,\n {\n referenceMode: \"user-priority\",\n // A re-fit is governed by the ad being re-fit. It leads the array and\n // is never shed; the logo sits behind it and yields first if the\n // payload runs out of room.\n sourceImageDataUrl: sourceScreenshot,\n protectLogo: false,\n // No product photos or other ad creatives here \u2014 this is a re-fit of\n // one specific ad, and unrelated brand imagery would pull it off-model.\n hostBrandAssetSlots: 0,\n hostAdReferenceSlots: 0,\n },\n );\n\n // The blind dead-center cover-crop below can cut real content near the\n // edges (e.g. a headline or CTA) whenever the target isn't an exact\n // match for the nearest native ratio. When the host has wired the\n // Tier-1 element-aware crop proxy, use it instead \u2014 it detects where\n // content actually landed and picks a crop that keeps it all inside,\n // escalating to Tier 2 internally if no safe crop exists.\n if (tier1CropUrl) {\n debug(`[AutoResize] ${debugLabel} \u2014 Tier-1 element-aware crop`);\n onProgress?.(\"Fitting ad to frame\u2026\");\n generatedDataUrl = await callTier1CropProxy(\n tier1CropUrl,\n generatedDataUrl,\n sourceScreenshot,\n tgtW,\n tgtH,\n signal,\n );\n // The host already returns an image cropped/scaled to the exact\n // target pixels \u2014 no further fit/crop needed.\n placedAtExactTargetSize = true;\n }\n }\n\n // \u2500\u2500 3. Place the generated image as a full-frame layer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.(\"Placing ad\u2026\");\n const { width: imgNatW, height: imgNatH } = await getImageNaturalDimensions(\n generatedDataUrl,\n );\n const {\n relX,\n relY,\n width: imgW,\n height: imgH,\n } = placedAtExactTargetSize\n ? { relX: 0, relY: 0, width: tgtW, height: tgtH }\n : fitImageToFrame(imgNatW, imgNatH, tgtW, tgtH);\n\n const currentPos = getCurrentFramePosition();\n const fileId = nanoid() as FileId;\n const imgEl = newImageElement({\n type: \"image\",\n x: currentPos.x + relX,\n y: currentPos.y + relY,\n width: imgW,\n height: imgH,\n fileId,\n status: \"pending\",\n scale: [1, 1],\n customData: { autoResizeBg: true, generatedByImageGen: true },\n });\n\n const elsAfterFrame = app.getSceneElements() as SceneElement[];\n const insertPos = findInsertAfterFrame(elsAfterFrame, frameId);\n const withImg = [\n ...elsAfterFrame.slice(0, insertPos),\n { ...imgEl, frameId } as SceneElement,\n ...elsAfterFrame.slice(insertPos),\n ];\n syncMovedIndices(withImg, arrayToMap([imgEl as SceneElement]));\n app.updateScene({ elements: withImg });\n\n app.addFiles([\n {\n id: fileId,\n dataURL: generatedDataUrl as DataURL,\n mimeType: getMimeTypeFromDataURL(generatedDataUrl) as ValueOf<\n typeof IMAGE_MIME_TYPES\n >,\n created: Date.now(),\n lastRetrieved: Date.now(),\n },\n ]);\n\n debug(`[AutoResize] ${debugLabel} \u2014 DONE (image-gen)`);\n return { frameId };\n}\n\n// ---------------------------------------------------------------------------\n// Pre-create all frames upfront (call before runAutoResize for instant feedback)\n// ---------------------------------------------------------------------------\n\nexport type PreCreatedFrameInfo = {\n frameId: string;\n x: number;\n y: number;\n width: number;\n height: number;\n};\n\nexport function preCreateAllFrames(\n targetDimensions: TargetDimension[],\n sourceFrame: ExcalidrawFrameLikeElement,\n app: AppClassProperties,\n): PreCreatedFrameInfo[] {\n const startX = sourceFrame.x + sourceFrame.width + 80;\n\n const frameInfos = targetDimensions.map((dim, i) => {\n const y = targetDimensions\n .slice(0, i)\n .reduce((acc, d) => acc + d.height + 40, sourceFrame.y);\n const newFrame = newFrameElement({\n ...FRAME_STYLE,\n x: startX,\n y,\n width: dim.width,\n height: dim.height,\n opacity: 100,\n locked: false,\n name: dim.label ?? `${dim.width}\u00D7${dim.height}`,\n });\n return {\n frame: newFrame as SceneElement,\n x: startX,\n y,\n width: dim.width,\n height: dim.height,\n };\n });\n\n // Batch-insert all frames at once for a single scene update\n const currentEls = app.getSceneElements() as SceneElement[];\n const newFrameEls = frameInfos.map((fi) => fi.frame);\n const withFrames = [...currentEls, ...newFrameEls];\n syncMovedIndices(withFrames, arrayToMap(newFrameEls));\n app.updateScene({ elements: withFrames });\n\n return frameInfos.map((fi) => ({\n frameId: fi.frame.id,\n x: fi.x,\n y: fi.y,\n width: fi.width,\n height: fi.height,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Top-level entry point\n// ---------------------------------------------------------------------------\n\nexport async function runAutoResize(opts: {\n sourceFrameId: string;\n targetDimensions: TargetDimension[];\n app: AppClassProperties;\n autoResizeUrl: string;\n /** Legacy option. Native Auto Resize uses RESIZE_MODEL (Grok 4.6). */\n chatModel?: string;\n /** Legacy option. Native Auto Resize uses RESIZE_MODEL (Grok 4.6). */\n reviewerModel?: string;\n /** Image model used ONLY to outpaint photographic backgrounds. */\n outpaintModel?: string;\n customFontMap?: Record<string, number>;\n onBeforeAutoResize?: () => Promise<{ allowed: boolean; error?: string }>;\n onAfterAutoResize?: () => void;\n onProgress?: ProgressCallback;\n signal?: AbortSignal;\n /** Pass the result of preCreateAllFrames() to skip redundant frame creation */\n preCreatedFrameInfos?: PreCreatedFrameInfo[];\n}): Promise<DimensionResult[]> {\n const { runNativeAutoResize } = await import(\"./nativeResizeAgent\");\n return runNativeAutoResize(opts);\n}\n", "// ---------------------------------------------------------------------------\n// HTML \u2192 Excalidraw element parser\n//\n// Renders HTML in a hidden off-screen iframe, then uses the browser's own\n// layout engine (getBoundingClientRect + getComputedStyle) to extract\n// exact pixel positions/sizes/colors and maps them to Excalidraw elements.\n//\n// Guardrail contract (system prompt enforces):\n// - All layer divs use `position: absolute; left/top/width/height` in px\n// - Root .canvas div uses `position: relative`\n// - No flexbox/grid layout on individual layer elements\n// - Colors are hex or rgba; gradients are CSS gradient functions\n// - No external URLs; background-image uses inline data URIs only\n// ---------------------------------------------------------------------------\n\nimport { normalizeStyleRanges } from \"@orangecatai/element\";\n\nimport type { TextRunStyle, TextStyleRange } from \"@orangecatai/element/types\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ParsedRectangle = {\n kind: \"rectangle\";\n x: number;\n y: number;\n width: number;\n height: number;\n backgroundColor: string; // hex or \"transparent\"\n backgroundGradient?: string; // raw CSS gradient string if present\n strokeColor: string;\n strokeWidth: number;\n opacity: number; // 0-100\n borderRadius: number; // px, single value (max of top-left)\n};\n\nexport type ParsedEllipse = {\n kind: \"ellipse\";\n x: number;\n y: number;\n width: number;\n height: number;\n backgroundColor: string; // hex or \"transparent\"\n backgroundGradient?: string; // raw CSS gradient string if present\n strokeColor: string;\n strokeWidth: number;\n opacity: number; // 0-100\n};\n\nexport type ParsedText = {\n kind: \"text\";\n x: number;\n y: number;\n width: number;\n height: number;\n text: string;\n fontSize: number;\n fontFamily: number; // Excalidraw font family enum: 4 = Liberation Sans (closest to Arial)\n color: string; // hex\n textAlign: \"left\" | \"center\" | \"right\";\n opacity: number;\n /** per-run overrides picked up from inline markup (<b>, <span style>, \u2026) */\n styleRanges?: readonly TextStyleRange[];\n};\n\nexport type ParsedImage = {\n kind: \"image\";\n x: number;\n y: number;\n width: number;\n height: number;\n dataUrl: string;\n};\n\nexport type ParsedElement =\n | ParsedRectangle\n | ParsedEllipse\n | ParsedText\n | ParsedImage;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst IFRAME_TIMEOUT_MS = 6000;\nconst MIN_DIMENSION_PX = 2;\n\nfunction colorToHex(color: string): string {\n if (!color || color === \"transparent\" || color === \"rgba(0, 0, 0, 0)\") {\n return \"transparent\";\n }\n // Already hex\n if (color.startsWith(\"#\")) {\n return color;\n }\n // rgb(r, g, b) or rgba(r, g, b, a)\n const match = color.match(\n /rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([0-9.]+))?\\)/,\n );\n if (!match) {\n return color;\n }\n const r = parseInt(match[1], 10);\n const g = parseInt(match[2], 10);\n const b = parseInt(match[3], 10);\n const hex = `#${r.toString(16).padStart(2, \"0\")}${g\n .toString(16)\n .padStart(2, \"0\")}${b.toString(16).padStart(2, \"0\")}`;\n return hex;\n}\n\nfunction cssAlphaToOpacity(color: string): number {\n const match = color.match(/rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*([0-9.]+)\\)/);\n if (match) {\n return Math.round(parseFloat(match[1]) * 100);\n }\n return 100;\n}\n\n/** Map CSS font-family string to Excalidraw font family enum.\n * customFontMap keys are lowercase trimmed font names \u2192 fontFamilyId.\n * Checked first so brand fonts take priority over the web-safe fallbacks.\n *\n * IMPORTANT: Check sans-serif keywords FIRST \u2014 'sans-serif' contains\n * the substring 'serif' and would otherwise be mis-routed to the\n * handwritten Excalifont. */\nfunction mapFontFamily(\n fontFamily: string,\n customFontMap?: Record<string, number>,\n): number {\n const lower = fontFamily.toLowerCase();\n if (customFontMap) {\n for (const [name, id] of Object.entries(customFontMap)) {\n if (lower.includes(name)) {\n return id;\n }\n }\n }\n // \u2500\u2500 Sans-serif families (must be checked BEFORE serif) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (\n lower.includes(\"sans-serif\") ||\n lower.includes(\"arial\") ||\n lower.includes(\"helvetica\") ||\n lower.includes(\"verdana\") ||\n lower.includes(\"impact\") ||\n lower.includes(\"nunito\") ||\n lower.includes(\"roboto\") ||\n lower.includes(\"inter\") ||\n lower.includes(\"liberation sans\") ||\n lower.includes(\"tahoma\") ||\n lower.includes(\"trebuchet\")\n ) {\n return 2; // Nunito / clean sans-serif\n }\n // \u2500\u2500 Serif / decorative families \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (\n lower.includes(\"georgia\") ||\n lower.includes(\"serif\") ||\n lower.includes(\"cursive\") ||\n lower.includes(\"script\") ||\n lower.includes(\"times\") ||\n lower.includes(\"playfair\")\n ) {\n return 1; // Excalifont (closest serif/hand-drawn)\n }\n // \u2500\u2500 Monospace \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (\n lower.includes(\"courier\") ||\n lower.includes(\"mono\") ||\n lower.includes(\"cascadia\")\n ) {\n return 3; // Comic Shanns (code-ish)\n }\n // Default: clean sans-serif\n return 2;\n}\n\n/** Apply CSS text-transform to raw text content */\nfunction applyTextTransform(text: string, transform: string): string {\n switch (transform) {\n case \"uppercase\":\n return text.toUpperCase();\n case \"lowercase\":\n return text.toLowerCase();\n case \"capitalize\":\n return text.replace(/\\b\\w/g, (c) => c.toUpperCase());\n default:\n return text;\n }\n}\n\nfunction parseFontSize(fontSize: string): number {\n const px = parseFloat(fontSize);\n return isNaN(px) ? 16 : Math.round(px);\n}\n\nfunction parseBorderRadius(style: CSSStyleDeclaration): number {\n const raw = style.borderTopLeftRadius || \"0\";\n const px = parseFloat(raw);\n return isNaN(px) ? 0 : Math.round(px);\n}\n\nfunction detectEllipse(\n style: CSSStyleDeclaration,\n width: number,\n height: number,\n): boolean {\n // Only detect as ellipse when border-radius is large enough on BOTH axes.\n // A capsule/stadium shape (border-radius covers only the short axis) stays\n // as a rounded rectangle \u2014 Excalidraw handles that natively.\n const raw = style.borderTopLeftRadius || \"0\";\n if (raw.includes(\"%\")) {\n const pct = parseFloat(raw);\n // 50% on both axes = perfect circle/ellipse\n if (!isNaN(pct) && pct >= 50) {\n return true;\n }\n } else {\n const px = parseFloat(raw);\n // Must cover BOTH half-width AND half-height to be a circle\n if (!isNaN(px) && px > 0 && px >= width / 2 && px >= height / 2) {\n return true;\n }\n }\n return false;\n}\n\nfunction isGradient(bgImage: string): boolean {\n return (\n bgImage !== \"none\" &&\n (bgImage.includes(\"linear-gradient\") || bgImage.includes(\"radial-gradient\"))\n );\n}\n\nfunction extractGradientColor(bgImage: string): string {\n // Extract color stops from the gradient\n const matches = [...bgImage.matchAll(/(#[\\da-fA-F]{3,8}|rgba?\\([^)]+\\))/g)];\n if (matches.length === 0) {\n return \"#1a1a2e\";\n }\n if (matches.length === 1) {\n return colorToHex(matches[0][1]);\n }\n\n // For repeating stripes/patterns, averaging them creates mud. Just use the first dominant color.\n if (bgImage.includes(\"repeating\")) {\n return colorToHex(matches[0][1]);\n }\n\n // Parse first and last hex stops and average RGB channels\n const first = colorToHex(matches[0][1]).replace(\"#\", \"\").padEnd(6, \"0\");\n const last = colorToHex(matches[matches.length - 1][1])\n .replace(\"#\", \"\")\n .padEnd(6, \"0\");\n\n const r1 = parseInt(first.slice(0, 2), 16);\n const g1 = parseInt(first.slice(2, 4), 16);\n const b1 = parseInt(first.slice(4, 6), 16);\n\n const r2 = parseInt(last.slice(0, 2), 16);\n const g2 = parseInt(last.slice(2, 4), 16);\n const b2 = parseInt(last.slice(4, 6), 16);\n\n const avg = (v1: number, v2: number) =>\n Math.round((v1 + v2) / 2)\n .toString(16)\n .padStart(2, \"0\");\n return `#${avg(r1, r2)}${avg(g1, g2)}${avg(b1, b2)}`;\n}\n\nfunction extractDataUri(bgImage: string): string | null {\n const match = bgImage.match(/url\\(\"?(data:[^\")]+)\"?\\)/);\n return match ? match[1] : null;\n}\n\nfunction getTextAlign(align: string): \"left\" | \"center\" | \"right\" {\n if (align === \"center\") {\n return \"center\";\n }\n if (align === \"right\" || align === \"end\") {\n return \"right\";\n }\n return \"left\";\n}\n\n/** Inline element tag names whose text should be merged into the parent text node */\nconst INLINE_TAGS = new Set([\"SPAN\", \"EM\", \"STRONG\", \"B\", \"I\", \"A\", \"LABEL\"]);\n\n/**\n * Collect the full visible text of an element, including inline children\n * (e.g., `<span>` for colour highlights) and `<br>` elements for newlines.\n * Block children are skipped.\n * Returns `null` if the element has no text-carrying inline content at all\n * (so background-only divs are not accidentally treated as text).\n */\nfunction getFullText(el: Element): string | null {\n return getFullTextWithStyles(el)?.text ?? null;\n}\n\n/**\n * Like `getFullText`, but also reports how inline children differ from the\n * parent (bold, colour, size, \u2026) as style ranges, so rich copy survives the\n * HTML -> canvas conversion instead of being flattened.\n */\nfunction getFullTextWithStyles(\n el: Element,\n getStyle?: (el: Element) => CSSStyleDeclaration,\n customFontMap?: Record<string, number>,\n): { text: string; styleRanges?: readonly TextStyleRange[] } | null {\n let text = \"\";\n let hasInlineContent = false;\n const ranges: TextStyleRange[] = [];\n\n const parentStyle = getStyle?.(el);\n\n const inlineStyle = (child: Element): TextRunStyle => {\n if (!getStyle || !parentStyle) {\n return {};\n }\n\n const style = getStyle(child);\n const run: TextRunStyle = {};\n\n const weight = parseInt(style.fontWeight, 10);\n const parentWeight = parseInt(parentStyle.fontWeight, 10);\n if (weight >= 600 && !(parentWeight >= 600)) {\n run.bold = true;\n }\n if (style.fontStyle !== \"normal\" && parentStyle.fontStyle === \"normal\") {\n run.italic = true;\n }\n const decoration = `${style.textDecorationLine || style.textDecoration}`;\n if (decoration.includes(\"underline\")) {\n run.underline = true;\n }\n if (decoration.includes(\"line-through\")) {\n run.strikethrough = true;\n }\n if (style.color && style.color !== parentStyle.color) {\n run.color = colorToHex(style.color);\n }\n if (style.fontSize && style.fontSize !== parentStyle.fontSize) {\n run.fontSize = parseFontSize(style.fontSize);\n }\n if (\n style.fontFamily &&\n style.fontFamily !== parentStyle.fontFamily &&\n customFontMap\n ) {\n run.fontFamily = mapFontFamily(\n style.fontFamily,\n customFontMap,\n ) as TextRunStyle[\"fontFamily\"];\n }\n\n return run;\n };\n\n for (const node of el.childNodes) {\n if (node.nodeType === Node.TEXT_NODE) {\n const t = node.textContent || \"\";\n if (t.trim()) {\n hasInlineContent = true;\n }\n text += t;\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n const child = node as Element;\n if (child.tagName === \"BR\") {\n text += \"\\n\";\n hasInlineContent = true;\n } else if (INLINE_TAGS.has(child.tagName)) {\n const t = child.textContent || \"\";\n if (t.trim()) {\n hasInlineContent = true;\n }\n const start = text.length;\n text += t;\n\n const style = inlineStyle(child);\n if (Object.keys(style).length) {\n ranges.push({ start, end: text.length, style });\n }\n }\n }\n }\n\n // Trim ONLY at the very ends, preserving internal newlines\n const leading = text.match(/^\\s+/)?.[0].length ?? 0;\n const trimmed = text.replace(/^\\s+|\\s+$/g, \"\");\n\n if (!hasInlineContent || !trimmed) {\n return null;\n }\n\n return {\n text: trimmed,\n styleRanges: normalizeStyleRanges(\n ranges.map((range) => ({\n start: range.start - leading,\n end: range.end - leading,\n style: range.style,\n })),\n trimmed.length,\n ),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Core parser\n// ---------------------------------------------------------------------------\n\n/**\n * Render `html` in an off-screen iframe and walk the DOM to produce an\n * array of ParsedElement objects, translated so the root .canvas div sits\n * at (frameX, frameY) in Excalidraw canvas coordinates.\n */\nexport async function htmlToExcalidrawElements(\n html: string,\n frameX: number,\n frameY: number,\n frameWidth: number,\n frameHeight: number,\n customFontMap?: Record<string, number>,\n): Promise<ParsedElement[]> {\n // \u2500\u2500 1. Create hidden iframe \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const iframe = document.createElement(\"iframe\");\n iframe.style.cssText = `\n position: fixed;\n left: -${frameWidth + 200}px;\n top: 0;\n width: ${frameWidth}px;\n height: ${frameHeight}px;\n border: none;\n visibility: hidden;\n `;\n document.body.appendChild(iframe);\n\n try {\n // \u2500\u2500 2. Inject HTML and wait for load \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => resolve(), IFRAME_TIMEOUT_MS);\n iframe.addEventListener(\n \"load\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n iframe.addEventListener(\n \"error\",\n () => {\n clearTimeout(timer);\n reject(new Error(\"iframe load error\"));\n },\n { once: true },\n );\n\n // Inject base styles to ensure browser defaults don't bleed into the Excalidraw parser\n // (e.g. browser default serif font would map to Excalifont)\n const baseStyles = `<style>\n * { box-sizing: border-box; font-family: Arial, Helvetica, sans-serif; }\n body { margin: 0; padding: 0; }\n </style>`;\n\n // console.log(\"=====================================\");\n // console.log(\"\uD83E\uDD16 AI GENERATED HTML FOR BANNER:\");\n // console.log(html);\n // console.log(\"=====================================\");\n\n iframe.srcdoc = baseStyles + html;\n });\n\n const doc = iframe.contentDocument;\n if (!doc) {\n return [];\n }\n\n // \u2500\u2500 3. Find root .canvas container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const root = doc.querySelector(\".canvas\") as HTMLElement | null;\n\n if (!root) {\n // Fallback: treat body as root\n }\n const rootEl = root ?? doc.body;\n const rootRect = rootEl.getBoundingClientRect();\n\n // \u2500\u2500 4. Walk all descendant elements \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const allEls = Array.from(rootEl.querySelectorAll(\"*\")) as HTMLElement[];\n const parsed: ParsedElement[] = [];\n\n for (const el of allEls) {\n const rect = el.getBoundingClientRect();\n\n // Skip invisible / zero-size elements\n if (rect.width < MIN_DIMENSION_PX || rect.height < MIN_DIMENSION_PX) {\n continue;\n }\n\n const style = iframe.contentWindow!.getComputedStyle(el);\n\n if (style.display === \"none\" || style.visibility === \"hidden\") {\n continue;\n }\n\n // Skip inline elements (span, em, etc.) whose text was already merged\n // into the parent by getFullText \u2014 processing them again would produce\n // duplicate / misaligned text elements.\n if (\n INLINE_TAGS.has(el.tagName) &&\n el.parentElement &&\n getFullText(el.parentElement) !== null\n ) {\n continue;\n }\n\n // Skip decorative elements with clip-path \u2014 Excalidraw cannot render\n // complex vector paths, and these elements (stars, custom shapes etc.)\n // would just show as tiny coloured squares.\n const clipPath =\n style.clipPath || (style as any).webkitClipPath || \"none\";\n if (clipPath !== \"none\" && clipPath.includes(\"polygon\")) {\n continue;\n }\n\n // Translate to Excalidraw canvas coordinates\n const x = Math.round(frameX + (rect.left - rootRect.left));\n const y = Math.round(frameY + (rect.top - rootRect.top));\n const w = Math.round(rect.width);\n const h = Math.round(rect.height);\n\n const globalOpacity = Math.round(parseFloat(style.opacity ?? \"1\") * 100);\n\n const bgImage = style.backgroundImage || \"none\";\n const bgColor = style.backgroundColor || \"transparent\";\n const borderColor = style.borderColor || \"transparent\";\n const borderWidth = parseFloat(style.borderWidth || \"0\");\n\n // \u2500\u2500 Image element (background-image: url(data:...)) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const dataUri = extractDataUri(bgImage);\n if (dataUri) {\n parsed.push({\n kind: \"image\",\n x,\n y,\n width: w,\n height: h,\n dataUrl: dataUri,\n });\n continue;\n }\n\n // \u2500\u2500 Text element \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // getFullText aggregates direct text AND inline children (span, em\u2026)\n const directTextWithStyles = getFullTextWithStyles(\n el,\n (node) => iframe.contentWindow!.getComputedStyle(node),\n customFontMap,\n );\n let directText = directTextWithStyles?.text ?? null;\n if (directText) {\n // Apply CSS text-transform (uppercase, lowercase, capitalize)\n const textTransform = style.textTransform || \"none\";\n directText = applyTextTransform(directText, textTransform);\n\n const textOpacity =\n globalOpacity < 100\n ? globalOpacity\n : cssAlphaToOpacity(style.color || \"rgb(0,0,0)\");\n const align = getTextAlign(style.textAlign || \"left\");\n // newTextElement treats opts.x as the visual anchor: center-x for\n // \"center\" alignment, right-x for \"right\". Pre-offset so text lands\n // inside the bounding box.\n let textX = x;\n if (align === \"center\") {\n textX = x + w / 2;\n } else if (align === \"right\") {\n textX = x + w;\n }\n\n parsed.push({\n kind: \"text\",\n x: textX,\n y,\n width: w,\n height: h,\n text: directText,\n styleRanges: directTextWithStyles?.styleRanges,\n fontSize: parseFontSize(style.fontSize || \"16px\"),\n fontFamily: mapFontFamily(style.fontFamily || \"\", customFontMap),\n color: colorToHex(style.color || \"#000000\"),\n textAlign: align,\n opacity: Math.min(globalOpacity, textOpacity),\n });\n // Don't skip \u2014 element might also have a background, fall through\n // to also emit a rectangle below.\n }\n\n // \u2500\u2500 Rectangle/background element \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const hasVisibleBg =\n bgColor !== \"transparent\" && bgColor !== \"rgba(0, 0, 0, 0)\";\n const hasGradient = isGradient(bgImage);\n const hasVisibleBorder =\n borderWidth > 0 &&\n borderColor !== \"transparent\" &&\n borderColor !== \"rgba(0, 0, 0, 0)\";\n\n if (hasVisibleBg || hasGradient || hasVisibleBorder) {\n const hexBg = hasGradient\n ? extractGradientColor(bgImage)\n : colorToHex(bgColor);\n const shouldBeEllipse = detectEllipse(style, w, h);\n\n const shapeEl: ParsedRectangle | ParsedEllipse = shouldBeEllipse\n ? {\n kind: \"ellipse\",\n x,\n y,\n width: w,\n height: h,\n backgroundColor:\n hasVisibleBg || hasGradient ? hexBg : \"transparent\",\n strokeColor: hasVisibleBorder\n ? colorToHex(borderColor)\n : \"transparent\",\n strokeWidth: hasVisibleBorder ? Math.round(borderWidth) : 0,\n opacity: globalOpacity,\n }\n : {\n kind: \"rectangle\",\n x,\n y,\n width: w,\n height: h,\n backgroundColor:\n hasVisibleBg || hasGradient ? hexBg : \"transparent\",\n strokeColor: hasVisibleBorder\n ? colorToHex(borderColor)\n : \"transparent\",\n strokeWidth: hasVisibleBorder ? Math.round(borderWidth) : 0,\n opacity: globalOpacity,\n borderRadius: parseBorderRadius(style),\n };\n\n if (hasGradient) {\n shapeEl.backgroundGradient = bgImage;\n }\n // Insert shape BEFORE the text (text must be on top)\n if (directText) {\n parsed.splice(parsed.length - 1, 0, shapeEl);\n } else {\n parsed.push(shapeEl);\n }\n }\n }\n\n // console.log(\"\uD83C\uDFAF PARSED EXCALIDRAW ELEMENTS:\");\n // console.log(parsed);\n // console.log(\"=====================================\");\n\n return parsed;\n } finally {\n document.body.removeChild(iframe);\n }\n}\n", "/**\n * Payload-size normalisation for images sent to the chat API.\n *\n * Every `image_url` content block in a chat request travels as a base64 data\n * URL embedded in JSON. Base64 inflates bytes by 4/3, and the host proxy runs\n * on Vercel, whose platform-level request cap is 4.5 MB \u2014 enforced at the edge\n * *before* the route handler runs, so it surfaces as an opaque 413 that never\n * reaches any of our error handling.\n *\n * Rather than budget-and-drop (which loses the user's images, the one thing\n * they actually asked the model to look at), every image is downscaled to a\n * size where the whole request comfortably fits. A 1280px-long-edge JPEG at\n * q0.78 lands around 100-150KB, so a request carrying the full complement of\n * user uploads + brand references + frame screenshot sits near 1MB \u2014 roughly\n * a quarter of the cap, with room for prompt text and tool schemas.\n *\n * This runs at payload-assembly time, not upload time, which is deliberate:\n * conversation history persisted before this existed still holds raw\n * multi-megabyte data URLs, and those are re-sent on every subsequent turn.\n * Normalising here fixes existing conversations, not just new uploads.\n */\n\nimport {\n dataUrlWireBytes,\n isRasterImageDataUrl,\n reencodeAtEdge,\n shrinkImageDataUrl,\n shrinkImageDataUrls,\n} from \"../../utils/imageCompression\";\n\n// Re-exported so existing importers of this module keep working; the\n// implementations now live in utils/imageCompression, shared with the\n// image-generation surfaces.\nexport {\n dataUrlWireBytes,\n isRasterImageDataUrl,\n shrinkImageDataUrl,\n shrinkImageDataUrls,\n};\n\n// ---------------------------------------------------------------------------\n// Request-level normalisation\n// ---------------------------------------------------------------------------\n\n/**\n * Hard ceiling for the serialised request body. Vercel rejects at 4.5 MB; this\n * leaves ~1 MB of headroom for the tool schemas (~70KB), system prompt (~9KB),\n * canvas element context, and accumulated tool-result strings.\n */\nconst MAX_REQUEST_WIRE_BYTES = 3.5 * 1024 * 1024;\n\ntype LooseMessage = { role: string; content: unknown; [k: string]: unknown };\n\nfunction isImagePart(part: unknown): part is { image_url: { url: string } } {\n return (\n !!part &&\n typeof part === \"object\" &&\n (part as { type?: unknown }).type === \"image_url\" &&\n typeof (part as { image_url?: { url?: unknown } }).image_url?.url ===\n \"string\"\n );\n}\n\n/** Every image part in the message list, in send order (oldest first). */\nfunction collectImageParts(\n messages: LooseMessage[],\n): { msgIndex: number; part: { image_url: { url: string } } }[] {\n const out: { msgIndex: number; part: { image_url: { url: string } } }[] = [];\n messages.forEach((m, msgIndex) => {\n if (!Array.isArray(m.content)) {\n return;\n }\n for (const part of m.content) {\n if (isImagePart(part)) {\n out.push({ msgIndex, part });\n }\n }\n });\n return out;\n}\n\n/**\n * Normalise every image in an outgoing request so the body cannot exceed the\n * platform request cap.\n *\n * Three escalating passes, so the common case costs nothing and the worst case\n * still succeeds rather than 413ing:\n *\n * 1. Downscale every image to {@link MAX_EDGE_PX}. Almost always sufficient \u2014\n * a full request of 9 images lands near 1MB.\n * 2. If still over (many images, or images that resisted re-encoding),\n * re-encode at successively lower quality.\n * 3. Only if *still* over, drop the oldest images \u2014 newest are the ones the\n * user is actually asking about. Images on the final message are never\n * dropped.\n *\n * Mutates the parts in place and returns the message array for convenience.\n */\nexport async function normalizeRequestImages<T extends { role: string }>(\n messages: T[],\n): Promise<T[]> {\n const loose = messages as unknown as LooseMessage[];\n const parts = collectImageParts(loose);\n if (parts.length === 0) {\n return messages;\n }\n\n // Pass 1 \u2014 standard downscale, in parallel.\n const shrunk = await Promise.all(\n parts.map(({ part }) => shrinkImageDataUrl(part.image_url.url)),\n );\n parts.forEach(({ part }, i) => {\n part.image_url.url = shrunk[i];\n });\n\n const bodySize = () => JSON.stringify(messages).length;\n if (bodySize() <= MAX_REQUEST_WIRE_BYTES) {\n return messages;\n }\n\n // Pass 2 \u2014 harder re-encode of the largest offenders first.\n for (const edge of [960, 720, 512]) {\n const ordered = [...parts].sort(\n (a, b) =>\n dataUrlWireBytes(b.part.image_url.url) -\n dataUrlWireBytes(a.part.image_url.url),\n );\n for (const { part } of ordered) {\n part.image_url.url = await reencodeAtEdge(part.image_url.url, edge);\n if (bodySize() <= MAX_REQUEST_WIRE_BYTES) {\n return messages;\n }\n }\n }\n\n // Pass 3 \u2014 last resort: shed oldest images, never the final message's.\n const lastMsgIndex = loose.length - 1;\n for (const { msgIndex, part } of parts) {\n if (msgIndex === lastMsgIndex) {\n continue;\n }\n const owner = loose[msgIndex];\n if (!Array.isArray(owner.content)) {\n continue;\n }\n owner.content = owner.content.filter((p) => p !== part);\n console.warn(\n \"[imagePayload] dropped an image from an earlier turn to fit the request cap\",\n );\n if (bodySize() <= MAX_REQUEST_WIRE_BYTES) {\n return messages;\n }\n }\n\n return messages;\n}\n\n/**\n * Build a user-facing message from a failed chat-proxy response.\n *\n * The previous handling assumed every non-OK status came from OpenRouter and\n * carried an OpenRouter-shaped JSON error. Platform-level rejections (notably\n * Vercel's 413 at the edge) return plain text, so `response.json()` threw and\n * the raw fallback `OpenRouter error 413` reached the user \u2014 naming the wrong\n * system and offering no action.\n */\nexport async function describeChatError(response: Response): Promise<string> {\n const raw = await response.text().catch(() => \"\");\n\n try {\n const parsed = JSON.parse(raw);\n if (parsed?.error?.message) {\n return parsed.error.message;\n }\n if (typeof parsed?.error === \"string\") {\n return parsed.error;\n }\n } catch {\n /* not JSON \u2014 fall through to status-based messages */\n }\n\n if (response.status === 413) {\n return \"This request was too large to send. Try attaching fewer images, or start a new chat to clear older ones.\";\n }\n if (response.status === 429) {\n return \"Rate limited \u2014 please wait a moment and try again.\";\n }\n if (response.status === 401 || response.status === 403) {\n return \"This request was not authorised. Please refresh and try again.\";\n }\n if (response.status >= 500) {\n return `The AI service is temporarily unavailable (${response.status}). Please try again.`;\n }\n\n const detail = raw.trim().slice(0, 200);\n return detail\n ? `Request failed (${response.status}): ${detail}`\n : `Request failed (${response.status}).`;\n}\n", "/**\n * reviewerAgent.ts\n *\n * Brand Governance Agent \u2014 stateless reviewer that runs after the main agent\n * generates a complete ad image (visuals + all copy baked in) and calls\n * finalize_ad. It receives a focused context (the rendered screenshot, frame\n * dimensions, brand guidelines, original user brief) and returns a precise\n * critique covering creative quality AND brand-governance compliance:\n * copy correctness, brand tone, logo usage, claim accuracy, legal mandatories,\n * offer disclaimers, regulatory sensitivity, prohibited language, competitive\n * claims, and data/privacy concerns.\n *\n * The reviewer is a separate LLM call from the main agent loop so it brings an\n * independent eye to the generated ad. Because the ad is a single image, every\n * fix is applied by regenerating the image with a revised prompt \u2014 there is no\n * HTML/CSS layer to patch.\n *\n * @deprecated note (2026-06-17): this reviewer previously audited an HTML text\n * layer composited over a text-free image and emitted CSS-only fixes. That\n * flow is gone \u2014 see the image-first agent loop in agentLoop.ts.\n */\n\nimport { normalizeRequestImages } from \"./imagePayload\";\n\nimport type { BrandContext } from \"../../types\";\n\n// ---------------------------------------------------------------------------\n// Exported types\n// ---------------------------------------------------------------------------\n\nexport type ReviewIssue = {\n /** Which design element or governance dimension is affected */\n element:\n | \"headline\"\n | \"subheadline\"\n | \"cta\"\n | \"image\"\n | \"layout\"\n | \"brand\"\n | \"brand_tone\"\n | \"logo\"\n | \"claim\"\n | \"legal\"\n | \"disclaimer\"\n | \"regulatory\"\n | \"prohibited_language\"\n | \"competitive_claim\"\n | \"privacy\";\n /** How bad it is */\n severity: \"critical\" | \"major\" | \"minor\";\n /** What is wrong \u2014 one sentence */\n problem: string;\n /** One-line direction for the fix */\n fix: string;\n /**\n * Precise instruction with numeric coordinates or px values.\n * Examples:\n * \"Reduce headline font-size from 72px to 48px\"\n * \"Move CTA button to x=380,y=440 (bottom-right quadrant)\"\n * \"Shift text container left boundary from x=0 to x=20 so it clears the image at x=260\"\n */\n preciseInstruction: string;\n};\n\nexport type ReviewerFeedback = {\n /** true = no actionable issues; main agent should finalize */\n approved: boolean;\n /** empty when approved */\n issues: ReviewIssue[];\n /** 1-2 sentence overall verdict */\n summary: string;\n /** which reviewer round produced this (1-based) */\n iteration: number;\n};\n\n// ---------------------------------------------------------------------------\n// System prompt\n// ---------------------------------------------------------------------------\n\nconst REVIEWER_SYSTEM_PROMPT = `You are the Brand Governance Agent \u2014 a Senior Ad Creative Director, Brand Compliance Officer, and Legal QA Auditor combined. You receive a rendered screenshot of a finished advertisement that was generated as a SINGLE AI image \u2014 the visuals AND all copy (headline, subheadline, CTA, body text, sign-off) are part of the image itself.\n\nYour job is to evaluate every output before it moves forward, covering BOTH creative quality AND brand-governance compliance. Return precise, actionable fixes that the art director will fold into a REVISED IMAGE-GENERATION PROMPT (the ad is regenerated, not edited element-by-element).\n\n## What you check (in priority order)\n\n### A \u2014 Creative Quality\n\n1. **Copy correctness** \u2014 Is every word of on-image text spelled correctly and rendered cleanly? AI image models frequently garble, misspell, duplicate, or drop letters. Flag any misspelled, malformed, gibberish, or truncated text and state the exact words that must appear.\n2. **Copy legibility** \u2014 Is the text large enough and high-contrast enough to read at a glance? Is it clear of busy areas of the image? Is the visual hierarchy right (headline dominant, CTA distinct)?\n3. **CTA prominence** \u2014 Is the call-to-action clearly readable and styled as an actionable element?\n4. **Composition & polish** \u2014 Balanced layout, adequate margins, no awkward cropping of subjects or text, professional advertising quality.\n\n### B \u2014 Brand Governance (check every dimension below)\n\n5. **Brand tone** \u2014 Does the copy register match the brand's prescribed tone-of-voice (e.g. friendly vs. authoritative, casual vs. formal, sentence length, vocabulary level, person/voice)? Flag tone drift even if individual words are correct.\n6. **Logo usage** \u2014 If a brand logo is visible in the ad: Is it correctly placed (not distorted, not overlapping busy areas)? Is it at an appropriate size? Has it been cropped, recolored, or stretched in violation of brand standards? If a logo was expected and is absent, flag that too.\n7. **Claim accuracy** \u2014 Are product or service claims in the copy factual and supportable? Flag superlatives (\"best\", \"fastest\", \"#1\") that are unqualified, exaggerated feature claims, and any copy that overpromises.\n8. **Legal mandatories** \u2014 Are required legal elements present where the brief or brand guidelines demand them (e.g. trademark symbols \u2122/\u00AE, copyright notices, required taglines, mandated disclosures)?\n9. **Offer disclaimers** \u2014 If the ad contains a promotional offer, price, discount, or time-limited deal, is the appropriate disclaimer or terms-and-conditions reference visible (e.g. \"T&Cs apply\", \"while stocks last\", \"offer ends DD/MM/YYYY\")?\n10. **Regulatory sensitivity** \u2014 Does the ad touch regulated categories (finance, health, alcohol, gambling, children, pharmaceuticals, food nutrition claims)? If so, flag any copy that would likely breach sector-specific advertising standards (e.g. health benefit claims without qualification, alcohol ads implying social success, financial promotions without risk warnings).\n11. **Prohibited language** \u2014 Is there any language that is explicitly banned by the brand's guidelines or that is generally off-limits in advertising (hate speech, discriminatory language, demeaning stereotypes, shock tactics beyond what the brief permits)?\n12. **Competitive claims** \u2014 Does the ad make comparative or disparaging claims about named competitors or competitor products? Unless the brief explicitly permits a named comparison, flag these as requiring legal review.\n13. **Data/privacy concerns** \u2014 Does the copy contain, imply, or reference personally identifiable information, user data, or surveillance-style targeting language that could raise GDPR/CCPA concerns or erode consumer trust?\n\n## Rules for your critique\n\n- **Express every fix as a prompt-level instruction**, e.g. \"Regenerate with the headline reading exactly 'Fresh Pasta, Made Daily' in a bold sans-serif\" or \"Add 'T&Cs apply' in small text at the bottom of the ad.\" Do NOT describe CSS, element coordinates, or canvas operations \u2014 there is no editable layer; the whole ad is regenerated from the prompt.\n- **Quote exact copy.** When text is wrong or missing, state the precise words that must appear so the next prompt can render them correctly.\n- If something is fine, do NOT invent issues. Approve confidently.\n- **User brief overrides brand guidelines.** If the user explicitly requested a color, style, or layout, treat it as intentional. Brand guidelines apply only where the brief is silent.\n- **NEVER suggest changes to the logo.** The logo is a supplied brand asset. Do NOT flag it as needing to be altered, recoloured, repositioned based on taste, or replaced. If you believe the logo is absent or severely distorted (e.g. unreadable), you may note it under the \"logo\" element, but your fix must only ask to reproduce it faithfully \u2014 never to redesign, recolour, or remove it.\n- **NEVER suggest changing the visual content of image elements.** Your mandate covers text copy correctness, legibility, legal mandatories, and brand tone. Do NOT suggest changing what an image depicts, what a product looks like, what the background scene contains, or the composition of any image asset. Only flag content that breaks a named compliance rule.\n- **Severity guide** \u2014 critical: blocks legal/compliance sign-off or is factually wrong; major: clear brand or quality failure that needs fixing before launch; minor: polish or preference that would improve the creative.\n\n## Approval criteria\n\nSet approved=true with an empty issues array only when ALL of the following are true:\n- All on-image copy is correctly spelled and matches the intended words\n- All text is legible (adequate size and contrast)\n- The CTA is clearly readable and distinct\n- Brand colors/typographic register are respected (where the brief is silent)\n- Brand tone-of-voice matches the prescribed register\n- Logo (if present) follows brand usage standards\n- No unqualified superlatives or unsupportable claims\n- All required legal mandatories and offer disclaimers are present\n- No regulatory red flags for the category\n- No prohibited language or inappropriate content\n- No unlicensed competitive claims or comparative disparagement\n- No data/privacy red flags in the copy\n- The composition looks intentional and professional\n\n## Output\n\nReturn ONLY a JSON object matching this schema exactly. No markdown, no prose, no code fences.\n\n{\n \"approved\": boolean,\n \"issues\": [\n {\n \"element\": \"headline\" | \"subheadline\" | \"cta\" | \"image\" | \"layout\" | \"brand\" | \"brand_tone\" | \"logo\" | \"claim\" | \"legal\" | \"disclaimer\" | \"regulatory\" | \"prohibited_language\" | \"competitive_claim\" | \"privacy\",\n \"severity\": \"critical\" | \"major\" | \"minor\",\n \"problem\": \"string \u2014 one sentence describing what is wrong\",\n \"fix\": \"string \u2014 one-line direction\",\n \"preciseInstruction\": \"string \u2014 a prompt-level instruction for the regenerated image, quoting exact copy where relevant\"\n }\n ],\n \"summary\": \"string \u2014 1-2 sentences overall verdict\"\n}\n\n## Example output (issues found)\n\n{\n \"approved\": false,\n \"issues\": [\n {\n \"element\": \"headline\",\n \"severity\": \"critical\",\n \"problem\": \"The headline renders as 'Frsh Psata' \u2014 the AI image garbled the text.\",\n \"fix\": \"Regenerate so the headline is spelled correctly.\",\n \"preciseInstruction\": \"Regenerate the ad with the headline reading EXACTLY 'Fresh Pasta' in a bold sans-serif, crisp and legible, no misspellings.\"\n },\n {\n \"element\": \"claim\",\n \"severity\": \"critical\",\n \"problem\": \"Copy reads 'The World's Best Pasta' \u2014 an unqualified superlative with no supporting evidence.\",\n \"fix\": \"Remove or qualify the superlative.\",\n \"preciseInstruction\": \"Replace 'The World's Best Pasta' with a specific, supportable claim such as 'Award-Winning Fresh Pasta' or 'Loved by 10,000 Customers'.\"\n },\n {\n \"element\": \"disclaimer\",\n \"severity\": \"major\",\n \"problem\": \"The ad promotes '50% off today only' with no terms-and-conditions reference.\",\n \"fix\": \"Add a disclaimer line for the limited-time offer.\",\n \"preciseInstruction\": \"Add small text at the bottom of the ad reading 'Offer valid today only. T&Cs apply. While stocks last.'\"\n },\n {\n \"element\": \"brand_tone\",\n \"severity\": \"minor\",\n \"problem\": \"The copy uses formal third-person language ('One may enjoy...') against a brand voice that is casual and second-person.\",\n \"fix\": \"Rewrite in the brand's casual, direct voice.\",\n \"preciseInstruction\": \"Replace the body copy with second-person, conversational language matching the brand tone, e.g. 'You'll love every bite \u2014 made fresh for you, every day.'\"\n }\n ],\n \"summary\": \"Critical compliance and quality issues: misspelled headline, unqualified superlative, missing offer disclaimer, and tone-of-voice drift. Regenerate the image with the corrections above.\"\n}\n\n## Example output (approved)\n\n{\n \"approved\": true,\n \"issues\": [],\n \"summary\": \"Clean, compliant creative: copy is correctly spelled and legible, all claims are supportable, legal mandatories are present, brand tone and colors are on-point, and the composition looks polished and professional.\"\n}`;\n\n// ---------------------------------------------------------------------------\n// Brand summary for reviewer context\n// ---------------------------------------------------------------------------\n\nfunction buildReviewerBrandSummary(ctx: BrandContext): string {\n const parts: string[] = [];\n\n if (ctx.colors) {\n const { primary, secondary, accent, background } = ctx.colors;\n const colorParts = [\n primary && `primary: ${primary}`,\n secondary && `secondary: ${secondary}`,\n accent && `CTA/accent: ${accent}`,\n background && `background: ${background}`,\n ].filter(Boolean);\n if (colorParts.length) {\n parts.push(`Brand colors \u2014 ${colorParts.join(\", \")}`);\n }\n }\n\n if (ctx.typography) {\n const { headline, body } = ctx.typography;\n if (headline?.family) {\n parts.push(\n `Headline font: ${headline.family}${\n headline.weight ? ` ${headline.weight}` : \"\"\n }`,\n );\n }\n if (body?.family) {\n parts.push(\n `Body font: ${body.family}${body.weight ? ` ${body.weight}` : \"\"}`,\n );\n }\n }\n\n if (ctx.visualStyle) {\n parts.push(`Visual style: ${ctx.visualStyle}`);\n }\n\n if (ctx.toneOfVoice) {\n const { register, vocabulary, ctaStyle, sentenceLength, person } =\n ctx.toneOfVoice;\n const toneParts = [\n register && `register: ${register}`,\n vocabulary && `vocabulary: ${vocabulary}`,\n person && `person/voice: ${person}`,\n sentenceLength && `sentence length: ${sentenceLength}`,\n ctaStyle && `CTA style: ${ctaStyle}`,\n ].filter(Boolean);\n if (toneParts.length) {\n parts.push(`Tone-of-voice \u2014 ${toneParts.join(\", \")}`);\n }\n }\n\n if (ctx.logoDataUrl) {\n const isBase64 = ctx.logoDataUrl.startsWith(\"data:\");\n parts.push(\n `Logo: provided as ${\n isBase64 ? \"base64 data URL\" : \"external URL\"\n } \u2014 a reference image is attached separately; verify it appears correctly in the ad (correct placement, scale, no distortion or recolouring)`,\n );\n }\n\n return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\nexport async function runReviewerAgent(opts: {\n screenshotDataUrl: string;\n htmlSource: string;\n frame: { width: number; height: number; id: string };\n imagePlacement?: { x: number; y: number; width: number; height: number };\n /**\n * All of the user's text turns in the conversation, concatenated \u2014 not\n * just the first message. The actual creative brief is often not\n * userMessages[0] (e.g. \"hi\" -> clarifying questions -> the message that\n * actually describes the ad), so anchoring to only the first message can\n * silently drop the real brief.\n */\n userBrief: string;\n /**\n * The original user-supplied reference image used to generate this frame\n * (distinct from the brand logo), if one was attached. Lets the reviewer\n * judge fidelity against what the user actually uploaded, not just the\n * text prompt.\n */\n originalImageDataUrl?: string;\n brandContext?: BrandContext;\n /** Host proxy URL for the reviewer's OpenRouter call. Key stays server-side. */\n chatUrl: string;\n /** OpenRouter model tag. Must be vision-capable. Defaults to minimax/minimax-m3 */\n reviewerModel?: string;\n signal?: AbortSignal;\n iteration: number;\n}): Promise<ReviewerFeedback> {\n const {\n screenshotDataUrl,\n htmlSource,\n frame,\n // imagePlacement retained in opts type for backward compatibility but unused\n // in the image-first flow (the ad is a single full-frame generated image).\n userBrief,\n originalImageDataUrl,\n brandContext,\n chatUrl,\n reviewerModel = \"google/gemini-2.5-flash\",\n signal,\n iteration,\n } = opts;\n\n const fallback: ReviewerFeedback = {\n approved: true,\n issues: [],\n summary: \"Reviewer unavailable; skipping.\",\n iteration,\n };\n\n // ---------------------------------------------------------------------------\n // Logo + font preview validation\n // ---------------------------------------------------------------------------\n const logoDataUrl = brandContext?.logoDataUrl;\n const logoIsBase64 =\n typeof logoDataUrl === \"string\" && logoDataUrl.startsWith(\"data:\");\n const logoIsExternalUrl = typeof logoDataUrl === \"string\" && !logoIsBase64;\n\n const fontPreviewDataUrl = brandContext?.headlineFontPreviewDataUrl;\n const fontPreviewIsBase64 =\n typeof fontPreviewDataUrl === \"string\" &&\n fontPreviewDataUrl.startsWith(\"data:\");\n\n const bodyFontPreviewDataUrl = brandContext?.bodyFontPreviewDataUrl;\n const bodyFontPreviewIsBase64 =\n typeof bodyFontPreviewDataUrl === \"string\" &&\n bodyFontPreviewDataUrl.startsWith(\"data:\");\n\n // ---------------------------------------------------------------------------\n // Build context message\n // ---------------------------------------------------------------------------\n const originalImageIsBase64 =\n typeof originalImageDataUrl === \"string\" &&\n originalImageDataUrl.startsWith(\"data:\");\n\n const contextLines: string[] = [\n `## Ad Brief (what the user requested)`,\n userBrief,\n ``,\n `## Frame dimensions`,\n `Width: ${frame.width}px, Height: ${frame.height}px`,\n ];\n\n if (originalImageIsBase64) {\n contextLines.push(\n ``,\n `## Original reference image (what the user uploaded)`,\n `The image the user originally attached is included below (immediately after the ad screenshot). Judge fidelity against it: the generated ad should preserve the subject, composition, and style of this reference unless the brief explicitly asked for a different treatment. Flag it as an issue if the ad has drifted from this reference in ways the brief did not request (wrong subject, different color scheme, altered composition).`,\n );\n }\n\n if (brandContext) {\n const brandSummary = buildReviewerBrandSummary(brandContext);\n if (brandSummary) {\n contextLines.push(``, `## Brand guidelines`, brandSummary);\n }\n }\n\n if (logoIsBase64) {\n contextLines.push(\n ``,\n `## Brand logo (reference)`,\n `The brand logo is attached below (after the ad screenshot${\n originalImageIsBase64 ? \" and the original reference image\" : \"\"\n }). Use it to evaluate whether the logo in the ad matches the brand's actual logo in colour, shape, and typographic treatment.`,\n );\n } else if (logoIsExternalUrl) {\n contextLines.push(\n ``,\n `## Brand logo (reference \u2014 URL, may not be visible)`,\n `A logo URL was provided but could not be embedded as an image. Evaluate logo usage from what is visible in the ad screenshot only.`,\n );\n }\n\n if (fontPreviewIsBase64 || bodyFontPreviewIsBase64) {\n const multiple = fontPreviewIsBase64 && bodyFontPreviewIsBase64;\n contextLines.push(\n ``,\n `## Brand font preview${multiple ? \"s\" : \"\"} (reference)`,\n `The brand font preview${\n multiple ? \"s are\" : \" is\"\n } attached below. Use ${\n multiple ? \"them\" : \"it\"\n } to evaluate whether the on-image text matches the brand typeface \u2014 check letterforms, weight, and typographic style. The brand may use ${\n multiple ? \"either font\" : \"this font\"\n } for headlines or body copy; do NOT flag a font as an issue simply because of which role it is applied to \u2014 only flag text that does not match the brand fonts at all.`,\n );\n }\n\n // htmlSource is retained in the signature for backward compatibility but is\n // empty in the image-first flow (the ad is a single generated image, not HTML).\n if (htmlSource && htmlSource.trim()) {\n contextLines.push(``, `## Source (legacy)`, \"```\", htmlSource, \"```\");\n }\n\n contextLines.push(\n ``,\n `## Your task`,\n `The screenshot of the generated ad is attached above. The whole ad (visuals + all copy) is a single AI-generated image. Evaluate it as the Brand Governance Agent \u2014 check creative quality (copy correctness, legibility, CTA, composition) AND all brand-governance dimensions (brand tone, logo usage, claim accuracy, legal mandatories, offer disclaimers, regulatory sensitivity, prohibited language, competitive claims, data/privacy concerns). Return your JSON critique. Every fix must be phrased as an instruction for regenerating the image.`,\n );\n\n // ---------------------------------------------------------------------------\n // Assemble user content blocks\n // Order: 1) ad screenshot, 2) original reference image (if base64),\n // 3) brand logo (if base64), 4) font preview (if base64)\n // ---------------------------------------------------------------------------\n const userContent: Array<\n | { type: \"text\"; text: string }\n | { type: \"image_url\"; image_url: { url: string } }\n > = [\n {\n type: \"image_url\",\n image_url: { url: screenshotDataUrl },\n },\n ];\n\n if (originalImageIsBase64 && originalImageDataUrl) {\n userContent.push({\n type: \"image_url\",\n image_url: { url: originalImageDataUrl },\n });\n }\n\n if (logoIsBase64 && logoDataUrl) {\n userContent.push({\n type: \"image_url\",\n image_url: { url: logoDataUrl },\n });\n }\n\n if (fontPreviewIsBase64 && fontPreviewDataUrl) {\n userContent.push({\n type: \"image_url\",\n image_url: { url: fontPreviewDataUrl },\n });\n }\n\n if (bodyFontPreviewIsBase64 && bodyFontPreviewDataUrl) {\n userContent.push({\n type: \"image_url\",\n image_url: { url: bodyFontPreviewDataUrl },\n });\n }\n\n userContent.push({\n type: \"text\",\n text: contextLines.join(\"\\n\"),\n });\n\n // The reviewer carries up to five images (banner screenshot, source image,\n // logo, two font previews). Downscale them like every other request \u2014 a\n // 413 here degrades to `fallback`, so it silently skipped the review rather\n // than surfacing an error.\n const payload = {\n model: reviewerModel,\n messages: await normalizeRequestImages([\n { role: \"system\", content: REVIEWER_SYSTEM_PROMPT },\n { role: \"user\", content: userContent },\n ]),\n response_format: { type: \"json_object\" },\n stream: false,\n temperature: 0.2, // low temperature for consistent, precise critiques\n };\n\n let response: Response;\n try {\n // Routed through the host proxy (chatUrl); the key stays server-side.\n response = await fetch(chatUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://dughub.app\",\n \"X-Title\": \"ADGEN Reviewer\",\n },\n body: JSON.stringify(payload),\n signal,\n });\n } catch (err) {\n // Network error or abort\n if (err instanceof Error && err.name === \"AbortError\") {\n throw err; // propagate abort so the outer loop can exit cleanly\n }\n return fallback;\n }\n\n if (!response.ok) {\n return fallback;\n }\n\n let raw: string;\n try {\n const data = await response.json();\n raw = data?.choices?.[0]?.message?.content ?? \"\";\n } catch {\n return fallback;\n }\n\n // Parse the JSON critique\n try {\n const parsed = JSON.parse(raw) as {\n approved?: boolean;\n issues?: ReviewIssue[];\n summary?: string;\n };\n\n const issues: ReviewIssue[] = Array.isArray(parsed.issues)\n ? parsed.issues.filter(\n (iss) =>\n iss &&\n typeof iss.element === \"string\" &&\n typeof iss.severity === \"string\" &&\n typeof iss.problem === \"string\" &&\n typeof iss.preciseInstruction === \"string\",\n )\n : [];\n\n const approved =\n typeof parsed.approved === \"boolean\"\n ? parsed.approved\n : issues.length === 0;\n\n return {\n approved,\n issues,\n summary:\n typeof parsed.summary === \"string\" && parsed.summary.trim()\n ? parsed.summary\n : approved\n ? \"Banner looks good.\"\n : `${issues.length} issue(s) found.`,\n iteration,\n };\n } catch {\n return fallback;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["content", "len", "canvas", "ctx", "getFrameChildren"]
|
|
7
|
+
}
|