@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/index.js CHANGED
@@ -66,10 +66,10 @@ import {
66
66
  serializeAsJSON,
67
67
  serializeLibraryAsJSON,
68
68
  strokeRectWithRotation_simple
69
- } from "./chunk-OIMA545L.js";
69
+ } from "./chunk-E6AX7UYW.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-DG4DHKBY.js";
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-AWVYCS2V.js").then(
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. Use the full canvas edge-to-edge with no borders, padding bars, or empty margins.
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 fonts are ${fontNames.join(
25550
+ const fontHint = fontNames.length ? ` Brand typography: the brand font${fontNames.length > 1 ? "s are" : " is"} ${fontNames.join(
25547
25551
  " and "
25548
- )} \u2014 render the on-image text in the brand fonts and use your judgment for which suits headlines vs body copy.` : "";
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
- "BRAND LOGO: The first reference image is the brand logo. You MUST reproduce it accurately and place it prominently in the ad (e.g. top-left corner, bottom bar, or wherever suits the layout). Do not alter, distort, or omit the logo."
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 font reference ${multiple ? "images show the brand fonts" : "image shows the brand font"}. Render all on-image text in the brand typeface${multiple ? "s" : ""} \u2014 match the letterforms, weight, and character closely${multiple ? ", using your judgment for which font fits headlines vs body copy" : ""}.`
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
- return `${prompt}${fontHint}${brandImageHint} Render all specified copy as crisp, correctly-spelled, legible text within the image. Professional advertising quality.`;
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 safePrompt = brandContext ? buildImageGenSafePrompt(prompt, brandContext) : prompt;
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 fonts are ${fontNames.join(
52524
+ const fontHint = fontNames.length ? ` Brand typography: the brand font${fontNames.length > 1 ? "s are" : " is"} ${fontNames.join(
52445
52525
  " and "
52446
- )} \u2014 render the on-image text in the brand fonts and use your judgment for which suits headlines vs body copy.` : "";
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
- "BRAND LOGO: The first reference image is the brand logo. You MUST reproduce it accurately and place it prominently in the ad (e.g. top-left corner, bottom bar, or wherever suits the layout). Do not alter, distort, or omit the logo."
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 font reference ${multiple ? "images show the brand fonts" : "image shows the brand font"}. Render all on-image text in the brand typeface${multiple ? "s" : ""} \u2014 match the letterforms, weight, and character closely${multiple ? ", using your judgment for which font fits headlines vs body copy" : ""}.`
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} Render all specified copy as crisp, correctly-spelled, legible text within the image. Do NOT squish, stretch, or distort any element horizontally or vertically \u2014 maintain the natural aspect ratio of all visuals, logos, and text. Professional advertising quality.`;
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 aspectRatio = width >= height ? width / height > 1.3 ? "16:9" : "1:1" : "9:16";
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: absX,
52524
- y: absY,
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 list = assets.map((a) => `- ${a.name} (${a.assetType}) [id:${a.id}]: ${a.blobUrl}`).join("\n");
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 ${assets.length} brand asset(s):
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 ${assets.length} assets`
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
- Aim for a polished, professional advertising creative \u2014 typography crisp and legible, copy correctly spelled, strong hierarchy, brand-consistent.
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
- **DEFAULT \u2014 do NOT call \`search_brand_assets\` or \`list_brand_templates\`** unless the user explicitly asks to use an existing asset or template (e.g. "use our product photo", "use the summer template", "find a template"). For a standard from-scratch generation proceed directly: \`create_frame\` \u2192 \`generate_image\` \u2192 \`finalize_ad\`.
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
- ASSET BANK: Only when the user explicitly asks to reference an existing brand visual, call \`search_brand_assets()\` to find it. When a relevant asset exists, pass it to \`generate_image\` as a reference so the generated ad stays on-brand.
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 (alternative path): If the user wants to use a pre-built template instead of generating from scratch, call \`list_brand_templates()\`, present matches, then:
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 (excalidrawAPI && value.length > prevValue.length && value.endsWith("@")) {
54745
- setAvailableFrames(listFrames(excalidrawAPI));
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__ */ jsx194("div", { className: "acp-mention-menu", children: availableFrames.length > 0 ? availableFrames.map((f) => /* @__PURE__ */ jsxs107(
55582
- "button",
55583
- {
55584
- className: "acp-mention-item",
55585
- onClick: () => handlePickFrame(f),
55586
- children: [
55587
- /* @__PURE__ */ jsx194(AtSign, { size: 14 }),
55588
- /* @__PURE__ */ jsxs107("span", { children: [
55589
- f.name,
55590
- " ",
55591
- /* @__PURE__ */ jsxs107("span", { className: "acp-mention-dim", children: [
55592
- "(",
55593
- f.width,
55594
- "x",
55595
- f.height,
55596
- ", ",
55597
- f.childCount,
55598
- " items)"
55599
- ] })
55600
- ] })
55601
- ]
55602
- },
55603
- f.id
55604
- )) : /* @__PURE__ */ jsx194("div", { className: "acp-mention-empty", children: "No frames on the canvas. Create a frame first, then type @ to reference it." }) }),
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,