@orangecatai/adgen-canvas 0.0.30 → 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-X6KPARLJ.js";
69
+ } from "./chunk-N472V5Q6.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-2WAJTVZS.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-HGVO7LLH.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")
@@ -26064,7 +26064,7 @@ var SIZE_SCALE_MAP = {
26064
26064
  };
26065
26065
  function buildBrandContextMessage(ctx) {
26066
26066
  const lines = [
26067
- "## 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"
26068
26068
  ];
26069
26069
  if (ctx.colors) {
26070
26070
  const { primary, secondary, accent, background } = ctx.colors;
@@ -26139,6 +26139,10 @@ function buildBrandContextMessage(ctx) {
26139
26139
  lines.push(`**Imagery direction**: ${parts.join(", ")}`);
26140
26140
  }
26141
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
+ );
26142
26146
  return lines.join("\n");
26143
26147
  }
26144
26148
  function buildImageGenSafePrompt(prompt, brandContext, dimensions) {
@@ -26168,7 +26172,9 @@ function buildImageGenSafePrompt(prompt, brandContext, dimensions) {
26168
26172
  }
26169
26173
  const brandImageHint = brandImageInstructions.length > 0 ? ` ${brandImageInstructions.join(" ")}` : "";
26170
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.` : "";
26171
- 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}`;
26172
26178
  }
26173
26179
  var clean = (value) => {
26174
26180
  const trimmed = value?.trim();
@@ -56721,6 +56727,38 @@ function execAddText(args, ctx) {
56721
56727
  };
56722
56728
  }
56723
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
+ }
56724
56762
  async function execGenerateImage(args, ctx) {
56725
56763
  const frameId = args.frameId;
56726
56764
  const prompt = args.prompt;
@@ -56737,6 +56775,11 @@ async function execGenerateImage(args, ctx) {
56737
56775
 
56738
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):
56739
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}"` : "";
56740
56783
  const fontNames = [ctx.brandHeadlineFontName, ctx.brandBodyFontName].filter(
56741
56784
  (n, i, arr) => Boolean(n) && arr.indexOf(n) === i
56742
56785
  );
@@ -56771,7 +56814,7 @@ LAYOUT & SPACING RULES \u2014 these are non-negotiable:
56771
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.
56772
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.
56773
56816
 
