@orangecatai/adgen-canvas 0.0.29 → 0.0.31

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-MKNEZM6D.js";
69
+ } from "./chunk-N472V5Q6.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-FOD3MMRJ.js";
72
+ } from "./chunk-3SKNYE32.js";
73
73
  import {
74
74
  en_default
75
75
  } from "./chunk-LIELFD5T.js";
@@ -9796,7 +9796,7 @@ var exportCanvas = async (type, elements, appState, files, {
9796
9796
  let blob = canvasToBlob(tempCanvas);
9797
9797
  if (appState.exportEmbedScene) {
9798
9798
  blob = blob.then(
9799
- (blob2) => import("./data/image-GKX5OVTE.js").then(
9799
+ (blob2) => import("./data/image-C5LHCG7R.js").then(
9800
9800
  ({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
9801
9801
  blob: blob2,
9802
9802
  metadata: serializeAsJSON(elements, appState, files, "local")
@@ -24225,6 +24225,7 @@ async function runReviewerAgent(opts) {
24225
24225
  // imagePlacement retained in opts type for backward compatibility but unused
24226
24226
  // in the image-first flow (the ad is a single full-frame generated image).
24227
24227
  userBrief,
24228
+ originalImageDataUrl,
24228
24229
  brandContext,
24229
24230
  chatUrl,
24230
24231
  reviewerModel = "google/gemini-2.5-flash",
@@ -24244,6 +24245,7 @@ async function runReviewerAgent(opts) {
24244
24245
  const fontPreviewIsBase64 = typeof fontPreviewDataUrl === "string" && fontPreviewDataUrl.startsWith("data:");
24245
24246
  const bodyFontPreviewDataUrl = brandContext?.bodyFontPreviewDataUrl;
24246
24247
  const bodyFontPreviewIsBase64 = typeof bodyFontPreviewDataUrl === "string" && bodyFontPreviewDataUrl.startsWith("data:");
24248
+ const originalImageIsBase64 = typeof originalImageDataUrl === "string" && originalImageDataUrl.startsWith("data:");
24247
24249
  const contextLines = [
24248
24250
  `## Ad Brief (what the user requested)`,
24249
24251
  userBrief,
@@ -24251,6 +24253,13 @@ async function runReviewerAgent(opts) {
24251
24253
  `## Frame dimensions`,
24252
24254
  `Width: ${frame.width}px, Height: ${frame.height}px`
24253
24255
  ];
24256
+ if (originalImageIsBase64) {
24257
+ contextLines.push(
24258
+ ``,
24259
+ `## Original reference image (what the user uploaded)`,
24260
+ `The image the user originally attached is included below (immediately after the ad screenshot). Judge fidelity against it: the generated ad should preserve the subject, composition, and style of this reference unless the brief explicitly asked for a different treatment. Flag it as an issue if the ad has drifted from this reference in ways the brief did not request (wrong subject, different color scheme, altered composition).`
24261
+ );
24262
+ }
24254
24263
  if (brandContext) {
24255
24264
  const brandSummary = buildReviewerBrandSummary(brandContext);
24256
24265
  if (brandSummary) {
@@ -24261,7 +24270,7 @@ async function runReviewerAgent(opts) {
24261
24270
  contextLines.push(
24262
24271
  ``,
24263
24272
  `## Brand logo (reference)`,
24264
- `The brand logo is attached as the second image below (after the ad screenshot). Use it to evaluate whether the logo in the ad matches the brand's actual logo in colour, shape, and typographic treatment.`
24273
+ `The brand logo is attached below (after the ad screenshot${originalImageIsBase64 ? " and the original reference image" : ""}). Use it to evaluate whether the logo in the ad matches the brand's actual logo in colour, shape, and typographic treatment.`
24265
24274
  );
24266
24275
  } else if (logoIsExternalUrl) {
24267
24276
  contextLines.push(
@@ -24292,6 +24301,12 @@ async function runReviewerAgent(opts) {
24292
24301
  image_url: { url: screenshotDataUrl }
24293
24302
  }
24294
24303
  ];
24304
+ if (originalImageIsBase64 && originalImageDataUrl) {
24305
+ userContent.push({
24306
+ type: "image_url",
24307
+ image_url: { url: originalImageDataUrl }
24308
+ });
24309
+ }
24295
24310
  if (logoIsBase64 && logoDataUrl) {
24296
24311
  userContent.push({
24297
24312
  type: "image_url",
@@ -26049,7 +26064,7 @@ var SIZE_SCALE_MAP = {
26049
26064
  };
26050
26065
  function buildBrandContextMessage(ctx) {
26051
26066
  const lines = [
26052
- "## Brand Identity \u2014 apply these guidelines to all ads you create"
26067
+ "## Brand Identity \u2014 you MUST apply every guideline below to all ads you create, by default"
26053
26068
  ];
26054
26069
  if (ctx.colors) {
26055
26070
  const { primary, secondary, accent, background } = ctx.colors;
@@ -26124,6 +26139,10 @@ function buildBrandContextMessage(ctx) {
26124
26139
  lines.push(`**Imagery direction**: ${parts.join(", ")}`);
26125
26140
  }
26126
26141
  }
26142
+ lines.push(
26143
+ "",
26144
+ "**Precedence**: these are defaults, not absolutes. If the user's request explicitly states or clearly implies something different from a guideline above \u2014 a different color, mood, style, subject treatment, or tone \u2014 follow the user's explicit instruction for that ad instead of the brand guideline it conflicts with. Do not silently override what the user asked for in favor of brand consistency; only apply a guideline where the user's request is silent on that point."
26145
+ );
26127
26146
  return lines.join("\n");
26128
26147
  }
26129
26148
  function buildImageGenSafePrompt(prompt, brandContext, dimensions) {
@@ -26153,7 +26172,9 @@ function buildImageGenSafePrompt(prompt, brandContext, dimensions) {
26153
26172
  }
26154
26173
  const brandImageHint = brandImageInstructions.length > 0 ? ` ${brandImageInstructions.join(" ")}` : "";
26155
26174
  const brandNameHint = brandContext.name ? ` This ad is for the brand "${brandContext.name}" \u2014 all visual elements, copy, and style must reflect this brand exclusively.` : "";
26156
- 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.`;
26175
+ 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.
26176
+
26177
+ ${PRECEDENCE_LINE}`;
26157
26178
  }
26158
26179
  var clean = (value) => {
26159
26180
  const trimmed = value?.trim();
@@ -56705,6 +56726,39 @@ function execAddText(args, ctx) {
56705
56726
  statusMessage: `Added text "${args.text.slice(0, 40)}${args.text.length > 40 ? "\u2026" : ""}"`
56706
56727
  };
56707
56728
  }
56729
+ var frameGenerationMemory = /* @__PURE__ */ new Map();
56730
+ var chatCreatedFrames = [];
56731
+ function getChatCreatedFramesContext() {
56732
+ if (chatCreatedFrames.length === 0)
56733
+ return "";
56734
+ const lines = chatCreatedFrames.map(
56735
+ (f, i) => `${i + 1}. frameId="${f.frameId}"${i === chatCreatedFrames.length - 1 ? " (most recent)" : ""} \u2014 ${f.summary}`
56736
+ );
56737
+ return [
56738
+ `## Frames generated in this conversation (not all frames on the canvas \u2014 the canvas may contain other unrelated images/content)`,
56739
+ ...lines,
56740
+ ``,
56741
+ `When the user refers to an image ambiguously \u2014 "the previous one," "that image," "the above ad," "fix it" \u2014 without an @mention or a distinguishing description, they mean the MOST RECENT frame in this list. Do NOT call list_frames() to search the whole canvas for it; use the frameId given here directly. Only search more broadly (list_frames) if the user's wording clearly points at something NOT in this list, or if this list has more than one plausible match and the user's description (e.g. "the dog one," "the second one") disambiguates between entries here.`
56742
+ ].join("\n");
56743
+ }
56744
+ function resetAndSeedFrameGenerationMemory(messages) {
56745
+ frameGenerationMemory.clear();
56746
+ chatCreatedFrames.length = 0;
56747
+ for (const msg of messages) {
56748
+ const g = msg.generatedFrame;
56749
+ if (!g || frameGenerationMemory.has(g.frameId))
56750
+ continue;
56751
+ frameGenerationMemory.set(g.frameId, {
56752
+ originalPrompt: g.prompt,
56753
+ referenceImageIndex: g.referenceImageIndex,
56754
+ userBrief: g.userBrief
56755
+ });
56756
+ chatCreatedFrames.push({
56757
+ frameId: g.frameId,
56758
+ summary: g.prompt.slice(0, 80) + (g.prompt.length > 80 ? "\u2026" : "")
56759
+ });
56760
+ }
56761
+ }
56708
56762
  async function execGenerateImage(args, ctx) {
56709
56763
  const frameId = args.frameId;
56710
56764
  const prompt = args.prompt;
@@ -56713,7 +56767,19 @@ async function execGenerateImage(args, ctx) {
56713
56767
  const width = args.width;
56714
56768
  const height = args.height;
56715
56769
  const referenceImageIndex = args.referenceImageIndex;
56716
- const referenceImageDataUrl = referenceImageIndex !== void 0 ? ctx.fileAttachments?.[referenceImageIndex]?.dataUrl : void 0;
56770
+ const priorGeneration = frameGenerationMemory.get(frameId);
56771
+ const effectiveReferenceImageIndex = referenceImageIndex !== void 0 ? referenceImageIndex : priorGeneration ? priorGeneration.referenceImageIndex : ctx.fileAttachments && ctx.fileAttachments.length > 0 ? 0 : void 0;
56772
+ const referenceImageDataUrl = effectiveReferenceImageIndex !== void 0 ? ctx.fileAttachments?.[effectiveReferenceImageIndex]?.dataUrl : void 0;
56773
+ const isRegeneration = !!priorGeneration && priorGeneration.originalPrompt !== prompt;
56774
+ const originalPromptAnchor = isRegeneration ? `
56775
+
56776
+ ORIGINAL AD BRIEF (this is a revision, not a new ad \u2014 keep the same subject, composition, and color palette as this original brief unless a fix above explicitly changes it):
56777
+ "${priorGeneration.originalPrompt}"` : "";
56778
+ const userBrief = priorGeneration?.userBrief ?? ctx.originalUserBrief;
56779
+ const userBriefAnchor = userBrief ? `
56780
+
56781
+ USER'S EXACT REQUEST (verbatim, do not paraphrase, simplify, or substitute the specific subject, action, pose, expression, or scene described here \u2014 depict it faithfully even if the description above worded it differently):
56782
+ "${userBrief}"` : "";
56717
56783
  const fontNames = [ctx.brandHeadlineFontName, ctx.brandBodyFontName].filter(
56718
56784
  (n, i, arr) => Boolean(n) && arr.indexOf(n) === i
56719
56785
  );
@@ -56748,7 +56814,7 @@ LAYOUT & SPACING RULES \u2014 these are non-negotiable:
56748
56814
  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.
56749
56815
  7. ASPECT RATIO: Do NOT squish, stretch, or distort any element. Maintain the natural proportions of all visuals, logos, and text at all times.
56750
56816
 
56751
- Render all text as crisp, correctly-spelled, legible copy. Professional advertising quality \u2014 strong visual hierarchy, brand-consistent, generous whitespace throughout.`;
56817
+ Render all text as crisp, correctly-spelled, legible copy. Professional advertising quality \u2014 strong visual hierarchy, brand-consistent, generous whitespace throughout.${originalPromptAnchor}${userBriefAnchor}`;
56752
56818
  if (!frameId || !prompt) {
56753
56819
  return {
56754
56820
  success: false,
@@ -56892,6 +56958,19 @@ Render all text as crisp, correctly-spelled, legible copy. Professional advertis
56892
56958
  lastRetrieved: Date.now()
56893
56959
  }
56894
56960
  ]);
56961
+ if (!priorGeneration) {
56962
+ frameGenerationMemory.set(frameId, {
56963
+ originalPrompt: prompt,
56964
+ referenceImageIndex: effectiveReferenceImageIndex,
56965
+ userBrief
56966
+ });
56967
+ chatCreatedFrames.push({
56968
+ frameId,
56969
+ summary: prompt.slice(0, 80) + (prompt.length > 80 ? "\u2026" : "")
56970
+ });
56971
+ } else if (effectiveReferenceImageIndex !== void 0) {
56972
+ priorGeneration.referenceImageIndex = effectiveReferenceImageIndex;
56973
+ }
56895
56974
  return {
56896
56975
  success: true,
56897
56976
  data: {
@@ -56901,7 +56980,8 @@ Render all text as crisp, correctly-spelled, legible copy. Professional advertis
56901
56980
  x: Math.round(absX),
56902
56981
  y: Math.round(absY),
56903
56982
  width: Math.round(width),
56904
- height: Math.round(height)
56983
+ height: Math.round(height),
56984
+ ...effectiveReferenceImageIndex !== void 0 ? { referenceImageIndex: effectiveReferenceImageIndex } : {}
56905
56985
  },
56906
56986
  statusMessage: `Generated image from prompt "${prompt.slice(0, 50)}${prompt.length > 50 ? "\u2026" : ""}"`
56907
56987
  };
@@ -58374,6 +58454,18 @@ function isStreamingResponse(response) {
58374
58454
  }
58375
58455
 
58376
58456
  // components/ai-chat/agentLoop.ts
58457
+ function isRasterImageDataUrl(dataUrl) {
58458
+ return /^data:image\/(png|jpe?g|gif|webp);base64,/i.test(dataUrl);
58459
+ }
58460
+ function dataUrlByteSize(dataUrl) {
58461
+ const commaIdx = dataUrl.indexOf(",");
58462
+ const b64 = commaIdx >= 0 ? dataUrl.slice(commaIdx + 1) : dataUrl;
58463
+ return Math.floor(b64.length * 3 / 4);
58464
+ }
58465
+ var MAX_REQUEST_IMAGE_BYTES = 8 * 1024 * 1024;
58466
+ var BRAND_CONTEXT_IMAGE_BUDGET = MAX_REQUEST_IMAGE_BYTES / 2;
58467
+ var PER_BRAND_IMAGE_BYTE_CAP = 600 * 1024;
58468
+ var MAX_TOTAL_IMAGES = 18;
58377
58469
  var SYSTEM_PROMPT = `You are an expert advertising art director embedded inside a canvas editor. You create finished advertisement creatives by generating a single complete image \u2014 visuals AND all copy baked directly into the image \u2014 using an AI image model. You do NOT assemble ads from separate text/shape layers.
58378
58470
 
58379
58471
  ## Core principle \u2014 ONE image IS the ad
@@ -58389,7 +58481,7 @@ Check: is the user asking to change something in an ad that already exists on th
58389
58481
  Triggers: "change the CTA", "update the headline", "change the button text", "make the headline say X", "use a different background", "change the colors", "different photo", etc.
58390
58482
 
58391
58483
  **For a from-scratch (image-generated) ad, the copy is baked into the image \u2014 it CANNOT be patched element-by-element.** To change anything (text or visuals):
58392
- 1. **Find the frame** \u2014 use the frameId from an @mention or your own tool-call history. If unsure, call \`list_frames()\`.
58484
+ 1. **Find the frame** \u2014 use the frameId from an @mention if present. Otherwise, for a vague reference like "the previous one", "that image", "the above ad", or "fix it", use the most recent frameId from the "Frames generated in this conversation" list provided to you \u2014 do NOT call \`list_frames()\` for this, the canvas may contain other unrelated frames that list excludes on purpose. Only call \`list_frames()\` when that list is empty or genuinely doesn't cover what the user means.
58393
58485
  2. **Duplicate the frame to a fresh, non-overlapping position** (you keep the original intact).
58394
58486
  3. Call \`generate_image\` on the duplicate with the SAME prompt as the original, modified with the requested change (new copy text, new visual direction, etc.).
58395
58487
  4. Call \`finalize_ad\` on the new frame.
@@ -58431,11 +58523,13 @@ The \`generate_image\` prompt is the ONLY creative call \u2014 it must produce t
58431
58523
  - Sign-off / brand name: exact words.
58432
58524
  Spell every word exactly as given; do not paraphrase on-image text.
58433
58525
 
58434
- 2. **BRAND COLORS** (use exact hex values from the Brand Identity context when present): which color is the background, the headline, the CTA button, accents.
58526
+ 2. **BRAND COLORS** (use exact hex values from the Brand Identity context when present, by default): which color is the background, the headline, the CTA button, accents.
58435
58527
 
58436
- 3. **TYPOGRAPHY STYLE** (described in words; the image model cannot load font files): describe the feel \u2014 e.g. "bold condensed sans-serif headline, clean body text", matching the brand typography from the Brand Identity context.
58528
+ 3. **TYPOGRAPHY STYLE** (described in words; the image model cannot load font files): describe the feel \u2014 e.g. "bold condensed sans-serif headline, clean body text", matching the brand typography from the Brand Identity context, by default.
58437
58529
 
58438
- 4. **VISUAL DIRECTION:** composition and layout (where the subject sits, where copy sits), lighting, mood, and emotional tone from the brief and \`imagery\` / \`visualStyle\`.
58530
+ **Brand guidelines vs. the user's request \u2014 precedence:** Brand Identity guidelines (colors, typography, visual style, tone) are defaults that apply where the user hasn't said otherwise. If the user's message explicitly asks for something that conflicts with a guideline \u2014 a different color, a different mood or style, a different tone \u2014 follow the user's explicit instruction for that ad. Do not silently revert to brand colors/style/tone over what the user actually asked for. This does not apply to the brand logo or brand font files (item 6 below) \u2014 those are always reproduced faithfully regardless of the rest of the prompt, since omitting or altering the logo isn't a stylistic choice the user is likely to have intended just by not mentioning it.
58531
+
58532
+ 4. **VISUAL DIRECTION:** composition and layout (where the subject sits, where copy sits), lighting, mood, and emotional tone from the brief and \`imagery\` / \`visualStyle\`. If the user described a specific subject, action, pose, expression, object, or animal (e.g. "boy playing guitar, dog watching with one ear up, amused"), you MUST depict that exact scene \u2014 do not invent, substitute, or simplify it into something else (e.g. do not turn "playing guitar" into "walking"). Preserve every specific detail the user gave you; only add the brand/style/spacing layers on top of it.
58439
58533
 
58440
58534
  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").
58441
58535
 
@@ -58611,8 +58705,12 @@ async function runAgentLoop(opts) {
58611
58705
  allImageAttachments.push(img);
58612
58706
  }
58613
58707
  }
58708
+ const fullUserBrief = userMessages.filter(
58709
+ (m) => m.role === "user" && typeof m.content === "string" && m.content.trim().length > 0
58710
+ ).map((m) => m.content).join("\n\n");
58614
58711
  const augmentedToolCtx = {
58615
58712
  ...toolCtx,
58713
+ ...fullUserBrief ? { originalUserBrief: fullUserBrief } : {},
58616
58714
  // Lets long-running tools (video gen) push interim progress lines into the
58617
58715
  // same status indicator the loop itself writes to.
58618
58716
  onStatus: (message) => onUpdate({ type: "status", message }),
@@ -58645,6 +58743,12 @@ async function runAgentLoop(opts) {
58645
58743
  ${elementContext}`
58646
58744
  });
58647
58745
  }
58746
+ const chatFramesContext = getChatCreatedFramesContext();
58747
+ if (chatFramesContext) {
58748
+ messages.push({ role: "system", content: chatFramesContext });
58749
+ }
58750
+ let brandContextBytesUsed = 0;
58751
+ let brandContextImagesUsed = 0;
58648
58752
  {
58649
58753
  const lastUserText = [...userMessages].reverse().find((m) => m.role === "user")?.content ?? "";
58650
58754
  if (lastUserText && (toolCtx.onSearchBrandAssets || toolCtx.onSearchAdCreatives || toolCtx.onListBrandTemplates)) {
@@ -58658,9 +58762,11 @@ ${elementContext}`
58658
58762
  toolCtx.onSearchAdCreatives ? toolCtx.onSearchAdCreatives(lastUserText) : Promise.resolve([]),
58659
58763
  toolCtx.onListBrandTemplates ? toolCtx.onListBrandTemplates(keywords) : Promise.resolve([])
58660
58764
  ]);
58661
- const top2Assets = (assetRes.status === "fulfilled" ? assetRes.value : []).filter((a) => a.previewDataUrl).slice(0, 2);
58662
- const top2Creatives = (creativeRes.status === "fulfilled" ? creativeRes.value : []).filter((c) => c.previewDataUrl).slice(0, 2);
58663
- const top2Templates = (templateRes.status === "fulfilled" ? templateRes.value : []).filter((t2) => t2.thumbnailDataUrl).slice(0, 2);
58765
+ const top2Assets = (assetRes.status === "fulfilled" ? assetRes.value : []).filter((a) => a.previewDataUrl && isRasterImageDataUrl(a.previewDataUrl)).slice(0, 2);
58766
+ const top2Creatives = (creativeRes.status === "fulfilled" ? creativeRes.value : []).filter((c) => c.previewDataUrl && isRasterImageDataUrl(c.previewDataUrl)).slice(0, 2);
58767
+ const top2Templates = (templateRes.status === "fulfilled" ? templateRes.value : []).filter(
58768
+ (t2) => t2.thumbnailDataUrl && isRasterImageDataUrl(t2.thumbnailDataUrl)
58769
+ ).slice(0, 2);
58664
58770
  console.log(
58665
58771
  `[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"}`
58666
58772
  );
@@ -58678,8 +58784,27 @@ ${elementContext}`
58678
58784
  dataUrl: t2.thumbnailDataUrl
58679
58785
  }))
58680
58786
  ];
58681
- if (previews.length > 0) {
58682
- previews.forEach(
58787
+ const budgetedPreviews = [];
58788
+ for (const p of previews) {
58789
+ const size = dataUrlByteSize(p.dataUrl);
58790
+ if (size > PER_BRAND_IMAGE_BYTE_CAP) {
58791
+ console.warn(
58792
+ `[agent:pre-fetch] dropping "${p.label}" \u2014 ${Math.round(size / 1024)}KB exceeds the per-image cap (${Math.round(PER_BRAND_IMAGE_BYTE_CAP / 1024)}KB)`
58793
+ );
58794
+ continue;
58795
+ }
58796
+ if (brandContextBytesUsed + size > BRAND_CONTEXT_IMAGE_BUDGET || brandContextImagesUsed >= MAX_TOTAL_IMAGES) {
58797
+ console.warn(
58798
+ `[agent:pre-fetch] dropping "${p.label}" \u2014 brand-context image budget exhausted`
58799
+ );
58800
+ continue;
58801
+ }
58802
+ brandContextBytesUsed += size;
58803
+ brandContextImagesUsed += 1;
58804
+ budgetedPreviews.push(p);
58805
+ }
58806
+ if (budgetedPreviews.length > 0) {
58807
+ budgetedPreviews.forEach(
58683
58808
  (p) => console.log(
58684
58809
  `[agent:pre-fetch] injecting "${p.label}" as base64 image \u2014 ${Math.round(p.dataUrl.length / 1024)}KB (${p.dataUrl.slice(
58685
58810
  0,
@@ -58690,7 +58815,7 @@ ${elementContext}`
58690
58815
  messages.push({
58691
58816
  role: "system",
58692
58817
  content: `BRAND REFERENCES (auto-fetched, up to 2 per category):
58693
- ${previews.map((p) => `\u2022 ${p.label}`).join(
58818
+ ${budgetedPreviews.map((p) => `\u2022 ${p.label}`).join(
58694
58819
  "\n"
58695
58820
  )}
58696
58821
 
@@ -58701,9 +58826,9 @@ Visual previews follow in the next message. Use them as context: match their sty
58701
58826
  content: [
58702
58827
  {
58703
58828
  type: "text",
58704
- text: `Brand reference previews (${previews.length} total):`
58829
+ text: `Brand reference previews (${budgetedPreviews.length} total):`
58705
58830
  },
58706
- ...previews.map((p) => ({
58831
+ ...budgetedPreviews.map((p) => ({
58707
58832
  type: "image_url",
58708
58833
  image_url: { url: p.dataUrl }
58709
58834
  }))
@@ -58711,17 +58836,57 @@ Visual previews follow in the next message. Use them as context: match their sty
58711
58836
  });
58712
58837
  messages.push({
58713
58838
  role: "assistant",
58714
- 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.`
58839
+ content: `Got it \u2014 I can see ${budgetedPreviews.length} brand reference(s): ${budgetedPreviews.map((p) => p.label).join(", ")}. I'll use these as visual context for the ad.`
58715
58840
  });
58716
58841
  }
58717
58842
  }
58718
58843
  }
58844
+ const frameScreenshotBytes = frameScreenshot ? dataUrlByteSize(frameScreenshot) : 0;
58845
+ const currentSendImagesForBudget = fileAttachments?.filter((a) => a.type === "image" && a.dataUrl) ?? [];
58846
+ const currentSendImageBytes = currentSendImagesForBudget.reduce(
58847
+ (sum2, a) => sum2 + dataUrlByteSize(a.dataUrl),
58848
+ 0
58849
+ );
58850
+ const currentSendImageCount = currentSendImagesForBudget.length + (frameScreenshot ? 1 : 0);
58851
+ const remainingBudgetBytes = MAX_REQUEST_IMAGE_BYTES - brandContextBytesUsed;
58852
+ const remainingBudgetImages = MAX_TOTAL_IMAGES - brandContextImagesUsed;
58853
+ if (currentSendImageBytes + frameScreenshotBytes > remainingBudgetBytes || currentSendImageCount > remainingBudgetImages) {
58854
+ const message = "This message is too large to process \u2014 try attaching fewer or smaller images.";
58855
+ console.warn(
58856
+ `[agent:budget] aborting \u2014 current-send content alone (${Math.round(
58857
+ (currentSendImageBytes + frameScreenshotBytes) / 1024
58858
+ )}KB, ${currentSendImageCount} image(s)) exceeds the remaining request budget`
58859
+ );
58860
+ onUpdate({ type: "final", message });
58861
+ return { reply: message, toolActions: [] };
58862
+ }
58863
+ let historicalBudgetBytes = remainingBudgetBytes - currentSendImageBytes - frameScreenshotBytes;
58864
+ let historicalBudgetImages = remainingBudgetImages - currentSendImageCount;
58865
+ const droppedHistoricalMessageIndices = /* @__PURE__ */ new Set();
58866
+ for (let i = userMessages.length - 1; i >= 0; i--) {
58867
+ const imgs = userMessages[i].imageAttachments;
58868
+ if (!imgs || imgs.length === 0)
58869
+ continue;
58870
+ const bytes = imgs.reduce(
58871
+ (sum2, img) => sum2 + dataUrlByteSize(img.dataUrl),
58872
+ 0
58873
+ );
58874
+ if (bytes <= historicalBudgetBytes && imgs.length <= historicalBudgetImages) {
58875
+ historicalBudgetBytes -= bytes;
58876
+ historicalBudgetImages -= imgs.length;
58877
+ } else {
58878
+ droppedHistoricalMessageIndices.add(i);
58879
+ console.warn(
58880
+ `[agent:budget] dropping ${imgs.length} historical image(s) from an earlier turn (${Math.round(bytes / 1024)}KB) \u2014 request budget exceeded`
58881
+ );
58882
+ }
58883
+ }
58719
58884
  for (let i = 0; i < userMessages.length; i++) {
58720
58885
  const msg = userMessages[i];
58721
58886
  const isLast = i === userMessages.length - 1;
58722
58887
  const lastMsgImages = isLast && fileAttachments ? fileAttachments.filter((a) => a.type === "image" && a.dataUrl) : [];
58723
58888
  const lastMsgPdfs = isLast && fileAttachments ? fileAttachments.filter((a) => a.type === "pdf" && a.dataUrl) : [];
58724
- const historicalImages2 = msg.imageAttachments ?? [];
58889
+ const historicalImages2 = droppedHistoricalMessageIndices.has(i) ? [] : msg.imageAttachments ?? [];
58725
58890
  const hasImages = historicalImages2.length > 0 || lastMsgImages.length > 0;
58726
58891
  const hasFrameScreenshot = isLast && !!frameScreenshot;
58727
58892
  const hasTextFiles = isLast && fileAttachments && fileAttachments.some((a) => a.type === "text" || a.type === "docx");
@@ -58807,7 +58972,16 @@ ${f.textContent}`).join("\n\n")}`;
58807
58972
  if (lastCreatedFrameId) {
58808
58973
  adGenShimmerStore.stopGenerating(lastCreatedFrameId);
58809
58974
  }
58810
- return { reply, toolActions };
58975
+ return {
58976
+ reply,
58977
+ toolActions,
58978
+ frameGeneration: imageGenData ? {
58979
+ frameId: imageGenData.frameId,
58980
+ prompt: imageGenData.prompt,
58981
+ referenceImageIndex: imageGenData.referenceImageIndex,
58982
+ userBrief: fullUserBrief
58983
+ } : void 0
58984
+ };
58811
58985
  }
58812
58986
  messages.push({
58813
58987
  role: "assistant",
@@ -58936,7 +59110,9 @@ ${f.textContent}`).join("\n\n")}`;
58936
59110
  x: result.data.x,
58937
59111
  y: result.data.y,
58938
59112
  width: result.data.width,
58939
- height: result.data.height
59113
+ height: result.data.height,
59114
+ referenceImageIndex: result.data.referenceImageIndex,
59115
+ prompt: parsedArgs.prompt ?? ""
58940
59116
  };
58941
59117
  if (onAfterImageGen) {
58942
59118
  onAfterImageGen();
@@ -58998,25 +59174,45 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
58998
59174
  ]);
58999
59175
  const assetSearchResult = result;
59000
59176
  if (searchToolNames.has(name) && result.success && assetSearchResult.previewDataUrls?.length) {
59001
- const previews = assetSearchResult.previewDataUrls;
59002
- console.log(
59003
- `[agent:mid-loop] ${name} injecting ${previews.length} preview image(s) as base64 vision content: ${previews.map(
59004
- (p) => `"${p.name}" (${Math.round(p.dataUrl.length / 1024)}KB)`
59005
- ).join(", ")}`
59006
- );
59007
- messages.push({
59008
- role: "user",
59009
- content: [
59010
- {
59011
- type: "text",
59012
- text: `Visual previews for the ${previews.length} result(s) from ${name}:`
59013
- },
59014
- ...previews.map((p) => ({
59015
- type: "image_url",
59016
- image_url: { url: p.dataUrl }
59017
- }))
59018
- ]
59019
- });
59177
+ const budgetedPreviews = [];
59178
+ for (const p of assetSearchResult.previewDataUrls) {
59179
+ const size = dataUrlByteSize(p.dataUrl);
59180
+ if (size > PER_BRAND_IMAGE_BYTE_CAP) {
59181
+ console.warn(
59182
+ `[agent:mid-loop] dropping "${p.name}" \u2014 ${Math.round(size / 1024)}KB exceeds the per-image cap`
59183
+ );
59184
+ continue;
59185
+ }
59186
+ if (brandContextBytesUsed + size > BRAND_CONTEXT_IMAGE_BUDGET || brandContextImagesUsed >= MAX_TOTAL_IMAGES) {
59187
+ console.warn(
59188
+ `[agent:mid-loop] dropping "${p.name}" \u2014 brand-context image budget exhausted`
59189
+ );
59190
+ continue;
59191
+ }
59192
+ brandContextBytesUsed += size;
59193
+ brandContextImagesUsed += 1;
59194
+ budgetedPreviews.push(p);
59195
+ }
59196
+ if (budgetedPreviews.length > 0) {
59197
+ console.log(
59198
+ `[agent:mid-loop] ${name} injecting ${budgetedPreviews.length} preview image(s) as base64 vision content: ${budgetedPreviews.map(
59199
+ (p) => `"${p.name}" (${Math.round(p.dataUrl.length / 1024)}KB)`
59200
+ ).join(", ")}`
59201
+ );
59202
+ messages.push({
59203
+ role: "user",
59204
+ content: [
59205
+ {
59206
+ type: "text",
59207
+ text: `Visual previews for the ${budgetedPreviews.length} result(s) from ${name}:`
59208
+ },
59209
+ ...budgetedPreviews.map((p) => ({
59210
+ type: "image_url",
59211
+ image_url: { url: p.dataUrl }
59212
+ }))
59213
+ ]
59214
+ });
59215
+ }
59020
59216
  }
59021
59217
  if (name === "generate_image" && result.success && imageGenData) {
59022
59218
  const autoFinalizeId = `auto_finalize_${Date.now()}`;
@@ -59074,6 +59270,7 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
59074
59270
  let feedback;
59075
59271
  try {
59076
59272
  const frameEl = augmentedToolCtx.excalidrawAPI.getSceneElements().find((e) => e.id === imageGenData.frameId);
59273
+ const originalImageDataUrl = imageGenData.referenceImageIndex !== void 0 ? allImageAttachments[imageGenData.referenceImageIndex]?.dataUrl : void 0;
59077
59274
  feedback = await runReviewerAgent({
59078
59275
  screenshotDataUrl: bannerScreenshot,
59079
59276
  htmlSource: "",
@@ -59088,7 +59285,12 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
59088
59285
  width: imageGenData.width,
59089
59286
  height: imageGenData.height
59090
59287
  },
59091
- userBrief: typeof userMessages[0]?.content === "string" ? userMessages[0].content : "",
59288
+ // Full conversation's user turns, not just userMessages[0] —
59289
+ // the actual creative brief is frequently NOT the first
59290
+ // message (e.g. "hi" -> clarifying questions -> the message
59291
+ // that actually describes the ad). See fullUserBrief above.
59292
+ userBrief: fullUserBrief,
59293
+ originalImageDataUrl,
59092
59294
  brandContext,
59093
59295
  chatUrl,
59094
59296
  reviewerModel: reviewerModel ?? chatModel,
@@ -59123,7 +59325,7 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
59123
59325
  "",
59124
59326
  `Overall verdict: ${feedback.summary}`,
59125
59327
  "",
59126
- `Apply ALL fixes by calling generate_image AGAIN on frameId="${imageGenData.frameId}" with a revised prompt that incorporates every correction above (re-state the full ad \u2014 visuals AND all copy \u2014 with every fix folded in). Prioritise critical issues first. Do NOT call generate_html_banner or add separate text/shape layers \u2014 the ad is a single generated image.`
59328
+ `Apply ALL fixes by calling generate_image AGAIN on frameId="${imageGenData.frameId}" with a revised prompt. This is a REVISION of the existing ad, not a new one: start from the exact prompt you used last time and change ONLY what the fixes above require \u2014 keep the same subject, composition, color palette, and any reference image untouched otherwise. Do NOT invent a new color scheme, layout, or brand style. If a fix mentions a specific position (e.g. "top-right", "bottom-left"), use that exact position. Prioritise critical issues first. Do NOT call generate_html_banner or add separate text/shape layers \u2014 the ad is a single generated image.`
59127
59329
  ].join("\n");
59128
59330
  messages.push({
59129
59331
  role: "user",
@@ -59188,7 +59390,16 @@ Describe the final ad to the user and stop.`
59188
59390
  if (lastCreatedFrameId) {
59189
59391
  adGenShimmerStore.stopGenerating(lastCreatedFrameId);
59190
59392
  }
59191
- return { reply: fallbackReply, toolActions };
59393
+ return {
59394
+ reply: fallbackReply,
59395
+ toolActions,
59396
+ frameGeneration: imageGenData ? {
59397
+ frameId: imageGenData.frameId,
59398
+ prompt: imageGenData.prompt,
59399
+ referenceImageIndex: imageGenData.referenceImageIndex,
59400
+ userBrief: fullUserBrief
59401
+ } : void 0
59402
+ };
59192
59403
  } catch (err) {
59193
59404
  if (lastCreatedFrameId) {
59194
59405
  adGenShimmerStore.stopGenerating(lastCreatedFrameId);
@@ -59663,9 +59874,11 @@ var AIChatPanel = React69.forwardRef(
59663
59874
  const [prompt, setPrompt] = useState60("");
59664
59875
  const [isLoading, setIsLoading] = useState60(false);
59665
59876
  const [statusText, setStatusText] = useState60("");
59666
- const [messages, setMessages] = useState60(
59667
- initialMessages ?? []
59668
- );
59877
+ const [messages, setMessages] = useState60(() => {
59878
+ const initial = initialMessages ?? [];
59879
+ resetAndSeedFrameGenerationMemory(initial);
59880
+ return initial;
59881
+ });
59669
59882
  const [sessions, setSessions] = useState60(
59670
59883
  initialSessions ?? []
59671
59884
  );
@@ -59771,6 +59984,7 @@ var AIChatPanel = React69.forwardRef(
59771
59984
  saveCurrentSession();
59772
59985
  const newSessionId = genId();
59773
59986
  setMessages([]);
59987
+ resetAndSeedFrameGenerationMemory([]);
59774
59988
  setCurrentSessionId(newSessionId);
59775
59989
  setIsLoadingSession(false);
59776
59990
  setHistoryOpen(false);
@@ -59788,9 +60002,11 @@ var AIChatPanel = React69.forwardRef(
59788
60002
  setError(null);
59789
60003
  if (session.messages.length > 0) {
59790
60004
  setMessages(session.messages);
60005
+ resetAndSeedFrameGenerationMemory(session.messages);
59791
60006
  setIsLoadingSession(false);
59792
60007
  } else {
59793
60008
  setMessages([]);
60009
+ resetAndSeedFrameGenerationMemory([]);
59794
60010
  setIsLoadingSession(true);
59795
60011
  if (onSessionSwitch) {
59796
60012
  onSessionSwitch(session.id);
@@ -60389,7 +60605,8 @@ var AIChatPanel = React69.forwardRef(
60389
60605
  content: result.reply,
60390
60606
  timestamp: /* @__PURE__ */ new Date(),
60391
60607
  toolActions: result.toolActions.length > 0 ? result.toolActions : void 0,
60392
- reviewFeedback: lastReviewFeedbackRef.current ?? void 0
60608
+ reviewFeedback: lastReviewFeedbackRef.current ?? void 0,
60609
+ generatedFrame: result.frameGeneration
60393
60610
  };
60394
60611
  const updatedMessagesAgent = [...nextMessages, assistantMsgAgent];
60395
60612
  setMessages(updatedMessagesAgent);
@@ -60463,6 +60680,7 @@ var AIChatPanel = React69.forwardRef(
60463
60680
  });
60464
60681
  if (targetSessionId === currentSessionIdRef.current) {
60465
60682
  setMessages(msgs);
60683
+ resetAndSeedFrameGenerationMemory(msgs);
60466
60684
  setIsLoadingSession(false);
60467
60685
  }
60468
60686
  },