@orangecatai/adgen-canvas 0.0.22 → 0.0.23
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-OIMA545L.js → chunk-E6AX7UYW.js} +3 -3
- package/dist/dev/{chunk-DG4DHKBY.js → chunk-KNIBTSFD.js} +2 -2
- package/dist/dev/data/{image-AWVYCS2V.js → image-WJAASDV4.js} +3 -3
- package/dist/dev/index.css +30 -0
- package/dist/dev/index.css.map +2 -2
- package/dist/dev/index.js +636 -61
- package/dist/dev/index.js.map +3 -3
- package/dist/dev/subset-shared.chunk.js +1 -1
- package/dist/dev/subset-worker.chunk.js +1 -1
- package/dist/prod/{chunk-PJYEU4Z2.js → chunk-GOHSI4TL.js} +1 -1
- package/dist/prod/{chunk-X4HZUT4M.js → chunk-XIMYEPMA.js} +2 -2
- package/dist/prod/data/image-QJTVNM22.js +1 -0
- package/dist/prod/index.css +1 -1
- package/dist/prod/index.js +106 -62
- package/dist/prod/subset-shared.chunk.js +1 -1
- package/dist/prod/subset-worker.chunk.js +1 -1
- package/dist/types/excalidraw/components/AIChatPanel.d.ts +34 -0
- package/dist/types/excalidraw/components/ai-chat/canvasTools.d.ts +79 -0
- package/dist/types/excalidraw/components/auto-resize/autoResizeEngine.d.ts +1 -0
- package/dist/types/excalidraw/index.d.ts +1 -0
- package/dist/types/excalidraw/types.d.ts +17 -0
- package/dist/types/excalidraw/utils/brandContextUtils.d.ts +4 -1
- package/package.json +1 -1
- package/dist/prod/data/image-YCVSAMH5.js +0 -1
- /package/dist/dev/{chunk-OIMA545L.js.map → chunk-E6AX7UYW.js.map} +0 -0
- /package/dist/dev/{chunk-DG4DHKBY.js.map → chunk-KNIBTSFD.js.map} +0 -0
- /package/dist/dev/data/{image-AWVYCS2V.js.map → image-WJAASDV4.js.map} +0 -0
package/dist/dev/index.js
CHANGED
|
@@ -66,10 +66,10 @@ import {
|
|
|
66
66
|
serializeAsJSON,
|
|
67
67
|
serializeLibraryAsJSON,
|
|
68
68
|
strokeRectWithRotation_simple
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-E6AX7UYW.js";
|
|
70
70
|
import {
|
|
71
71
|
define_import_meta_env_default
|
|
72
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-KNIBTSFD.js";
|
|
73
73
|
import {
|
|
74
74
|
en_default
|
|
75
75
|
} from "./chunk-4K5LIQQU.js";
|
|
@@ -9770,7 +9770,7 @@ var exportCanvas = async (type, elements, appState, files, {
|
|
|
9770
9770
|
let blob = canvasToBlob(tempCanvas);
|
|
9771
9771
|
if (appState.exportEmbedScene) {
|
|
9772
9772
|
blob = blob.then(
|
|
9773
|
-
(blob2) => import("./data/image-
|
|
9773
|
+
(blob2) => import("./data/image-WJAASDV4.js").then(
|
|
9774
9774
|
({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
|
|
9775
9775
|
blob: blob2,
|
|
9776
9776
|
metadata: serializeAsJSON(elements, appState, files, "local")
|
|
@@ -24142,18 +24142,21 @@ function buildAutoResizeImagePrompt(srcW, srcH, tgtW, tgtH, aspectRatio, brandIm
|
|
|
24142
24142
|
"the SOURCE AD CREATIVE that you must recompose (this is the design to adapt)"
|
|
24143
24143
|
);
|
|
24144
24144
|
const refList = refs.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
|
|
24145
|
+
const brandNameRule = brandImages?.name ? `
|
|
24146
|
+
BRAND: 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.` : "";
|
|
24145
24147
|
return `You are an expert ad designer. You are given the following reference images, in order:
|
|
24146
24148
|
${refList}
|
|
24147
24149
|
|
|
24148
|
-
TASK: Recreate the SOURCE AD CREATIVE as a single, polished advertisement recomposed for a ${tgtW}\xD7${tgtH} pixel canvas (aspect ratio ${aspectRatio}). The source was designed at ${srcW}\xD7${srcH}
|
|
24150
|
+
TASK: Recreate the SOURCE AD CREATIVE as a single, polished advertisement recomposed for a ${tgtW}\xD7${tgtH} pixel canvas (aspect ratio ${aspectRatio}). The source was designed at ${srcW}\xD7${srcH}.${brandNameRule}
|
|
24149
24151
|
|
|
24150
24152
|
HARD REQUIREMENTS:
|
|
24151
24153
|
\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.
|
|
24152
24154
|
\u2022 Preserve the brand: same colours, same logo (from the logo reference), same typeface (from the font reference). Render all text crisply and legibly.
|
|
24153
24155
|
\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.
|
|
24156
|
+
\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.
|
|
24154
24157
|
\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.
|
|
24155
24158
|
\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.
|
|
24156
|
-
\u2022 RE-ARRANGE the composition to suit the new aspect ratio \u2014 reposition and rescale elements so the layout feels native to ${tgtW}\xD7${tgtH}. Do NOT simply stretch, squash, or letterbox the original.
|
|
24159
|
+
\u2022 RE-ARRANGE the composition to suit the new aspect ratio \u2014 reposition and rescale elements so the layout feels native to ${tgtW}\xD7${tgtH}. Do NOT simply stretch, squash, or letterbox the original. 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.
|
|
24157
24160
|
\u2022 Maintain professional advertising quality: balanced layout, clear visual hierarchy, generous but purposeful spacing.
|
|
24158
24161
|
|
|
24159
24162
|
Output ONLY the final recomposed ad image at ${tgtW}\xD7${tgtH}.`;
|
|
@@ -24659,7 +24662,8 @@ var AutoResizePanel = ({
|
|
|
24659
24662
|
const brandImages = {
|
|
24660
24663
|
logo: brand?.logoDataUrl,
|
|
24661
24664
|
fontPreview: brand?.headlineFontPreviewDataUrl,
|
|
24662
|
-
fontUrl: brand?.headlineFontUrl
|
|
24665
|
+
fontUrl: brand?.headlineFontUrl,
|
|
24666
|
+
name: brand?.name
|
|
24663
24667
|
};
|
|
24664
24668
|
const elements = app.getSceneElements();
|
|
24665
24669
|
const sourceFrame = elements.find((el) => el.id === element.id);
|
|
@@ -25537,19 +25541,21 @@ function buildBrandContextMessage(ctx) {
|
|
|
25537
25541
|
}
|
|
25538
25542
|
return lines.join("\n");
|
|
25539
25543
|
}
|
|
25540
|
-
function buildImageGenSafePrompt(prompt, brandContext) {
|
|
25544
|
+
function buildImageGenSafePrompt(prompt, brandContext, dimensions) {
|
|
25541
25545
|
const headlineFontName = brandContext.typography?.headline?.family;
|
|
25542
25546
|
const bodyFontName = brandContext.typography?.body?.family;
|
|
25543
25547
|
const fontNames = [headlineFontName, bodyFontName].filter(
|
|
25544
25548
|
(n, i, arr) => Boolean(n) && arr.indexOf(n) === i
|
|
25545
25549
|
);
|
|
25546
|
-
const fontHint = fontNames.length ? ` Brand typography: the brand
|
|
25550
|
+
const fontHint = fontNames.length ? ` Brand typography: the brand font${fontNames.length > 1 ? "s are" : " is"} ${fontNames.join(
|
|
25547
25551
|
" and "
|
|
25548
|
-
)} \u2014 render
|
|
25552
|
+
)} \u2014 you MUST render ALL on-image text exclusively in this exact typeface as shown in the font reference image${fontNames.length > 1 ? "s" : ""}. Do NOT use any generic, default, or substitute font.` : "";
|
|
25553
|
+
const isPortrait = dimensions ? dimensions.height > dimensions.width : false;
|
|
25554
|
+
const logoPosition = isPortrait ? "top-center (horizontally centered, near the top edge)" : "top-left corner";
|
|
25549
25555
|
const brandImageInstructions = [];
|
|
25550
25556
|
if (brandContext.logoDataUrl) {
|
|
25551
25557
|
brandImageInstructions.push(
|
|
25552
|
-
|
|
25558
|
+
`BRAND LOGO: The first reference image is the exact brand logo graphic. Embed it VERBATIM in the ad \u2014 do not redraw, simplify, stylize, or alter it in any way. Place it at the ${logoPosition} with clear padding around it (do not overlap other copy or elements). The logo must appear exactly as it does in the reference image.`
|
|
25553
25559
|
);
|
|
25554
25560
|
}
|
|
25555
25561
|
if (brandContext.headlineFontPreviewDataUrl || brandContext.bodyFontPreviewDataUrl) {
|
|
@@ -25557,11 +25563,12 @@ function buildImageGenSafePrompt(prompt, brandContext) {
|
|
|
25557
25563
|
brandContext.headlineFontPreviewDataUrl && brandContext.bodyFontPreviewDataUrl
|
|
25558
25564
|
);
|
|
25559
25565
|
brandImageInstructions.push(
|
|
25560
|
-
`BRAND FONT: The
|
|
25566
|
+
`BRAND FONT: The reference ${multiple ? "images are" : "image is"} the brand's exact typeface${multiple ? "s" : ""}. You MUST render EVERY piece of on-image text \u2014 headlines, subheadlines, CTA, sign-off, and body copy \u2014 EXCLUSIVELY in ${multiple ? "these exact fonts as shown (use the first for headlines, the second for body/supporting copy)" : "this exact font as shown in the image"}. Do NOT use any generic, default, or substitute font anywhere in the ad.`
|
|
25561
25567
|
);
|
|
25562
25568
|
}
|
|
25563
25569
|
const brandImageHint = brandImageInstructions.length > 0 ? ` ${brandImageInstructions.join(" ")}` : "";
|
|
25564
|
-
|
|
25570
|
+
const brandNameHint = brandContext.name ? ` This ad is for the brand "${brandContext.name}" \u2014 all visual elements, copy, and style must reflect this brand exclusively.` : "";
|
|
25571
|
+
return `${prompt}${brandNameHint}${fontHint}${brandImageHint} Render all specified copy as crisp, correctly-spelled, legible text within the image. Maintain generous padding and whitespace \u2014 at minimum 4\u20135% of the frame width as margin on all sides; copy blocks, the logo, and the CTA must each have clear breathing room and must not feel cramped or crowded. Professional advertising quality.`;
|
|
25565
25572
|
}
|
|
25566
25573
|
|
|
25567
25574
|
// components/image-generation/pendingGenerations.ts
|
|
@@ -27154,7 +27161,8 @@ var ImageQuickEditPanel = ({
|
|
|
27154
27161
|
fontUrl: brandContext.headlineFontUrl,
|
|
27155
27162
|
brandContextText: buildBrandContextMessage(brandContext)
|
|
27156
27163
|
} : void 0;
|
|
27157
|
-
const
|
|
27164
|
+
const basePrompt = brandContext ? buildImageGenSafePrompt(prompt, brandContext) : prompt;
|
|
27165
|
+
const safePrompt = `${basePrompt} CRITICAL PRESERVATION RULE: Apply ONLY the specific change described above. The number of visual elements (text blocks, logos, images, shapes, buttons) must be IDENTICAL before and after this edit \u2014 do NOT add any new element, do NOT remove any existing element, do NOT rearrange the layout or composition. Every element that exists in the source image must appear exactly once in the output, in the same position and arrangement. Only the specific requested change may differ.`;
|
|
27158
27166
|
const imageDataUrl = await callOpenRouterImageAPI(
|
|
27159
27167
|
safePrompt,
|
|
27160
27168
|
cfg.modelId,
|
|
@@ -51139,6 +51147,11 @@ import {
|
|
|
51139
51147
|
} from "@orangecatai/element";
|
|
51140
51148
|
import { getTextFromElements as getTextFromElements3 } from "@orangecatai/element";
|
|
51141
51149
|
import { isInvisiblySmallElement as isInvisiblySmallElement4 } from "@orangecatai/element";
|
|
51150
|
+
import {
|
|
51151
|
+
newImageElement as newImageElement5,
|
|
51152
|
+
newTextElement as newTextElement7,
|
|
51153
|
+
newFrameElement as newFrameElement5
|
|
51154
|
+
} from "@orangecatai/element";
|
|
51142
51155
|
|
|
51143
51156
|
// data/reconcile.ts
|
|
51144
51157
|
import throttle4 from "lodash.throttle";
|
|
@@ -51937,6 +51950,24 @@ var BANNER_TOOLS = [
|
|
|
51937
51950
|
}
|
|
51938
51951
|
}
|
|
51939
51952
|
},
|
|
51953
|
+
{
|
|
51954
|
+
type: "function",
|
|
51955
|
+
function: {
|
|
51956
|
+
name: "search_ad_creatives",
|
|
51957
|
+
description: "Search uploaded ad creatives (past static ads) by name or tag keywords. Returns top 3 matches with visual previews. Use this to find reference ads for inspiration or remixing.",
|
|
51958
|
+
parameters: {
|
|
51959
|
+
type: "object",
|
|
51960
|
+
properties: {
|
|
51961
|
+
query: {
|
|
51962
|
+
type: "string",
|
|
51963
|
+
description: "Search query \u2014 name or tag keywords"
|
|
51964
|
+
}
|
|
51965
|
+
},
|
|
51966
|
+
required: ["query"],
|
|
51967
|
+
additionalProperties: false
|
|
51968
|
+
}
|
|
51969
|
+
}
|
|
51970
|
+
},
|
|
51940
51971
|
{
|
|
51941
51972
|
type: "function",
|
|
51942
51973
|
function: {
|
|
@@ -52062,6 +52093,49 @@ If no search context exists, call with an empty keywords array to list all templ
|
|
|
52062
52093
|
additionalProperties: false
|
|
52063
52094
|
}
|
|
52064
52095
|
}
|
|
52096
|
+
},
|
|
52097
|
+
{
|
|
52098
|
+
type: "function",
|
|
52099
|
+
function: {
|
|
52100
|
+
name: "place_brand_asset",
|
|
52101
|
+
description: "Place a brand image asset or ad creative directly into a frame as an image element. Use after search_brand_assets or search_ad_creatives to insert the found asset. Maintains the asset's aspect ratio inside the frame.",
|
|
52102
|
+
parameters: {
|
|
52103
|
+
type: "object",
|
|
52104
|
+
properties: {
|
|
52105
|
+
frameId: {
|
|
52106
|
+
type: "string",
|
|
52107
|
+
description: "The frame to place the asset in"
|
|
52108
|
+
},
|
|
52109
|
+
assetId: {
|
|
52110
|
+
type: "string",
|
|
52111
|
+
description: "Asset ID from search_brand_assets or search_ad_creatives [id:...] field"
|
|
52112
|
+
},
|
|
52113
|
+
assetType: {
|
|
52114
|
+
type: "string",
|
|
52115
|
+
enum: ["image-asset", "ad-creative"],
|
|
52116
|
+
description: "Type of asset \u2014 defaults to image-asset"
|
|
52117
|
+
},
|
|
52118
|
+
x: {
|
|
52119
|
+
type: "number",
|
|
52120
|
+
description: "X offset from frame left edge (optional, centers by default)"
|
|
52121
|
+
},
|
|
52122
|
+
y: {
|
|
52123
|
+
type: "number",
|
|
52124
|
+
description: "Y offset from frame top edge (optional, centers by default)"
|
|
52125
|
+
},
|
|
52126
|
+
width: {
|
|
52127
|
+
type: "number",
|
|
52128
|
+
description: "Width of placed image (optional, auto-fits to frame by default)"
|
|
52129
|
+
},
|
|
52130
|
+
height: {
|
|
52131
|
+
type: "number",
|
|
52132
|
+
description: "Height of placed image (optional, auto-fits to frame by default)"
|
|
52133
|
+
}
|
|
52134
|
+
},
|
|
52135
|
+
required: ["frameId", "assetId"],
|
|
52136
|
+
additionalProperties: false
|
|
52137
|
+
}
|
|
52138
|
+
}
|
|
52065
52139
|
}
|
|
52066
52140
|
];
|
|
52067
52141
|
function serializeElement(el) {
|
|
@@ -52199,8 +52273,12 @@ async function executeCanvasTool(toolName, rawArgs, ctx) {
|
|
|
52199
52273
|
return execListFrames(ctx);
|
|
52200
52274
|
case "generate_html_banner":
|
|
52201
52275
|
return execGenerateHtmlBanner(args, ctx);
|
|
52276
|
+
case "find_brand_asset":
|
|
52277
|
+
return execFindBrandAsset(args, ctx);
|
|
52202
52278
|
case "search_brand_assets":
|
|
52203
52279
|
return execSearchBrandAssets(args, ctx);
|
|
52280
|
+
case "search_ad_creatives":
|
|
52281
|
+
return execSearchAdCreatives(args, ctx);
|
|
52204
52282
|
case "list_brand_templates":
|
|
52205
52283
|
return execListBrandTemplates(args, ctx);
|
|
52206
52284
|
case "get_template_variant":
|
|
@@ -52209,6 +52287,8 @@ async function executeCanvasTool(toolName, rawArgs, ctx) {
|
|
|
52209
52287
|
return execLoadTemplateIntoFrame(args, ctx);
|
|
52210
52288
|
case "fill_template_slots":
|
|
52211
52289
|
return execFillTemplateSlots(args, ctx);
|
|
52290
|
+
case "place_brand_asset":
|
|
52291
|
+
return execPlaceBrandAsset(args, ctx);
|
|
52212
52292
|
case "finalize_ad":
|
|
52213
52293
|
return execFinalizeAd(args, ctx);
|
|
52214
52294
|
default:
|
|
@@ -52441,13 +52521,15 @@ async function execGenerateImage(args, ctx) {
|
|
|
52441
52521
|
const fontNames = [ctx.brandHeadlineFontName, ctx.brandBodyFontName].filter(
|
|
52442
52522
|
(n, i, arr) => Boolean(n) && arr.indexOf(n) === i
|
|
52443
52523
|
);
|
|
52444
|
-
const fontHint = fontNames.length ? ` Brand typography: the brand
|
|
52524
|
+
const fontHint = fontNames.length ? ` Brand typography: the brand font${fontNames.length > 1 ? "s are" : " is"} ${fontNames.join(
|
|
52445
52525
|
" and "
|
|
52446
|
-
)} \u2014 render
|
|
52526
|
+
)} \u2014 you MUST render ALL on-image text exclusively in this exact typeface as shown in the font reference image${fontNames.length > 1 ? "s" : ""}. Do NOT use any generic, default, or substitute font.` : "";
|
|
52527
|
+
const isPortrait = height > width;
|
|
52528
|
+
const logoPosition = isPortrait ? "top-center (horizontally centered, near the top edge)" : "top-left corner";
|
|
52447
52529
|
const brandImageInstructions = [];
|
|
52448
52530
|
if (ctx.brandLogoDataUrl) {
|
|
52449
52531
|
brandImageInstructions.push(
|
|
52450
|
-
|
|
52532
|
+
`BRAND LOGO: The first reference image is the ONLY logo permitted in this ad. You MUST reproduce it exactly as-is \u2014 pixel-for-pixel, without any redrawing, simplification, stylization, re-interpretation, or recreation. Do NOT invent a version of the logo; do NOT use any icon, symbol, or wordmark other than the one in the reference image. Place it at the ${logoPosition} with clear padding around it (do not overlap other copy or elements). Any deviation from the reference image logo is a critical error.`
|
|
52451
52533
|
);
|
|
52452
52534
|
}
|
|
52453
52535
|
if (ctx.brandFontPreviewDataUrl || ctx.brandBodyFontPreviewDataUrl) {
|
|
@@ -52455,11 +52537,22 @@ async function execGenerateImage(args, ctx) {
|
|
|
52455
52537
|
ctx.brandFontPreviewDataUrl && ctx.brandBodyFontPreviewDataUrl
|
|
52456
52538
|
);
|
|
52457
52539
|
brandImageInstructions.push(
|
|
52458
|
-
`BRAND FONT: The
|
|
52540
|
+
`BRAND FONT: The reference ${multiple ? "images are" : "image is"} the brand's exact typeface${multiple ? "s" : ""}. You MUST render EVERY piece of on-image text \u2014 headlines, subheadlines, CTA, sign-off, and body copy \u2014 EXCLUSIVELY in ${multiple ? "these exact fonts as shown (use the first for headlines, the second for body/supporting copy)" : "this exact font as shown in the image"}. Do NOT use any generic, default, or substitute font anywhere in the ad.`
|
|
52459
52541
|
);
|
|
52460
52542
|
}
|
|
52461
52543
|
const brandImageHint = brandImageInstructions.length > 0 ? ` ${brandImageInstructions.join(" ")}` : "";
|
|
52462
|
-
const safePrompt = `${prompt}${fontHint}${brandImageHint}
|
|
52544
|
+
const safePrompt = `${prompt}${fontHint}${brandImageHint}
|
|
52545
|
+
|
|
52546
|
+
LAYOUT & SPACING RULES \u2014 these are non-negotiable:
|
|
52547
|
+
1. SAFE ZONE: Keep all text, logos, and UI elements at least 8% of the canvas width away from every edge. Nothing touches or bleeds off the border.
|
|
52548
|
+
2. ELEMENT SEPARATION: Every distinct element (logo, headline, subheadline, CTA, product image) must have a minimum gap of 6% of the canvas height between it and any neighbouring element. No two elements may touch or overlap.
|
|
52549
|
+
3. LOGO ZONE: The logo occupies its own dedicated region with clear empty space on all four sides \u2014 at least 4% of canvas width as padding around the logo boundary. Nothing else enters this zone.
|
|
52550
|
+
4. TEXT BLOCKS: Headlines, subheadlines, and body copy each have breathing room above and below equal to at least half their own line-height. Multi-line text must not feel compressed.
|
|
52551
|
+
5. CTA: The call-to-action button or text has at least 3% of canvas height clear space above it and below it, and at least 5% of canvas width padding on its left and right.
|
|
52552
|
+
6. NO CLUTTER: The overall composition must feel open and airy \u2014 fewer elements with more space between them is always better than many elements squeezed together.
|
|
52553
|
+
7. ASPECT RATIO: Do NOT squish, stretch, or distort any element. Maintain the natural proportions of all visuals, logos, and text at all times.
|
|
52554
|
+
|
|
52555
|
+
Render all text as crisp, correctly-spelled, legible copy. Professional advertising quality \u2014 strong visual hierarchy, brand-consistent, generous whitespace throughout.`;
|
|
52463
52556
|
if (!frameId || !prompt) {
|
|
52464
52557
|
return {
|
|
52465
52558
|
success: false,
|
|
@@ -52487,7 +52580,16 @@ async function execGenerateImage(args, ctx) {
|
|
|
52487
52580
|
const isLikelyLocal = x < frame.width && y < frame.height && Math.abs(x - frame.x) > 50;
|
|
52488
52581
|
const absX = isLikelyLocal ? frame.x + x : x;
|
|
52489
52582
|
const absY = isLikelyLocal ? frame.y + y : y;
|
|
52490
|
-
const
|
|
52583
|
+
const frameRatio = width / height;
|
|
52584
|
+
const aspectRatio = [
|
|
52585
|
+
{ label: "16:9", value: 16 / 9 },
|
|
52586
|
+
{ label: "4:3", value: 4 / 3 },
|
|
52587
|
+
{ label: "1:1", value: 1 },
|
|
52588
|
+
{ label: "3:4", value: 3 / 4 },
|
|
52589
|
+
{ label: "9:16", value: 9 / 16 }
|
|
52590
|
+
].reduce(
|
|
52591
|
+
(best, c) => Math.abs(c.value - frameRatio) < Math.abs(best.value - frameRatio) ? c : best
|
|
52592
|
+
).label;
|
|
52491
52593
|
const brandImagesLog = {
|
|
52492
52594
|
logo: ctx.brandLogoDataUrl ? `\u2713 base64 ${Math.round(ctx.brandLogoDataUrl.length / 1024)} KB` : "\u2717 not provided",
|
|
52493
52595
|
fontPreview: ctx.brandFontPreviewDataUrl ? `\u2713 base64 ${Math.round(ctx.brandFontPreviewDataUrl.length / 1024)} KB` : "\u2717 not provided",
|
|
@@ -52517,13 +52619,43 @@ async function execGenerateImage(args, ctx) {
|
|
|
52517
52619
|
statusMessage: "Image generation cancelled"
|
|
52518
52620
|
};
|
|
52519
52621
|
}
|
|
52622
|
+
let placeW = width;
|
|
52623
|
+
let placeH = height;
|
|
52624
|
+
let placeX = absX;
|
|
52625
|
+
let placeY = absY;
|
|
52626
|
+
try {
|
|
52627
|
+
const actualDims = await Promise.race([
|
|
52628
|
+
new Promise((resolve, reject) => {
|
|
52629
|
+
const img = new Image();
|
|
52630
|
+
img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight });
|
|
52631
|
+
img.onerror = reject;
|
|
52632
|
+
img.src = imageDataUrl;
|
|
52633
|
+
}),
|
|
52634
|
+
new Promise(
|
|
52635
|
+
(_, reject) => setTimeout(() => reject(new Error("Image load timeout")), 200)
|
|
52636
|
+
)
|
|
52637
|
+
]);
|
|
52638
|
+
const scale = Math.min(width / actualDims.w, height / actualDims.h);
|
|
52639
|
+
placeW = actualDims.w * scale;
|
|
52640
|
+
placeH = actualDims.h * scale;
|
|
52641
|
+
placeX = absX + (width - placeW) / 2;
|
|
52642
|
+
placeY = absY + (height - placeH) / 2;
|
|
52643
|
+
if (Math.abs(placeW - width) > 1 || Math.abs(placeH - height) > 1) {
|
|
52644
|
+
console.log(
|
|
52645
|
+
`[generate_image] contain-fit: generated ${actualDims.w}\xD7${actualDims.h} \u2192 placed ${Math.round(placeW)}\xD7${Math.round(
|
|
52646
|
+
placeH
|
|
52647
|
+
)} inside ${width}\xD7${height} frame`
|
|
52648
|
+
);
|
|
52649
|
+
}
|
|
52650
|
+
} catch {
|
|
52651
|
+
}
|
|
52520
52652
|
const fileId = nanoid4();
|
|
52521
52653
|
const imageEl = newImageElement4({
|
|
52522
52654
|
type: "image",
|
|
52523
|
-
x:
|
|
52524
|
-
y:
|
|
52525
|
-
width,
|
|
52526
|
-
height,
|
|
52655
|
+
x: placeX,
|
|
52656
|
+
y: placeY,
|
|
52657
|
+
width: placeW,
|
|
52658
|
+
height: placeH,
|
|
52527
52659
|
fileId,
|
|
52528
52660
|
status: "pending",
|
|
52529
52661
|
scale: [1, 1]
|
|
@@ -52913,6 +53045,53 @@ async function execGenerateHtmlBanner(args, ctx) {
|
|
|
52913
53045
|
screenshot: screenshot ?? void 0
|
|
52914
53046
|
};
|
|
52915
53047
|
}
|
|
53048
|
+
async function execFindBrandAsset(args, ctx) {
|
|
53049
|
+
const type = args.type;
|
|
53050
|
+
const keywords = Array.isArray(args.keywords) ? args.keywords : typeof args.keywords === "string" ? [args.keywords] : [];
|
|
53051
|
+
if (!ctx.onFindBrandAssets) {
|
|
53052
|
+
return { success: true, statusMessage: "No find-assets configured" };
|
|
53053
|
+
}
|
|
53054
|
+
try {
|
|
53055
|
+
const { results } = await ctx.onFindBrandAssets(type, keywords);
|
|
53056
|
+
if (results.length === 0) {
|
|
53057
|
+
return {
|
|
53058
|
+
success: true,
|
|
53059
|
+
data: { results: [] },
|
|
53060
|
+
statusMessage: `No ${type} found for keywords: ${keywords.join(", ") || "(all)"}`
|
|
53061
|
+
};
|
|
53062
|
+
}
|
|
53063
|
+
const summary = results.map(
|
|
53064
|
+
(r, i) => `${i + 1}. "${r.name}"${r.width && r.height ? ` (${r.width}x${r.height})` : ""}${r.tags?.length ? ` [${r.tags.slice(0, 3).join(", ")}]` : ""} [id:${r.id}]`
|
|
53065
|
+
).join("\n");
|
|
53066
|
+
return {
|
|
53067
|
+
success: true,
|
|
53068
|
+
data: {
|
|
53069
|
+
results: results.map((r) => ({
|
|
53070
|
+
id: r.id,
|
|
53071
|
+
name: r.name,
|
|
53072
|
+
tags: r.tags,
|
|
53073
|
+
width: r.width,
|
|
53074
|
+
height: r.height
|
|
53075
|
+
})),
|
|
53076
|
+
summary: `Found ${results.length} ${type}(s):
|
|
53077
|
+
${summary}
|
|
53078
|
+
|
|
53079
|
+
Visual previews are attached above.`
|
|
53080
|
+
},
|
|
53081
|
+
statusMessage: `Found ${results.length} result(s)`,
|
|
53082
|
+
previewDataUrls: results.map((r) => ({
|
|
53083
|
+
name: r.name,
|
|
53084
|
+
dataUrl: r.previewDataUrl
|
|
53085
|
+
}))
|
|
53086
|
+
};
|
|
53087
|
+
} catch {
|
|
53088
|
+
return {
|
|
53089
|
+
success: false,
|
|
53090
|
+
error: `Failed to find brand assets.`,
|
|
53091
|
+
statusMessage: "find_brand_asset failed"
|
|
53092
|
+
};
|
|
53093
|
+
}
|
|
53094
|
+
}
|
|
52916
53095
|
async function execSearchBrandAssets(args, ctx) {
|
|
52917
53096
|
const query = args.query;
|
|
52918
53097
|
const type = args.type;
|
|
@@ -52928,15 +53107,24 @@ async function execSearchBrandAssets(args, ctx) {
|
|
|
52928
53107
|
statusMessage: `No brand assets found for "${query}"`
|
|
52929
53108
|
};
|
|
52930
53109
|
}
|
|
52931
|
-
const
|
|
53110
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
53111
|
+
const top = assets.map((a) => ({
|
|
53112
|
+
asset: a,
|
|
53113
|
+
score: words.filter(
|
|
53114
|
+
(w) => a.name.toLowerCase().includes(w) || a.tags.some((t2) => t2.toLowerCase().includes(w))
|
|
53115
|
+
).length
|
|
53116
|
+
})).sort((a, b) => b.score - a.score).slice(0, 3).map((s) => s.asset);
|
|
53117
|
+
const list = top.map((a) => `- ${a.name} (${a.assetType}) [id:${a.id}]: ${a.blobUrl}`).join("\n");
|
|
53118
|
+
const previews = top.filter((a) => a.previewDataUrl).map((a) => ({ name: a.name, dataUrl: a.previewDataUrl }));
|
|
52932
53119
|
return {
|
|
52933
53120
|
success: true,
|
|
52934
53121
|
data: {
|
|
52935
|
-
assets,
|
|
52936
|
-
summary: `Found ${
|
|
52937
|
-
${list}`
|
|
53122
|
+
assets: top,
|
|
53123
|
+
summary: `Found ${top.length} brand asset(s):
|
|
53124
|
+
${list}${previews.length > 0 ? "\n\nVisual previews attached above." : ""}`
|
|
52938
53125
|
},
|
|
52939
|
-
statusMessage: `Found ${
|
|
53126
|
+
statusMessage: `Found ${top.length} assets`,
|
|
53127
|
+
previewDataUrls: previews.length > 0 ? previews : void 0
|
|
52940
53128
|
};
|
|
52941
53129
|
} catch {
|
|
52942
53130
|
return {
|
|
@@ -52946,6 +53134,47 @@ ${list}`
|
|
|
52946
53134
|
};
|
|
52947
53135
|
}
|
|
52948
53136
|
}
|
|
53137
|
+
async function execSearchAdCreatives(args, ctx) {
|
|
53138
|
+
const query = args.query;
|
|
53139
|
+
if (!ctx.onSearchAdCreatives) {
|
|
53140
|
+
return { success: true, statusMessage: "No ad creative search configured" };
|
|
53141
|
+
}
|
|
53142
|
+
try {
|
|
53143
|
+
const creatives = await ctx.onSearchAdCreatives(query);
|
|
53144
|
+
if (creatives.length === 0) {
|
|
53145
|
+
return {
|
|
53146
|
+
success: true,
|
|
53147
|
+
data: { creatives: [] },
|
|
53148
|
+
statusMessage: `No ad creatives found for "${query}"`
|
|
53149
|
+
};
|
|
53150
|
+
}
|
|
53151
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
53152
|
+
const top = creatives.map((c) => ({
|
|
53153
|
+
creative: c,
|
|
53154
|
+
score: words.filter(
|
|
53155
|
+
(w) => c.name.toLowerCase().includes(w) || c.tags.some((t2) => t2.toLowerCase().includes(w))
|
|
53156
|
+
).length
|
|
53157
|
+
})).sort((a, b) => b.score - a.score).slice(0, 3).map((s) => s.creative);
|
|
53158
|
+
const list = top.map((c, i) => `${i + 1}. "${c.name}" [id:${c.id}]: ${c.blobUrl}`).join("\n");
|
|
53159
|
+
const previews = top.filter((c) => c.previewDataUrl).map((c) => ({ name: c.name, dataUrl: c.previewDataUrl }));
|
|
53160
|
+
return {
|
|
53161
|
+
success: true,
|
|
53162
|
+
data: {
|
|
53163
|
+
creatives: top,
|
|
53164
|
+
summary: `Found ${top.length} ad creative(s):
|
|
53165
|
+
${list}${previews.length > 0 ? "\n\nVisual previews attached above." : ""}`
|
|
53166
|
+
},
|
|
53167
|
+
statusMessage: `Found ${top.length} ad creative(s)`,
|
|
53168
|
+
previewDataUrls: previews.length > 0 ? previews : void 0
|
|
53169
|
+
};
|
|
53170
|
+
} catch {
|
|
53171
|
+
return {
|
|
53172
|
+
success: false,
|
|
53173
|
+
error: "Failed to search ad creatives.",
|
|
53174
|
+
statusMessage: "Ad creative search failed"
|
|
53175
|
+
};
|
|
53176
|
+
}
|
|
53177
|
+
}
|
|
52949
53178
|
async function execFinalizeAd(args, ctx) {
|
|
52950
53179
|
const frameId = args.frameId;
|
|
52951
53180
|
const screenshot = await captureFrameScreenshot(ctx.excalidrawAPI, frameId);
|
|
@@ -52975,14 +53204,28 @@ async function execListBrandTemplates(args, ctx) {
|
|
|
52975
53204
|
statusMessage: "No templates found"
|
|
52976
53205
|
};
|
|
52977
53206
|
}
|
|
53207
|
+
if (keywords && keywords.length > 0) {
|
|
53208
|
+
const kws = keywords.map((k) => k.toLowerCase());
|
|
53209
|
+
templates.sort((a, b) => {
|
|
53210
|
+
const scoreA = kws.filter(
|
|
53211
|
+
(k) => a.name.toLowerCase().includes(k) || (a.campaignTag ?? "").toLowerCase().includes(k) || (a.label ?? "").toLowerCase().includes(k)
|
|
53212
|
+
).length;
|
|
53213
|
+
const scoreB = kws.filter(
|
|
53214
|
+
(k) => b.name.toLowerCase().includes(k) || (b.campaignTag ?? "").toLowerCase().includes(k) || (b.label ?? "").toLowerCase().includes(k)
|
|
53215
|
+
).length;
|
|
53216
|
+
return scoreB - scoreA;
|
|
53217
|
+
});
|
|
53218
|
+
}
|
|
52978
53219
|
const list = templates.map((t2) => {
|
|
52979
53220
|
const dims = t2.width && t2.height ? ` (${t2.width}x${t2.height})` : "";
|
|
52980
53221
|
return `Template: ${t2.name}${t2.campaignTag ? ` [${t2.campaignTag}]` : ""}${dims} [id:${t2.id}]`;
|
|
52981
53222
|
}).join("\n");
|
|
53223
|
+
const previews = templates.filter((t2) => t2.thumbnailDataUrl).slice(0, 3).map((t2) => ({ name: t2.name, dataUrl: t2.thumbnailDataUrl }));
|
|
52982
53224
|
return {
|
|
52983
53225
|
success: true,
|
|
52984
53226
|
data: { templates, summary: list },
|
|
52985
|
-
statusMessage: `Found ${templates.length} template(s)
|
|
53227
|
+
statusMessage: `Found ${templates.length} template(s)`,
|
|
53228
|
+
previewDataUrls: previews.length > 0 ? previews : void 0
|
|
52986
53229
|
};
|
|
52987
53230
|
} catch {
|
|
52988
53231
|
return {
|
|
@@ -53367,6 +53610,104 @@ async function execFillTemplateSlots(args, ctx) {
|
|
|
53367
53610
|
statusMessage: `Filled ${filledCount} slots`
|
|
53368
53611
|
};
|
|
53369
53612
|
}
|
|
53613
|
+
async function execPlaceBrandAsset(args, ctx) {
|
|
53614
|
+
const frameId = args.frameId;
|
|
53615
|
+
const assetId = args.assetId;
|
|
53616
|
+
const assetType = args.assetType ?? "image-asset";
|
|
53617
|
+
if (!ctx.onFindBrandAssets) {
|
|
53618
|
+
return {
|
|
53619
|
+
success: false,
|
|
53620
|
+
error: "Asset fetching not configured.",
|
|
53621
|
+
statusMessage: "No find-assets configured"
|
|
53622
|
+
};
|
|
53623
|
+
}
|
|
53624
|
+
try {
|
|
53625
|
+
const { results } = await ctx.onFindBrandAssets(assetType, [
|
|
53626
|
+
`id:${assetId}`
|
|
53627
|
+
]);
|
|
53628
|
+
if (results.length === 0) {
|
|
53629
|
+
return {
|
|
53630
|
+
success: false,
|
|
53631
|
+
error: `Asset ${assetId} not found.`,
|
|
53632
|
+
statusMessage: "Asset not found"
|
|
53633
|
+
};
|
|
53634
|
+
}
|
|
53635
|
+
const asset = results[0];
|
|
53636
|
+
const dataUrl = asset.previewDataUrl;
|
|
53637
|
+
if (!dataUrl) {
|
|
53638
|
+
return {
|
|
53639
|
+
success: false,
|
|
53640
|
+
error: "Could not load asset image.",
|
|
53641
|
+
statusMessage: "Asset image unavailable"
|
|
53642
|
+
};
|
|
53643
|
+
}
|
|
53644
|
+
const allElements = ctx.excalidrawAPI.getSceneElements();
|
|
53645
|
+
const frame = allElements.find((el) => el.id === frameId);
|
|
53646
|
+
if (!frame) {
|
|
53647
|
+
return {
|
|
53648
|
+
success: false,
|
|
53649
|
+
error: `Frame ${frameId} not found.`,
|
|
53650
|
+
statusMessage: "Frame not found"
|
|
53651
|
+
};
|
|
53652
|
+
}
|
|
53653
|
+
const imgW = asset.width ?? frame.width;
|
|
53654
|
+
const imgH = asset.height ?? frame.height;
|
|
53655
|
+
const scale = Math.min(frame.width / imgW, frame.height / imgH);
|
|
53656
|
+
const fitW = args.width != null ? args.width : imgW * scale;
|
|
53657
|
+
const fitH = args.height != null ? args.height : imgH * scale;
|
|
53658
|
+
const absX = frame.x + (args.x != null ? args.x : (frame.width - fitW) / 2);
|
|
53659
|
+
const absY = frame.y + (args.y != null ? args.y : (frame.height - fitH) / 2);
|
|
53660
|
+
const fileId = nanoid4();
|
|
53661
|
+
const imageEl = newImageElement4({
|
|
53662
|
+
type: "image",
|
|
53663
|
+
x: absX,
|
|
53664
|
+
y: absY,
|
|
53665
|
+
width: fitW,
|
|
53666
|
+
height: fitH,
|
|
53667
|
+
fileId,
|
|
53668
|
+
status: "pending",
|
|
53669
|
+
scale: [1, 1]
|
|
53670
|
+
});
|
|
53671
|
+
const frameIndex = allElements.findIndex((el) => el.id === frameId);
|
|
53672
|
+
const updatedImage = { ...imageEl, frameId };
|
|
53673
|
+
const newElements = [...allElements];
|
|
53674
|
+
const insertAt = findInsertIndexByTier(
|
|
53675
|
+
newElements,
|
|
53676
|
+
frameIndex === -1 ? 0 : frameIndex,
|
|
53677
|
+
1 /* IMAGE */
|
|
53678
|
+
);
|
|
53679
|
+
newElements.splice(insertAt, 0, updatedImage);
|
|
53680
|
+
syncMovedIndices7(newElements, arrayToMap30([updatedImage]));
|
|
53681
|
+
ctx.excalidrawAPI.updateScene({ elements: newElements });
|
|
53682
|
+
ctx.excalidrawAPI.addFiles([
|
|
53683
|
+
{
|
|
53684
|
+
id: fileId,
|
|
53685
|
+
dataURL: dataUrl,
|
|
53686
|
+
mimeType: getMimeTypeFromDataURL(dataUrl),
|
|
53687
|
+
created: Date.now(),
|
|
53688
|
+
lastRetrieved: Date.now()
|
|
53689
|
+
}
|
|
53690
|
+
]);
|
|
53691
|
+
return {
|
|
53692
|
+
success: true,
|
|
53693
|
+
data: {
|
|
53694
|
+
elementId: imageEl.id,
|
|
53695
|
+
frameId,
|
|
53696
|
+
x: Math.round(absX),
|
|
53697
|
+
y: Math.round(absY),
|
|
53698
|
+
width: Math.round(fitW),
|
|
53699
|
+
height: Math.round(fitH)
|
|
53700
|
+
},
|
|
53701
|
+
statusMessage: `Placed "${asset.name}" in frame`
|
|
53702
|
+
};
|
|
53703
|
+
} catch (err) {
|
|
53704
|
+
return {
|
|
53705
|
+
success: false,
|
|
53706
|
+
error: `Failed to place asset: ${err instanceof Error ? err.message : String(err)}`,
|
|
53707
|
+
statusMessage: "Place asset failed"
|
|
53708
|
+
};
|
|
53709
|
+
}
|
|
53710
|
+
}
|
|
53370
53711
|
|
|
53371
53712
|
// components/ai-chat/openRouterStream.ts
|
|
53372
53713
|
function parseOpenRouterChunk(jsonStr) {
|
|
@@ -53514,7 +53855,20 @@ The \`generate_image\` prompt is the ONLY creative call \u2014 it must produce t
|
|
|
53514
53855
|
|
|
53515
53856
|
5. **DIMENSIONS / ASPECT:** describe the format so the composition fits (e.g. "square 1080\xD71080 social post", "landscape 1200\xD7628 banner", "portrait 1080\xD71920 story").
|
|
53516
53857
|
|
|
53517
|
-
|
|
53858
|
+
6. **LOGO PLACEMENT:** Always state where the logo goes based on orientation:
|
|
53859
|
+
- Portrait / vertical (height > width): brand logo **top-center** (horizontally centered, near the top edge).
|
|
53860
|
+
- Landscape or square: brand logo **top-left corner**.
|
|
53861
|
+
Include the exact position in your prompt (e.g. "brand logo top-left corner with 24px clear padding"). The logo reference image must be embedded exactly as provided \u2014 do not redraw, simplify, or stylize it.
|
|
53862
|
+
|
|
53863
|
+
7. **SPACING & BREATHING ROOM \u2014 MANDATORY:** Your image prompt MUST include explicit spacing constraints. Use language like:
|
|
53864
|
+
- "8% safe zone margin on all edges \u2014 no element touches the border"
|
|
53865
|
+
- "minimum 6% canvas-height gap between every element"
|
|
53866
|
+
- "logo has its own dedicated zone with clear padding on all sides"
|
|
53867
|
+
- "CTA has generous empty space above and below"
|
|
53868
|
+
- "open, airy composition \u2014 elements are not crowded or touching"
|
|
53869
|
+
Never omit these constraints. An ad where elements are cramped, touching, or bleeding to the edge is a failed output.
|
|
53870
|
+
|
|
53871
|
+
Aim for a polished, professional advertising creative \u2014 typography crisp and legible, copy correctly spelled, strong visual hierarchy, brand-consistent, generous whitespace throughout, every element with room to breathe.
|
|
53518
53872
|
|
|
53519
53873
|
## Language and script
|
|
53520
53874
|
|
|
@@ -53525,11 +53879,15 @@ Aim for a polished, professional advertising creative \u2014 typography crisp an
|
|
|
53525
53879
|
|
|
53526
53880
|
Brand context is mandatory input, not optional decoration. The generated ad must reflect the brand kit / brand code, the approved copy, and the visual direction \u2014 never generate something arbitrary.
|
|
53527
53881
|
|
|
53528
|
-
|
|
53882
|
+
BRAND REFERENCES are auto-fetched and provided to you as visual context at the start of every session (up to 2 brand assets, 2 ad creatives, 2 templates). Check them before deciding how to generate:
|
|
53883
|
+
|
|
53884
|
+
- If a **template** clearly fits the user's request \u2192 use it: \`load_template_into_frame\` + \`fill_template_slots\`. Do NOT call \`generate_image\` on a template ad.
|
|
53885
|
+
- If a **brand asset or ad creative** is relevant \u2192 pass its image as a reference to \`generate_image\` so the output matches existing brand visuals.
|
|
53886
|
+
- If nothing pre-fetched is relevant \u2192 generate from scratch: \`create_frame\` \u2192 \`generate_image\`.
|
|
53529
53887
|
|
|
53530
|
-
|
|
53888
|
+
You may still call \`search_brand_assets()\`, \`search_ad_creatives()\`, or \`list_brand_templates()\` explicitly if the user asks for something specific that may not have been in the auto-fetched top 2.
|
|
53531
53889
|
|
|
53532
|
-
TEMPLATE BANK (
|
|
53890
|
+
TEMPLATE BANK (when using a template): call \`list_brand_templates()\`, present matches, then:
|
|
53533
53891
|
1. \`load_template_into_frame(frameId, variantId)\`
|
|
53534
53892
|
2. \`get_frame_elements(frameId)\` to see the slots
|
|
53535
53893
|
3. \`fill_template_slots(frameId, headline, subhead, cta, background_color, product_image_url, logo_url)\` \u2014 all slots in one call.
|
|
@@ -53697,6 +54055,77 @@ async function runAgentLoop(opts) {
|
|
|
53697
54055
|
${elementContext}`
|
|
53698
54056
|
});
|
|
53699
54057
|
}
|
|
54058
|
+
{
|
|
54059
|
+
const lastUserText = [...userMessages].reverse().find((m) => m.role === "user")?.content ?? "";
|
|
54060
|
+
if (lastUserText && (toolCtx.onSearchBrandAssets || toolCtx.onSearchAdCreatives || toolCtx.onListBrandTemplates)) {
|
|
54061
|
+
const keywords = [
|
|
54062
|
+
...new Set(
|
|
54063
|
+
lastUserText.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3)
|
|
54064
|
+
)
|
|
54065
|
+
].slice(0, 10);
|
|
54066
|
+
const [assetRes, creativeRes, templateRes] = await Promise.allSettled([
|
|
54067
|
+
toolCtx.onSearchBrandAssets ? toolCtx.onSearchBrandAssets(lastUserText) : Promise.resolve([]),
|
|
54068
|
+
toolCtx.onSearchAdCreatives ? toolCtx.onSearchAdCreatives(lastUserText) : Promise.resolve([]),
|
|
54069
|
+
toolCtx.onListBrandTemplates ? toolCtx.onListBrandTemplates(keywords) : Promise.resolve([])
|
|
54070
|
+
]);
|
|
54071
|
+
const top2Assets = (assetRes.status === "fulfilled" ? assetRes.value : []).filter((a) => a.previewDataUrl).slice(0, 2);
|
|
54072
|
+
const top2Creatives = (creativeRes.status === "fulfilled" ? creativeRes.value : []).filter((c) => c.previewDataUrl).slice(0, 2);
|
|
54073
|
+
const top2Templates = (templateRes.status === "fulfilled" ? templateRes.value : []).filter((t2) => t2.thumbnailDataUrl).slice(0, 2);
|
|
54074
|
+
console.log(
|
|
54075
|
+
`[agent:pre-fetch] brand assets=${top2Assets.length}/${assetRes.status === "fulfilled" ? assetRes.value.length : "err"} ad-creatives=${top2Creatives.length}/${creativeRes.status === "fulfilled" ? creativeRes.value.length : "err"} templates=${top2Templates.length}/${templateRes.status === "fulfilled" ? templateRes.value.length : "err"}`
|
|
54076
|
+
);
|
|
54077
|
+
const previews = [
|
|
54078
|
+
...top2Assets.map((a) => ({
|
|
54079
|
+
label: `Brand Asset \u2014 ${a.name}`,
|
|
54080
|
+
dataUrl: a.previewDataUrl
|
|
54081
|
+
})),
|
|
54082
|
+
...top2Creatives.map((c) => ({
|
|
54083
|
+
label: `Ad Creative \u2014 ${c.name}`,
|
|
54084
|
+
dataUrl: c.previewDataUrl
|
|
54085
|
+
})),
|
|
54086
|
+
...top2Templates.map((t2) => ({
|
|
54087
|
+
label: `Template \u2014 ${t2.name}`,
|
|
54088
|
+
dataUrl: t2.thumbnailDataUrl
|
|
54089
|
+
}))
|
|
54090
|
+
];
|
|
54091
|
+
if (previews.length > 0) {
|
|
54092
|
+
previews.forEach(
|
|
54093
|
+
(p) => console.log(
|
|
54094
|
+
`[agent:pre-fetch] injecting "${p.label}" as base64 image \u2014 ${Math.round(p.dataUrl.length / 1024)}KB (${p.dataUrl.slice(
|
|
54095
|
+
0,
|
|
54096
|
+
30
|
|
54097
|
+
)}...)`
|
|
54098
|
+
)
|
|
54099
|
+
);
|
|
54100
|
+
messages.push({
|
|
54101
|
+
role: "system",
|
|
54102
|
+
content: `BRAND REFERENCES (auto-fetched, up to 2 per category):
|
|
54103
|
+
${previews.map((p) => `\u2022 ${p.label}`).join(
|
|
54104
|
+
"\n"
|
|
54105
|
+
)}
|
|
54106
|
+
|
|
54107
|
+
Visual previews follow in the next message. Use them as context: match their style, reuse template layouts where relevant. If a template clearly fits the request, prefer load_template_into_frame + fill_template_slots over generate_image.`
|
|
54108
|
+
});
|
|
54109
|
+
messages.push({
|
|
54110
|
+
role: "user",
|
|
54111
|
+
content: [
|
|
54112
|
+
{
|
|
54113
|
+
type: "text",
|
|
54114
|
+
text: `Brand reference previews (${previews.length} total):`
|
|
54115
|
+
},
|
|
54116
|
+
...previews.map((p) => ({
|
|
54117
|
+
type: "image_url",
|
|
54118
|
+
image_url: { url: p.dataUrl }
|
|
54119
|
+
}))
|
|
54120
|
+
]
|
|
54121
|
+
});
|
|
54122
|
+
messages.push({
|
|
54123
|
+
role: "assistant",
|
|
54124
|
+
content: `Got it \u2014 I can see ${previews.length} brand reference(s): ${previews.map((p) => p.label).join(", ")}. I'll use these as visual context for the ad.`
|
|
54125
|
+
});
|
|
54126
|
+
}
|
|
54127
|
+
}
|
|
54128
|
+
}
|
|
53700
54129
|
for (let i = 0; i < userMessages.length; i++) {
|
|
53701
54130
|
const msg = userMessages[i];
|
|
53702
54131
|
const isLast = i === userMessages.length - 1;
|
|
@@ -53971,6 +54400,34 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
|
|
|
53971
54400
|
} : { success: false, error: result.error }
|
|
53972
54401
|
)
|
|
53973
54402
|
});
|
|
54403
|
+
const searchToolNames = /* @__PURE__ */ new Set([
|
|
54404
|
+
"find_brand_asset",
|
|
54405
|
+
"search_brand_assets",
|
|
54406
|
+
"search_ad_creatives",
|
|
54407
|
+
"list_brand_templates"
|
|
54408
|
+
]);
|
|
54409
|
+
const assetSearchResult = result;
|
|
54410
|
+
if (searchToolNames.has(name) && result.success && assetSearchResult.previewDataUrls?.length) {
|
|
54411
|
+
const previews = assetSearchResult.previewDataUrls;
|
|
54412
|
+
console.log(
|
|
54413
|
+
`[agent:mid-loop] ${name} injecting ${previews.length} preview image(s) as base64 vision content: ${previews.map(
|
|
54414
|
+
(p) => `"${p.name}" (${Math.round(p.dataUrl.length / 1024)}KB)`
|
|
54415
|
+
).join(", ")}`
|
|
54416
|
+
);
|
|
54417
|
+
messages.push({
|
|
54418
|
+
role: "user",
|
|
54419
|
+
content: [
|
|
54420
|
+
{
|
|
54421
|
+
type: "text",
|
|
54422
|
+
text: `Visual previews for the ${previews.length} result(s) from ${name}:`
|
|
54423
|
+
},
|
|
54424
|
+
...previews.map((p) => ({
|
|
54425
|
+
type: "image_url",
|
|
54426
|
+
image_url: { url: p.dataUrl }
|
|
54427
|
+
}))
|
|
54428
|
+
]
|
|
54429
|
+
});
|
|
54430
|
+
}
|
|
53974
54431
|
if (name === "generate_image" && result.success && imageGenData) {
|
|
53975
54432
|
const autoFinalizeId = `auto_finalize_${Date.now()}`;
|
|
53976
54433
|
const autoFinalizeArgs = { frameId: imageGenData.frameId };
|
|
@@ -54594,8 +55051,11 @@ var AIChatPanel = React65.forwardRef(
|
|
|
54594
55051
|
agentImageModel,
|
|
54595
55052
|
brandContext,
|
|
54596
55053
|
onSearchBrandAssets,
|
|
55054
|
+
onSearchAdCreatives,
|
|
54597
55055
|
onListBrandTemplates,
|
|
54598
55056
|
onGetTemplateVariant,
|
|
55057
|
+
onFindBrandAssets,
|
|
55058
|
+
onMentionSearch,
|
|
54599
55059
|
reviewerModel,
|
|
54600
55060
|
maxReviewRounds
|
|
54601
55061
|
}, ref) => {
|
|
@@ -54617,6 +55077,8 @@ var AIChatPanel = React65.forwardRef(
|
|
|
54617
55077
|
const [frameRef, setFrameRef] = useState56(null);
|
|
54618
55078
|
const [mentionMenuOpen, setMentionMenuOpen] = useState56(false);
|
|
54619
55079
|
const [availableFrames, setAvailableFrames] = useState56([]);
|
|
55080
|
+
const [brandMentions, setBrandMentions] = useState56([]);
|
|
55081
|
+
const [mentionLoading, setMentionLoading] = useState56(false);
|
|
54620
55082
|
const [attachedFiles, setAttachedFiles] = useState56([]);
|
|
54621
55083
|
const [webSearchEnabled, setWebSearchEnabled] = useState56(false);
|
|
54622
55084
|
const [isLoadingSession, setIsLoadingSession] = useState56(false);
|
|
@@ -54736,17 +55198,49 @@ var AIChatPanel = React65.forwardRef(
|
|
|
54736
55198
|
},
|
|
54737
55199
|
[saveCurrentSession, onSessionSwitch]
|
|
54738
55200
|
);
|
|
55201
|
+
const searchBrandMentions = useCallback29(
|
|
55202
|
+
(query) => {
|
|
55203
|
+
if (!onMentionSearch) {
|
|
55204
|
+
return;
|
|
55205
|
+
}
|
|
55206
|
+
setMentionLoading(true);
|
|
55207
|
+
onMentionSearch(query).then((results) => setBrandMentions(results)).catch(() => setBrandMentions([])).finally(() => setMentionLoading(false));
|
|
55208
|
+
},
|
|
55209
|
+
[onMentionSearch]
|
|
55210
|
+
);
|
|
54739
55211
|
const handlePromptChange = useCallback29(
|
|
54740
55212
|
(value) => {
|
|
54741
55213
|
const prevValue = prevPromptRef.current;
|
|
54742
55214
|
setPrompt(value);
|
|
54743
55215
|
prevPromptRef.current = value;
|
|
54744
|
-
if (
|
|
54745
|
-
|
|
55216
|
+
if (value.length > prevValue.length && value.endsWith("@")) {
|
|
55217
|
+
if (excalidrawAPI) {
|
|
55218
|
+
setAvailableFrames(listFrames(excalidrawAPI));
|
|
55219
|
+
}
|
|
55220
|
+
setBrandMentions([]);
|
|
54746
55221
|
setMentionMenuOpen(true);
|
|
55222
|
+
searchBrandMentions("");
|
|
55223
|
+
return;
|
|
55224
|
+
}
|
|
55225
|
+
if (mentionMenuOpen) {
|
|
55226
|
+
const atIndex = value.lastIndexOf("@");
|
|
55227
|
+
if (atIndex >= 0) {
|
|
55228
|
+
const query = value.slice(atIndex + 1);
|
|
55229
|
+
if (excalidrawAPI) {
|
|
55230
|
+
const all = listFrames(excalidrawAPI);
|
|
55231
|
+
setAvailableFrames(
|
|
55232
|
+
query ? all.filter(
|
|
55233
|
+
(f) => f.name.toLowerCase().includes(query.toLowerCase())
|
|
55234
|
+
) : all
|
|
55235
|
+
);
|
|
55236
|
+
}
|
|
55237
|
+
searchBrandMentions(query);
|
|
55238
|
+
} else {
|
|
55239
|
+
setMentionMenuOpen(false);
|
|
55240
|
+
}
|
|
54747
55241
|
}
|
|
54748
55242
|
},
|
|
54749
|
-
[excalidrawAPI]
|
|
55243
|
+
[excalidrawAPI, mentionMenuOpen, searchBrandMentions]
|
|
54750
55244
|
);
|
|
54751
55245
|
const handlePickFrame = useCallback29(
|
|
54752
55246
|
async (frame) => {
|
|
@@ -54780,6 +55274,50 @@ var AIChatPanel = React65.forwardRef(
|
|
|
54780
55274
|
const handleRemoveRef = useCallback29(() => {
|
|
54781
55275
|
setFrameRef(null);
|
|
54782
55276
|
}, []);
|
|
55277
|
+
const handlePickBrandMention = useCallback29(
|
|
55278
|
+
async (item) => {
|
|
55279
|
+
setMentionMenuOpen(false);
|
|
55280
|
+
setPrompt((prev) => {
|
|
55281
|
+
const atIndex = prev.lastIndexOf("@");
|
|
55282
|
+
return atIndex >= 0 ? prev.slice(0, atIndex) : prev;
|
|
55283
|
+
});
|
|
55284
|
+
prevPromptRef.current = (() => {
|
|
55285
|
+
const atIndex = prevPromptRef.current.lastIndexOf("@");
|
|
55286
|
+
return atIndex >= 0 ? prevPromptRef.current.slice(0, atIndex) : prevPromptRef.current;
|
|
55287
|
+
})();
|
|
55288
|
+
if (!onFindBrandAssets) {
|
|
55289
|
+
return;
|
|
55290
|
+
}
|
|
55291
|
+
try {
|
|
55292
|
+
const { results } = await onFindBrandAssets(item.type, [
|
|
55293
|
+
`id:${item.id}`
|
|
55294
|
+
]);
|
|
55295
|
+
const match = results.find((r) => r.id === item.id) ?? results[0];
|
|
55296
|
+
if (!match?.previewDataUrl) {
|
|
55297
|
+
return;
|
|
55298
|
+
}
|
|
55299
|
+
const dataUrl = match.previewDataUrl;
|
|
55300
|
+
const mimeMatch = dataUrl.match(/^data:([^;]+)/);
|
|
55301
|
+
const mime = mimeMatch?.[1] ?? "image/png";
|
|
55302
|
+
const ext = mime.split("/")[1] ?? "png";
|
|
55303
|
+
const res = await fetch(dataUrl);
|
|
55304
|
+
const blob = await res.blob();
|
|
55305
|
+
const file2 = new File([blob], `${match.name}.${ext}`, { type: mime });
|
|
55306
|
+
setAttachedFiles((prev) => [
|
|
55307
|
+
...prev,
|
|
55308
|
+
{
|
|
55309
|
+
id: genId(),
|
|
55310
|
+
file: file2,
|
|
55311
|
+
name: match.name,
|
|
55312
|
+
type: "image",
|
|
55313
|
+
dataUrl
|
|
55314
|
+
}
|
|
55315
|
+
]);
|
|
55316
|
+
} catch {
|
|
55317
|
+
}
|
|
55318
|
+
},
|
|
55319
|
+
[onFindBrandAssets]
|
|
55320
|
+
);
|
|
54783
55321
|
const handleFileUpload = useCallback29(
|
|
54784
55322
|
async (e) => {
|
|
54785
55323
|
const files = e.target.files;
|
|
@@ -55167,8 +55705,10 @@ var AIChatPanel = React65.forwardRef(
|
|
|
55167
55705
|
signal: controller.signal,
|
|
55168
55706
|
agentImageModel,
|
|
55169
55707
|
onSearchBrandAssets,
|
|
55708
|
+
onSearchAdCreatives,
|
|
55170
55709
|
onListBrandTemplates,
|
|
55171
|
-
onGetTemplateVariant
|
|
55710
|
+
onGetTemplateVariant,
|
|
55711
|
+
onFindBrandAssets
|
|
55172
55712
|
},
|
|
55173
55713
|
onUpdate: (update) => {
|
|
55174
55714
|
if (update.type === "content_delta") {
|
|
@@ -55578,30 +56118,55 @@ var AIChatPanel = React65.forwardRef(
|
|
|
55578
56118
|
)
|
|
55579
56119
|
] }, f.id)) }),
|
|
55580
56120
|
/* @__PURE__ */ jsx194("div", { className: "acp-input-area", children: /* @__PURE__ */ jsxs107("div", { className: "acp-input-wrapper", ref: mentionMenuRef, children: [
|
|
55581
|
-
mentionMenuOpen && /* @__PURE__ */
|
|
55582
|
-
"
|
|
55583
|
-
|
|
55584
|
-
|
|
55585
|
-
|
|
55586
|
-
|
|
55587
|
-
|
|
55588
|
-
|
|
55589
|
-
|
|
55590
|
-
|
|
55591
|
-
|
|
55592
|
-
|
|
55593
|
-
|
|
55594
|
-
|
|
55595
|
-
|
|
55596
|
-
|
|
55597
|
-
|
|
55598
|
-
|
|
55599
|
-
|
|
55600
|
-
|
|
55601
|
-
|
|
55602
|
-
|
|
55603
|
-
|
|
55604
|
-
|
|
56121
|
+
mentionMenuOpen && /* @__PURE__ */ jsxs107("div", { className: "acp-mention-menu", children: [
|
|
56122
|
+
availableFrames.length > 0 && /* @__PURE__ */ jsxs107("div", { className: "acp-mention-section", children: [
|
|
56123
|
+
/* @__PURE__ */ jsx194("div", { className: "acp-mention-section-label", children: "Frames" }),
|
|
56124
|
+
availableFrames.map((f) => /* @__PURE__ */ jsxs107(
|
|
56125
|
+
"button",
|
|
56126
|
+
{
|
|
56127
|
+
className: "acp-mention-item",
|
|
56128
|
+
onClick: () => handlePickFrame(f),
|
|
56129
|
+
children: [
|
|
56130
|
+
/* @__PURE__ */ jsx194(AtSign, { size: 14 }),
|
|
56131
|
+
/* @__PURE__ */ jsxs107("span", { children: [
|
|
56132
|
+
f.name,
|
|
56133
|
+
" ",
|
|
56134
|
+
/* @__PURE__ */ jsxs107("span", { className: "acp-mention-dim", children: [
|
|
56135
|
+
"(",
|
|
56136
|
+
f.width,
|
|
56137
|
+
"x",
|
|
56138
|
+
f.height,
|
|
56139
|
+
", ",
|
|
56140
|
+
f.childCount,
|
|
56141
|
+
" items)"
|
|
56142
|
+
] })
|
|
56143
|
+
] })
|
|
56144
|
+
]
|
|
56145
|
+
},
|
|
56146
|
+
f.id
|
|
56147
|
+
))
|
|
56148
|
+
] }),
|
|
56149
|
+
(brandMentions.length > 0 || mentionLoading) && /* @__PURE__ */ jsxs107("div", { className: "acp-mention-section", children: [
|
|
56150
|
+
/* @__PURE__ */ jsx194("div", { className: "acp-mention-section-label", children: "Brand" }),
|
|
56151
|
+
mentionLoading ? /* @__PURE__ */ jsx194("div", { className: "acp-mention-loading", children: "Searching\u2026" }) : brandMentions.map((item) => /* @__PURE__ */ jsxs107(
|
|
56152
|
+
"button",
|
|
56153
|
+
{
|
|
56154
|
+
className: "acp-mention-item",
|
|
56155
|
+
onClick: () => handlePickBrandMention(item),
|
|
56156
|
+
children: [
|
|
56157
|
+
/* @__PURE__ */ jsx194(AtSign, { size: 14 }),
|
|
56158
|
+
/* @__PURE__ */ jsxs107("span", { children: [
|
|
56159
|
+
item.name,
|
|
56160
|
+
" ",
|
|
56161
|
+
/* @__PURE__ */ jsx194("span", { className: "acp-mention-dim", children: item.type === "template" ? "template" : item.type === "ad-creative" ? "ad creative" : "asset" })
|
|
56162
|
+
] })
|
|
56163
|
+
]
|
|
56164
|
+
},
|
|
56165
|
+
`${item.type}:${item.id}`
|
|
56166
|
+
))
|
|
56167
|
+
] }),
|
|
56168
|
+
availableFrames.length === 0 && brandMentions.length === 0 && !mentionLoading && /* @__PURE__ */ jsx194("div", { className: "acp-mention-empty", children: "No frames or brand items found. Type to search your brand library." })
|
|
56169
|
+
] }),
|
|
55605
56170
|
/* @__PURE__ */ jsx194(
|
|
55606
56171
|
"input",
|
|
55607
56172
|
{
|
|
@@ -55925,6 +56490,11 @@ function buildPlainChatBrandContext(ctx) {
|
|
|
55925
56490
|
const lines = [
|
|
55926
56491
|
"## Brand Identity \u2014 use these guidelines for copywriting and design suggestions"
|
|
55927
56492
|
];
|
|
56493
|
+
if (ctx.name) {
|
|
56494
|
+
lines.push(
|
|
56495
|
+
`CRITICAL BRAND RULE: All outputs in this session are exclusively for the brand "${ctx.name}". Every design, copy, image, and suggestion MUST be built around "${ctx.name}" only. Do not reference or promote any other brand or product.`
|
|
56496
|
+
);
|
|
56497
|
+
}
|
|
55928
56498
|
if (ctx.colors) {
|
|
55929
56499
|
const { primary, secondary, accent, background } = ctx.colors;
|
|
55930
56500
|
const parts = [
|
|
@@ -56039,6 +56609,7 @@ var ExcalidrawBase = (props) => {
|
|
|
56039
56609
|
onSearchBrandAssets,
|
|
56040
56610
|
onListBrandTemplates,
|
|
56041
56611
|
onGetTemplateVariant,
|
|
56612
|
+
onFindBrandAssets,
|
|
56042
56613
|
brandContext
|
|
56043
56614
|
} = props;
|
|
56044
56615
|
const canvasActions = props.UIOptions?.canvasActions;
|
|
@@ -56135,6 +56706,7 @@ var ExcalidrawBase = (props) => {
|
|
|
56135
56706
|
onSearchBrandAssets,
|
|
56136
56707
|
onListBrandTemplates,
|
|
56137
56708
|
onGetTemplateVariant,
|
|
56709
|
+
onFindBrandAssets,
|
|
56138
56710
|
brandContext,
|
|
56139
56711
|
children
|
|
56140
56712
|
}
|
|
@@ -56235,6 +56807,9 @@ export {
|
|
|
56235
56807
|
mergeLibraryItems,
|
|
56236
56808
|
mutateElement2 as mutateElement,
|
|
56237
56809
|
newElementWith12 as newElementWith,
|
|
56810
|
+
newFrameElement5 as newFrameElement,
|
|
56811
|
+
newImageElement5 as newImageElement,
|
|
56812
|
+
newTextElement7 as newTextElement,
|
|
56238
56813
|
normalizeLink4 as normalizeLink,
|
|
56239
56814
|
parseLibraryTokensFromUrl,
|
|
56240
56815
|
reconcileElements,
|