56774
- Render all text as crisp, correctly-spelled, legible copy. Professional advertising quality \u2014 strong visual hierarchy, brand-consistent, generous whitespace throughout.${originalPromptAnchor}`;
56817
+ Render all text as crisp, correctly-spelled, legible copy. Professional advertising quality \u2014 strong visual hierarchy, brand-consistent, generous whitespace throughout.${originalPromptAnchor}${userBriefAnchor}`;
56775
56818
  if (!frameId || !prompt) {
56776
56819
  return {
56777
56820
  success: false,
@@ -56918,7 +56961,12 @@ Render all text as crisp, correctly-spelled, legible copy. Professional advertis
56918
56961
  if (!priorGeneration) {
56919
56962
  frameGenerationMemory.set(frameId, {
56920
56963
  originalPrompt: prompt,
56921
- referenceImageIndex: effectiveReferenceImageIndex
56964
+ referenceImageIndex: effectiveReferenceImageIndex,
56965
+ userBrief
56966
+ });
56967
+ chatCreatedFrames.push({
56968
+ frameId,
56969
+ summary: prompt.slice(0, 80) + (prompt.length > 80 ? "\u2026" : "")
56922
56970
  });
56923
56971
  } else if (effectiveReferenceImageIndex !== void 0) {
56924
56972
  priorGeneration.referenceImageIndex = effectiveReferenceImageIndex;
@@ -58406,6 +58454,18 @@ function isStreamingResponse(response) {
58406
58454
  }
58407
58455
 
58408
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;
58409
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.
58410
58470
 
58411
58471
  ## Core principle \u2014 ONE image IS the ad
@@ -58421,7 +58481,7 @@ Check: is the user asking to change something in an ad that already exists on th
58421
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.
58422
58482
 
58423
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):
58424
- 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.
58425
58485
  2. **Duplicate the frame to a fresh, non-overlapping position** (you keep the original intact).
58426
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.).
58427
58487
  4. Call \`finalize_ad\` on the new frame.
@@ -58463,11 +58523,13 @@ The \`generate_image\` prompt is the ONLY creative call \u2014 it must produce t
58463
58523
  - Sign-off / brand name: exact words.
58464
58524
  Spell every word exactly as given; do not paraphrase on-image text.
58465
58525
 
58466
- 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.
58527
+
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.
58467
58529
 
58468
- 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.
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.
58469
58531
 
58470
- 4. **VISUAL DIRECTION:** composition and layout (where the subject sits, where copy sits), lighting, mood, and emotional tone from the brief and \`imagery\` / \`visualStyle\`.
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.
58471
58533
 
58472
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").
58473
58535
 
@@ -58643,8 +58705,12 @@ async function runAgentLoop(opts) {
58643
58705
  allImageAttachments.push(img);
58644
58706
  }
58645
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");
58646
58711
  const augmentedToolCtx = {
58647
58712
  ...toolCtx,
58713
+ ...fullUserBrief ? { originalUserBrief: fullUserBrief } : {},
58648
58714
  // Lets long-running tools (video gen) push interim progress lines into the
58649
58715
  // same status indicator the loop itself writes to.
58650
58716
  onStatus: (message) => onUpdate({ type: "status", message }),
@@ -58677,6 +58743,12 @@ async function runAgentLoop(opts) {
58677
58743
  ${elementContext}`
58678
58744
  });
58679
58745
  }
