@orangecatai/adgen-canvas 0.0.32 → 0.0.33

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-OWDDSUUQ.js";
69
+ } from "./chunk-O66HSY73.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-VL47Q2RH.js";
72
+ } from "./chunk-BSCN3CPW.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-5Y3GORNT.js").then(
9799
+ (blob2) => import("./data/image-ADAEIYVZ.js").then(
9800
9800
  ({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
9801
9801
  blob: blob2,
9802
9802
  metadata: serializeAsJSON(elements, appState, files, "local")
@@ -24070,6 +24070,176 @@ async function htmlToExcalidrawElements(html, frameX, frameY, frameWidth, frameH
24070
24070
  }
24071
24071
  }
24072
24072
 
24073
+ // components/ai-chat/imagePayload.ts
24074
+ var MAX_EDGE_PX = 1280;
24075
+ var JPEG_QUALITY = 0.78;
24076
+ var PASSTHROUGH_BYTES = 200 * 1024;
24077
+ function dataUrlWireBytes(dataUrl) {
24078
+ const commaIdx = dataUrl.indexOf(",");
24079
+ return commaIdx >= 0 ? dataUrl.length - commaIdx - 1 : dataUrl.length;
24080
+ }
24081
+ function isRasterImageDataUrl(dataUrl) {
24082
+ return /^data:image\/(png|jpe?g|gif|webp);base64,/i.test(dataUrl);
24083
+ }
24084
+ var DECODE_TIMEOUT_MS = 1e4;
24085
+ function loadImage(src) {
24086
+ return new Promise((resolve, reject) => {
24087
+ const img = new Image();
24088
+ const timer = setTimeout(() => {
24089
+ img.onload = null;
24090
+ img.onerror = null;
24091
+ reject(new Error("image decode timed out"));
24092
+ }, DECODE_TIMEOUT_MS);
24093
+ img.onload = () => {
24094
+ clearTimeout(timer);
24095
+ resolve(img);
24096
+ };
24097
+ img.onerror = () => {
24098
+ clearTimeout(timer);
24099
+ reject(new Error("image decode failed"));
24100
+ };
24101
+ img.src = src;
24102
+ });
24103
+ }
24104
+ async function shrinkImageDataUrl(dataUrl) {
24105
+ if (!isRasterImageDataUrl(dataUrl)) {
24106
+ return dataUrl;
24107
+ }
24108
+ if (dataUrlWireBytes(dataUrl) <= PASSTHROUGH_BYTES) {
24109
+ return dataUrl;
24110
+ }
24111
+ try {
24112
+ const img = await loadImage(dataUrl);
24113
+ const { width, height } = img;
24114
+ if (!width || !height) {
24115
+ return dataUrl;
24116
+ }
24117
+ const scale = Math.min(1, MAX_EDGE_PX / Math.max(width, height));
24118
+ const targetW = Math.max(1, Math.round(width * scale));
24119
+ const targetH = Math.max(1, Math.round(height * scale));
24120
+ const canvas = document.createElement("canvas");
24121
+ canvas.width = targetW;
24122
+ canvas.height = targetH;
24123
+ const ctx = canvas.getContext("2d");
24124
+ if (!ctx) {
24125
+ return dataUrl;
24126
+ }
24127
+ ctx.fillStyle = "#ffffff";
24128
+ ctx.fillRect(0, 0, targetW, targetH);
24129
+ ctx.drawImage(img, 0, 0, targetW, targetH);
24130
+ const shrunk = canvas.toDataURL("image/jpeg", JPEG_QUALITY);
24131
+ return dataUrlWireBytes(shrunk) < dataUrlWireBytes(dataUrl) ? shrunk : dataUrl;
24132
+ } catch {
24133
+ return dataUrl;
24134
+ }
24135
+ }
24136
+ var MAX_REQUEST_WIRE_BYTES = 3.5 * 1024 * 1024;
24137
+ function isImagePart(part) {
24138
+ return !!part && typeof part === "object" && part.type === "image_url" && typeof part.image_url?.url === "string";
24139
+ }
24140
+ function collectImageParts(messages) {
24141
+ const out = [];
24142
+ messages.forEach((m, msgIndex) => {
24143
+ if (!Array.isArray(m.content))
24144
+ return;
24145
+ for (const part of m.content) {
24146
+ if (isImagePart(part))
24147
+ out.push({ msgIndex, part });
24148
+ }
24149
+ });
24150
+ return out;
24151
+ }
24152
+ async function normalizeRequestImages(messages) {
24153
+ const loose = messages;
24154
+ const parts = collectImageParts(loose);
24155
+ if (parts.length === 0)
24156
+ return messages;
24157
+ const shrunk = await Promise.all(
24158
+ parts.map(({ part }) => shrinkImageDataUrl(part.image_url.url))
24159
+ );
24160
+ parts.forEach(({ part }, i) => {
24161
+ part.image_url.url = shrunk[i];
24162
+ });
24163
+ const bodySize = () => JSON.stringify(messages).length;
24164
+ if (bodySize() <= MAX_REQUEST_WIRE_BYTES)
24165
+ return messages;
24166
+ for (const edge of [960, 720, 512]) {
24167
+ const ordered = [...parts].sort(
24168
+ (a, b) => dataUrlWireBytes(b.part.image_url.url) - dataUrlWireBytes(a.part.image_url.url)
24169
+ );
24170
+ for (const { part } of ordered) {
24171
+ part.image_url.url = await reencodeAtEdge(part.image_url.url, edge);
24172
+ if (bodySize() <= MAX_REQUEST_WIRE_BYTES)
24173
+ return messages;
24174
+ }
24175
+ }
24176
+ const lastMsgIndex = loose.length - 1;
24177
+ for (const { msgIndex, part } of parts) {
24178
+ if (msgIndex === lastMsgIndex)
24179
+ continue;
24180
+ const owner = loose[msgIndex];
24181
+ if (!Array.isArray(owner.content))
24182
+ continue;
24183
+ owner.content = owner.content.filter((p) => p !== part);
24184
+ console.warn(
24185
+ "[imagePayload] dropped an image from an earlier turn to fit the request cap"
24186
+ );
24187
+ if (bodySize() <= MAX_REQUEST_WIRE_BYTES)
24188
+ return messages;
24189
+ }
24190
+ return messages;
24191
+ }
24192
+ async function describeChatError(response) {
24193
+ const raw = await response.text().catch(() => "");
24194
+ try {
24195
+ const parsed = JSON.parse(raw);
24196
+ if (parsed?.error?.message)
24197
+ return parsed.error.message;
24198
+ if (typeof parsed?.error === "string")
24199
+ return parsed.error;
24200
+ } catch {
24201
+ }
24202
+ if (response.status === 413) {
24203
+ return "This request was too large to send. Try attaching fewer images, or start a new chat to clear older ones.";
24204
+ }
24205
+ if (response.status === 429) {
24206
+ return "Rate limited \u2014 please wait a moment and try again.";
24207
+ }
24208
+ if (response.status === 401 || response.status === 403) {
24209
+ return "This request was not authorised. Please refresh and try again.";
24210
+ }
24211
+ if (response.status >= 500) {
24212
+ return `The AI service is temporarily unavailable (${response.status}). Please try again.`;
24213
+ }
24214
+ const detail = raw.trim().slice(0, 200);
24215
+ return detail ? `Request failed (${response.status}): ${detail}` : `Request failed (${response.status}).`;
24216
+ }
24217
+ async function reencodeAtEdge(dataUrl, edge) {
24218
+ if (!isRasterImageDataUrl(dataUrl))
24219
+ return dataUrl;
24220
+ try {
24221
+ const img = await loadImage(dataUrl);
24222
+ if (!img.width || !img.height)
24223
+ return dataUrl;
24224
+ const scale = Math.min(1, edge / Math.max(img.width, img.height));
24225
+ const w = Math.max(1, Math.round(img.width * scale));
24226
+ const h = Math.max(1, Math.round(img.height * scale));
24227
+ const canvas = document.createElement("canvas");
24228
+ canvas.width = w;
24229
+ canvas.height = h;
24230
+ const ctx = canvas.getContext("2d");
24231
+ if (!ctx)
24232
+ return dataUrl;
24233
+ ctx.fillStyle = "#ffffff";
24234
+ ctx.fillRect(0, 0, w, h);
24235
+ ctx.drawImage(img, 0, 0, w, h);
24236
+ const out = canvas.toDataURL("image/jpeg", 0.7);
24237
+ return dataUrlWireBytes(out) < dataUrlWireBytes(dataUrl) ? out : dataUrl;
24238
+ } catch {
24239
+ return dataUrl;
24240
+ }
24241
+ }
24242
+
24073
24243
  // components/ai-chat/reviewerAgent.ts
24074
24244
  var REVIEWER_SYSTEM_PROMPT = `You are the Brand Governance Agent \u2014 a Senior Ad Creative Director, Brand Compliance Officer, and Legal QA Auditor combined. You receive a rendered screenshot of a finished advertisement that was generated as a SINGLE AI image \u2014 the visuals AND all copy (headline, subheadline, CTA, body text, sign-off) are part of the image itself.
24075
24245
 
@@ -24350,10 +24520,10 @@ async function runReviewerAgent(opts) {
24350
24520
  });
24351
24521
  const payload = {
24352
24522
  model: reviewerModel,
24353
- messages: [
24523
+ messages: await normalizeRequestImages([
24354
24524
  { role: "system", content: REVIEWER_SYSTEM_PROMPT },
24355
24525
  { role: "user", content: userContent }
24356
- ],
24526
+ ]),
24357
24527
  response_format: { type: "json_object" },
24358
24528
  stream: false,
24359
24529
  temperature: 0.2
@@ -59321,7 +59491,7 @@ function isStreamingResponse(response) {
59321
59491
  }
59322
59492
 
59323
59493
  // components/ai-chat/agentLoop.ts
59324
- function isRasterImageDataUrl(dataUrl) {
59494
+ function isRasterImageDataUrl2(dataUrl) {
59325
59495
  return /^data:image\/(png|jpe?g|gif|webp);base64,/i.test(dataUrl);
59326
59496
  }
59327
59497
  function dataUrlByteSize(dataUrl) {
@@ -59329,9 +59499,6 @@ function dataUrlByteSize(dataUrl) {
59329
59499
  const b64 = commaIdx >= 0 ? dataUrl.slice(commaIdx + 1) : dataUrl;
59330
59500
  return Math.floor(b64.length * 3 / 4);
59331
59501
  }
59332
- var MAX_REQUEST_IMAGE_BYTES = 8 * 1024 * 1024;
59333
- var BRAND_CONTEXT_IMAGE_BUDGET = MAX_REQUEST_IMAGE_BYTES / 2;
59334
- var PER_BRAND_IMAGE_BYTE_CAP = 600 * 1024;
59335
59502
  var MAX_TOTAL_IMAGES = 18;
59336
59503
  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.
59337
59504
 
@@ -59432,14 +59599,12 @@ var MAX_ITERATIONS = 15;
59432
59599
  async function callOpenRouter(messages, chatUrl, signal, webSearchEnabled, chatModel, onContentChunk, tools = CANVAS_TOOLS) {
59433
59600
  const baseModel = chatModel || "minimax/minimax-m3";
59434
59601
  const model = webSearchEnabled ? `${baseModel}:online` : baseModel;
59435
- const body = {
59436
- model,
59437
- stream: true,
59438
- messages: messages.map((m) => {
59602
+ const wireMessages = await normalizeRequestImages(
59603
+ messages.map((m) => {
59439
59604
  if (m.role === "assistant" && m.tool_calls) {
59440
59605
  return {
59441
59606
  role: m.role,
59442
- content: m.content,
59607
+ content: structuredClone(m.content),
59443
59608
  tool_calls: m.tool_calls
59444
59609
  };
59445
59610
  }
@@ -59450,8 +59615,13 @@ async function callOpenRouter(messages, chatUrl, signal, webSearchEnabled, chatM
59450
59615
  content: m.content
59451
59616
  };
59452
59617
  }
59453
- return { role: m.role, content: m.content };
59454
- }),
59618
+ return { role: m.role, content: structuredClone(m.content) };
59619
+ })
59620
+ );
59621
+ const body = {
59622
+ model,
59623
+ stream: true,
59624
+ messages: wireMessages,
59455
59625
  tools,
59456
59626
  tool_choice: "auto"
59457
59627
  };
@@ -59477,15 +59647,7 @@ async function callOpenRouter(messages, chatUrl, signal, webSearchEnabled, chatM
59477
59647
  body: JSON.stringify(body)
59478
59648
  });
59479
59649
  if (!response.ok) {
59480
- let message = `OpenRouter error ${response.status}`;
59481
- try {
59482
- const err = await response.json();
59483
- if (err?.error?.message) {
59484
- message = err.error.message;
59485
- }
59486
- } catch {
59487
- }
59488
- throw new Error(message);
59650
+ throw new Error(await describeChatError(response));
59489
59651
  }
59490
59652
  if (isStreamingResponse(response)) {
59491
59653
  let fullContent = "";
@@ -59621,13 +59783,13 @@ ${elementContext}`
59621
59783
  toolCtx.onListBrandTemplates ? toolCtx.onListBrandTemplates(keywords) : Promise.resolve([])
59622
59784
  ]);
59623
59785
  const top2Assets = (assetRes.status === "fulfilled" ? assetRes.value : []).filter(
59624
- (a) => a.previewDataUrl && isRasterImageDataUrl(a.previewDataUrl)
59786
+ (a) => a.previewDataUrl && isRasterImageDataUrl2(a.previewDataUrl)
59625
59787
  ).slice(0, 2);
59626
59788
  const top2Creatives = (creativeRes.status === "fulfilled" ? creativeRes.value : []).filter(
59627
- (c) => c.previewDataUrl && isRasterImageDataUrl(c.previewDataUrl)
59789
+ (c) => c.previewDataUrl && isRasterImageDataUrl2(c.previewDataUrl)
59628
59790
  ).slice(0, 2);
59629
59791
  const top2Templates = (templateRes.status === "fulfilled" ? templateRes.value : []).filter(
59630
- (t2) => t2.thumbnailDataUrl && isRasterImageDataUrl(t2.thumbnailDataUrl)
59792
+ (t2) => t2.thumbnailDataUrl && isRasterImageDataUrl2(t2.thumbnailDataUrl)
59631
59793
  ).slice(0, 2);
59632
59794
  console.log(
59633
59795
  `[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"}`
@@ -59648,24 +59810,13 @@ ${elementContext}`
59648
59810
  ];
59649
59811
  const budgetedPreviews = [];
59650
59812
  for (const p of previews) {
59651
- const size = dataUrlByteSize(p.dataUrl);
59652
- if (size > PER_BRAND_IMAGE_BYTE_CAP) {
59813
+ if (brandContextImagesUsed >= MAX_TOTAL_IMAGES) {
59653
59814
  console.warn(
59654
- `[agent:pre-fetch] dropping "${p.label}" \u2014 ${Math.round(
59655
- size / 1024
59656
- )}KB exceeds the per-image cap (${Math.round(
59657
- PER_BRAND_IMAGE_BYTE_CAP / 1024
59658
- )}KB)`
59815
+ `[agent:pre-fetch] dropping "${p.label}" \u2014 per-request image count cap reached`
59659
59816
  );
59660
59817
  continue;
59661
59818
  }
59662
- if (brandContextBytesUsed + size > BRAND_CONTEXT_IMAGE_BUDGET || brandContextImagesUsed >= MAX_TOTAL_IMAGES) {
59663
- console.warn(
59664
- `[agent:pre-fetch] dropping "${p.label}" \u2014 brand-context image budget exhausted`
59665
- );
59666
- continue;
59667
- }
59668
- brandContextBytesUsed += size;
59819
+ brandContextBytesUsed += dataUrlByteSize(p.dataUrl);
59669
59820
  brandContextImagesUsed += 1;
59670
59821
  budgetedPreviews.push(p);
59671
59822
  }
@@ -59707,46 +59858,20 @@ Visual previews follow in the next message. Use them as context: match their sty
59707
59858
  }
59708
59859
  }
59709
59860
  }
59710
- const frameScreenshotBytes = frameScreenshot ? dataUrlByteSize(frameScreenshot) : 0;
59711
- const currentSendImagesForBudget = fileAttachments?.filter((a) => a.type === "image" && a.dataUrl) ?? [];
59712
- const currentSendImageBytes = currentSendImagesForBudget.reduce(
59713
- (sum2, a) => sum2 + dataUrlByteSize(a.dataUrl),
59714
- 0
59715
- );
59716
- const currentSendImageCount = currentSendImagesForBudget.length + (frameScreenshot ? 1 : 0);
59717
- const remainingBudgetBytes = MAX_REQUEST_IMAGE_BYTES - brandContextBytesUsed;
59718
- const remainingBudgetImages = MAX_TOTAL_IMAGES - brandContextImagesUsed;
59719
- if (currentSendImageBytes + frameScreenshotBytes > remainingBudgetBytes || currentSendImageCount > remainingBudgetImages) {
59720
- const message = "This message is too large to process \u2014 try attaching fewer or smaller images.";
59721
- console.warn(
59722
- `[agent:budget] aborting \u2014 current-send content alone (${Math.round(
59723
- (currentSendImageBytes + frameScreenshotBytes) / 1024
59724
- )}KB, ${currentSendImageCount} image(s)) exceeds the remaining request budget`
59725
- );
59726
- onUpdate({ type: "final", message });
59727
- return { reply: message, toolActions: [] };
59728
- }
59729
- let historicalBudgetBytes = remainingBudgetBytes - currentSendImageBytes - frameScreenshotBytes;
59730
- let historicalBudgetImages = remainingBudgetImages - currentSendImageCount;
59861
+ const currentSendImageCount = (fileAttachments?.filter((a) => a.type === "image" && a.dataUrl).length ?? 0) + (frameScreenshot ? 1 : 0);
59731
59862
  const droppedHistoricalMessageIndices = /* @__PURE__ */ new Set();
59863
+ let historicalImagesRemaining = MAX_TOTAL_IMAGES - brandContextImagesUsed - currentSendImageCount;
59732
59864
  for (let i = userMessages.length - 1; i >= 0; i--) {
59733
59865
  const imgs = userMessages[i].imageAttachments;
59734
59866
  if (!imgs || imgs.length === 0) {
59735
59867
  continue;
59736
59868
  }
59737
- const bytes = imgs.reduce(
59738
- (sum2, img) => sum2 + dataUrlByteSize(img.dataUrl),
59739
- 0
59740
- );
59741
- if (bytes <= historicalBudgetBytes && imgs.length <= historicalBudgetImages) {
59742
- historicalBudgetBytes -= bytes;
59743
- historicalBudgetImages -= imgs.length;
59869
+ if (imgs.length <= historicalImagesRemaining) {
59870
+ historicalImagesRemaining -= imgs.length;
59744
59871
  } else {
59745
59872
  droppedHistoricalMessageIndices.add(i);
59746
59873
  console.warn(
59747
- `[agent:budget] dropping ${imgs.length} historical image(s) from an earlier turn (${Math.round(
59748
- bytes / 1024
59749
- )}KB) \u2014 request budget exceeded`
59874
+ `[agent:budget] dropping ${imgs.length} historical image(s) from an earlier turn \u2014 per-request image count cap (${MAX_TOTAL_IMAGES}) reached`
59750
59875
  );
59751
59876
  }
59752
59877
  }
@@ -60045,22 +60170,13 @@ Use the frameId="${imageGenData.frameId}" for all elements. Keep all elements wi
60045
60170
  if (searchToolNames.has(name) && result.success && assetSearchResult.previewDataUrls?.length) {
60046
60171
  const budgetedPreviews = [];
60047
60172
  for (const p of assetSearchResult.previewDataUrls) {
60048
- const size = dataUrlByteSize(p.dataUrl);
60049
- if (size > PER_BRAND_IMAGE_BYTE_CAP) {
60050
- console.warn(
60051
- `[agent:mid-loop] dropping "${p.name}" \u2014 ${Math.round(
60052
- size / 1024
60053
- )}KB exceeds the per-image cap`
60054
- );
60055
- continue;
60056
- }
60057
- if (brandContextBytesUsed + size > BRAND_CONTEXT_IMAGE_BUDGET || brandContextImagesUsed >= MAX_TOTAL_IMAGES) {
60173
+ if (brandContextImagesUsed >= MAX_TOTAL_IMAGES) {
60058
60174
  console.warn(
60059
- `[agent:mid-loop] dropping "${p.name}" \u2014 brand-context image budget exhausted`
60175
+ `[agent:mid-loop] dropping "${p.name}" \u2014 per-request image count cap reached`
60060
60176
  );
60061
60177
  continue;
60062
60178
  }
60063
- brandContextBytesUsed += size;
60179
+ brandContextBytesUsed += dataUrlByteSize(p.dataUrl);
60064
60180
  brandContextImagesUsed += 1;
60065
60181
  budgetedPreviews.push(p);
60066
60182
  }
@@ -62127,7 +62243,10 @@ ${f.textContent}`).join("\n\n")}`;
62127
62243
  if (hasPdfs) {
62128
62244
  plugins.push({ id: "file-parser", pdf: { engine: "cloudflare-ai" } });
62129
62245
  }
62130
- const body = { model, messages: apiMessages };
62246
+ const body = {
62247
+ model,
62248
+ messages: await normalizeRequestImages(apiMessages)
62249
+ };
62131
62250
  if (plugins.length > 0) {
62132
62251
  body.plugins = plugins;
62133
62252
  }
@@ -62143,15 +62262,7 @@ ${f.textContent}`).join("\n\n")}`;
62143
62262
  body: JSON.stringify(body)
62144
62263
  });
62145
62264
  if (!response.ok) {
62146
- let message = `OpenRouter error ${response.status}`;
62147
- try {
62148
- const err = await response.json();
62149
- if (err?.error?.message) {
62150
- message = err.error.message;
62151
- }
62152
- } catch {
62153
- }
62154
- throw new Error(message);
62265
+ throw new Error(await describeChatError(response));
62155
62266
  }
62156
62267
  if (opts?.onChunk && isStreamingResponse(response)) {
62157
62268
  let fullContent = "";