58746
+ const chatFramesContext = getChatCreatedFramesContext();
58747
+ if (chatFramesContext) {
58748
+ messages.push({ role: "system", content: chatFramesContext });
58749
+ }
58750
+ let brandContextBytesUsed = 0;
58751
+ let brandContextImagesUsed = 0;
58680
58752
  {
58681
58753
  const lastUserText = [...userMessages].reverse().find((m) => m.role === "user")?.content ?? "";
58682
58754
  if (lastUserText && (toolCtx.onSearchBrandAssets || toolCtx.onSearchAdCreatives || toolCtx.onListBrandTemplates)) {
@@ -58690,9 +58762,11 @@ ${elementContext}`
58690
58762
  toolCtx.onSearchAdCreatives ? toolCtx.onSearchAdCreatives(lastUserText) : Promise.resolve([]),
58691
58763
  toolCtx.onListBrandTemplates ? toolCtx.onListBrandTemplates(keywords) : Promise.resolve([])
58692
58764
  ]);
58693
- const top2Assets = (assetRes.status === "fulfilled" ? assetRes.value : []).filter((a) => a.previewDataUrl).slice(0, 2);
58694
- const top2Creatives = (creativeRes.status === "fulfilled" ? creativeRes.value : []).filter((c) => c.previewDataUrl).slice(0, 2);
58695
- 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);
58696
58770
  console.log(
58697
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"}`
58698
58772
  );
@@ -58710,8 +58784,27 @@ ${elementContext}`
58710
58784
  dataUrl: t2.thumbnailDataUrl
58711
58785
  }))
58712
58786
  ];
58713
- if (previews.length > 0) {
58714
- 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(
58715
58808
  (p) => console.log(
58716
58809
  `[agent:pre-fetch] injecting "${p.label}" as base64 image \u2014 ${Math.round(p.dataUrl.length / 1024)}KB (${p.dataUrl.slice(
58717
58810
  0,
@@ -58722,7 +58815,7 @@ ${elementContext}`
58722
58815
  messages.push({
58723
58816
  role: "system",
58724
58817
  content: `BRAND REFERENCES (auto-fetched, up to 2 per category):
58725
- ${previews.map((p) => `\u2022 ${p.label}`).join(
58818
+ ${budgetedPreviews.map((p) => `\u2022 ${p.label}`).join(
58726
58819
  "\n"
58727
58820
  )}
58728
58821
 
@@ -58733,9 +58826,9 @@ Visual previews follow in the next message. Use them as context: match their sty
58733
58826
  content: [
58734
58827
  {
58735
58828
  type: "text",
58736
- text: `Brand reference previews (${previews.length} total):`
58829
+ text: `Brand reference previews (${budgetedPreviews.length} total):`
58737
58830
  },
58738
- ...previews.map((p) => ({
58831
+ ...budgetedPreviews.map((p) => ({
58739
58832
  type: "image_url",
58740
58833
  image_url: { url: p.dataUrl }
58741
58834
  }))
@@ -58743,17 +58836,57 @@ Visual previews follow in the next message. Use them as context: match their sty
58743
58836
  });
58744
58837
  messages.push({
58745
58838
  role: "assistant",
58746
- 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.`
58747
58840
  });
58748
58841
  }
58749
58842
  }
58750
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
+ }
58751
58884
  for (let i = 0; i < userMessages.length; i++) {
58752
58885
  const msg = userMessages[i];
58753
58886
  const isLast = i === userMessages.length - 1;
58754
58887
  const lastMsgImages = isLast && fileAttachments ? fileAttachments.filter((a) => a.type === "image" && a.dataUrl) : [];
58755
58888
  const lastMsgPdfs = isLast && fileAttachments ? fileAttachments.filter((a) => a.type === "pdf" && a.dataUrl) : [];
58756
- const historicalImages2 = msg.imageAttachments ?? [];
58889
+ const historicalImages2 = droppedHistoricalMessageIndices.has(i) ? [] : msg.imageAttachments ?? [];
58757
58890
  const hasImages = historicalImages2.length > 0 || lastMsgImages.length > 0;
58758
58891
  const hasFrameScreenshot = isLast && !!frameScreenshot;
58759
58892
  const hasTextFiles = isLast && fileAttachments && fileAttachments.some((a) => a.type === "text" || a.type === "docx");
@@ -58839,7 +58972,16 @@ ${f.textContent}`).join("\n\n")}`;
58839
58972
  if (lastCreatedFrameId) {
58840
58973
  adGenShimmerStore.stopGenerating(lastCreatedFrameId);
58841
58974
  }
58842
- 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
+ };
58843
58985
  }
58844
58986
  messages.push({
58845
58987
  role: "assistant",
@@ -58969,7 +59111,8 @@ ${f.textContent}`).join("\n\n")}`;
58969
59111
  y: result.data.y,
58970
59112
  width: result.data.width,
58971
59113
  height: result.data.height,
58972
- referenceImageIndex: result.data.referenceImageIndex
59114
+ referenceImageIndex: result.data.referenceImageIndex,
59115
+ prompt: parsedArgs.prompt ?? ""
58973
59116
  };
58974
59117
  if (onAfterImageGen) {
58975
59118
  onAfterImageGen();
@@ -59031,25 +59174,45 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
59031
59174
  ]);
59032
59175
  const assetSearchResult = result;
59033
59176
  if (searchToolNames.has(name) && result.success && assetSearchResult.previewDataUrls?.length) {
59034
- const previews = assetSearchResult.previewDataUrls;
59035
- console.log(
59036
- `[agent:mid-loop] ${name} injecting ${previews.length} preview image(s) as base64 vision content: ${previews.map(
59037
- (p) => `"${p.name}" (${Math.round(p.dataUrl.length / 1024)}KB)`
59038
- ).join(", ")}`
59039
- );
59040
- messages.push({
59041
- role: "user",
59042
- content: [
59043
- {
59044
- type: "text",
59045
- text: `Visual previews for the ${previews.length} result(s) from ${name}:`
59046
- },
59047
- ...previews.map((p) => ({
59048
- type: "image_url",
59049
- image_url: { url: p.dataUrl }
59050
- }))
59051
- ]
59052
- });
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
+ }
59053
59216
  }
59054
59217
  if (name === "generate_image" && result.success && imageGenData) {
59055
59218
  const autoFinalizeId = `auto_finalize_${Date.now()}`;
@@ -59122,7 +59285,11 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
59122
59285
  width: imageGenData.width,
59123
59286
  height: imageGenData.height
59124
59287
  },
59125
- 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,
59126
59293
  originalImageDataUrl,
59127
59294
  brandContext,
59128
59295
  chatUrl,
@@ -59223,7 +59390,16 @@ Describe the final ad to the user and stop.`
59223
59390
  if (lastCreatedFrameId) {
59224
59391
  adGenShimmerStore.stopGenerating(lastCreatedFrameId);
59225
59392
  }
59226
- 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
+ };
59227
59403
  } catch (err) {
59228
59404
  if (lastCreatedFrameId) {
59229
59405
  adGenShimmerStore.stopGenerating(lastCreatedFrameId);
@@ -59698,9 +59874,11 @@ var AIChatPanel = React69.forwardRef(
59698
59874
  const [prompt, setPrompt] = useState60("");
59699
59875
  const [isLoading, setIsLoading] = useState60(false);
59700
59876
  const [statusText, setStatusText] = useState60("");
59701
- const [messages, setMessages] = useState60(
59702
- initialMessages ?? []
59703
- );
59877
+ const [messages, setMessages] = useState60(() => {
59878
+ const initial = initialMessages ?? [];
59879
+ resetAndSeedFrameGenerationMemory(initial);
59880
+ return initial;
59881
+ });
59704
59882
  const [sessions, setSessions] = useState60(
59705
59883
  initialSessions ?? []
59706
59884
  );
@@ -59806,6 +59984,7 @@ var AIChatPanel = React69.forwardRef(
59806
59984
  saveCurrentSession();
59807
59985
  const newSessionId = genId();
59808
59986
  setMessages([]);
59987
+ resetAndSeedFrameGenerationMemory([]);
59809
59988
  setCurrentSessionId(newSessionId);
59810
59989
  setIsLoadingSession(false);
59811
59990
  setHistoryOpen(false);
@@ -59823,9 +60002,11 @@ var AIChatPanel = React69.forwardRef(
59823
60002
  setError(null);
59824
60003
  if (session.messages.length > 0) {
59825
60004
  setMessages(session.messages);
60005
+ resetAndSeedFrameGenerationMemory(session.messages);
59826
60006
  setIsLoadingSession(false);
59827
60007
  } else {
59828
60008
  setMessages([]);
60009
+ resetAndSeedFrameGenerationMemory([]);
59829
60010
  setIsLoadingSession(true);
59830
60011
  if (onSessionSwitch) {
59831
60012
  onSessionSwitch(session.id);
@@ -60424,7 +60605,8 @@ var AIChatPanel = React69.forwardRef(
60424
60605
  content: result.reply,
60425
60606
  timestamp: /* @__PURE__ */ new Date(),
60426
60607
  toolActions: result.toolActions.length > 0 ? result.toolActions : void 0,
60427
- reviewFeedback: lastReviewFeedbackRef.current ?? void 0
60608
+ reviewFeedback: lastReviewFeedbackRef.current ?? void 0,
60609
+ generatedFrame: result.frameGeneration
60428
60610
  };
60429
60611
  const updatedMessagesAgent = [...nextMessages, assistantMsgAgent];
60430
60612
  setMessages(updatedMessagesAgent);
@@ -60498,6 +60680,7 @@ var AIChatPanel = React69.forwardRef(
60498
60680
  });
60499
60681
  if (targetSessionId === currentSessionIdRef.current) {
60500
60682
  setMessages(msgs);
60683
+ resetAndSeedFrameGenerationMemory(msgs);
60501
60684
  setIsLoadingSession(false);
60502
60685
  }
60503
60686
  },