@orangecatai/adgen-canvas 0.0.39 → 0.1.0

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.
Files changed (95) hide show
  1. package/dist/dev/chunk-2K6ZAAHB.js +1535 -0
  2. package/dist/dev/chunk-2K6ZAAHB.js.map +7 -0
  3. package/dist/dev/{chunk-E7VZGEUJ.js → chunk-2Q5Z4UTF.js} +2 -2
  4. package/dist/dev/{chunk-E7VZGEUJ.js.map → chunk-2Q5Z4UTF.js.map} +1 -1
  5. package/dist/dev/{chunk-LIELFD5T.js → chunk-3WIRZ6S7.js} +6 -1
  6. package/dist/dev/chunk-3WIRZ6S7.js.map +7 -0
  7. package/dist/dev/{chunk-5XP2WJAQ.js → chunk-7XZBXIDZ.js} +1300 -1252
  8. package/dist/dev/chunk-7XZBXIDZ.js.map +7 -0
  9. package/dist/dev/chunk-AKA4GZ3R.js +62 -0
  10. package/dist/dev/chunk-AKA4GZ3R.js.map +7 -0
  11. package/dist/dev/chunk-LWUV2KR2.js +27 -0
  12. package/dist/dev/chunk-LWUV2KR2.js.map +7 -0
  13. package/dist/dev/chunk-T57LHRZV.js +2167 -0
  14. package/dist/dev/chunk-T57LHRZV.js.map +7 -0
  15. package/dist/dev/components/auto-resize/nativeResizeAgent-5XWJJ74W.js +1074 -0
  16. package/dist/dev/components/auto-resize/nativeResizeAgent-5XWJJ74W.js.map +7 -0
  17. package/dist/dev/data/{image-PUPXCX7M.js → image-MTDE3WXB.js} +4 -3
  18. package/dist/dev/index.css +618 -27
  19. package/dist/dev/index.css.map +3 -3
  20. package/dist/dev/index.js +6769 -8217
  21. package/dist/dev/index.js.map +4 -4
  22. package/dist/dev/locales/{en-VCZQBJ2N.js → en-3S2H3QGO.js} +2 -2
  23. package/dist/dev/model-capabilities.js +47 -0
  24. package/dist/dev/model-capabilities.js.map +7 -0
  25. package/dist/dev/subset-shared.chunk.js +1 -1
  26. package/dist/dev/subset-worker.chunk.js +1 -1
  27. package/dist/prod/chunk-6YYSQB5E.js +1 -0
  28. package/dist/prod/{chunk-CLQPYXMS.js → chunk-PIM2DC2L.js} +3 -3
  29. package/dist/prod/chunk-POCDVKZK.js +142 -0
  30. package/dist/prod/chunk-PWBUVPCU.js +1 -0
  31. package/dist/prod/{chunk-WBFDC4WN.js → chunk-R4DNAEVN.js} +1 -1
  32. package/dist/prod/chunk-U2I2OXQ7.js +4 -0
  33. package/dist/prod/chunk-UR7GPHHO.js +1 -0
  34. package/dist/prod/components/auto-resize/nativeResizeAgent-45UNA3LO.js +5 -0
  35. package/dist/prod/data/image-U4RMOYQQ.js +1 -0
  36. package/dist/prod/index.css +1 -1
  37. package/dist/prod/index.js +67 -298
  38. package/dist/prod/locales/{en-ZZURI326.js → en-Z3CKUL4S.js} +1 -1
  39. package/dist/prod/model-capabilities.js +1 -0
  40. package/dist/prod/subset-shared.chunk.js +1 -1
  41. package/dist/prod/subset-worker.chunk.js +1 -1
  42. package/dist/types/common/src/utils.d.ts +4 -1
  43. package/dist/types/element/src/index.d.ts +3 -0
  44. package/dist/types/element/src/newElement.d.ts +2 -1
  45. package/dist/types/element/src/textHtml.d.ts +14 -0
  46. package/dist/types/element/src/textLayout.d.ts +97 -0
  47. package/dist/types/element/src/textStyleRanges.d.ts +62 -0
  48. package/dist/types/element/src/textWrapping.d.ts +27 -0
  49. package/dist/types/element/src/types.d.ts +35 -0
  50. package/dist/types/excalidraw/actions/actionFrame.d.ts +1 -0
  51. package/dist/types/excalidraw/actions/actionLinearEditor.d.ts +1 -0
  52. package/dist/types/excalidraw/actions/actionProperties.d.ts +50 -0
  53. package/dist/types/excalidraw/actions/index.d.ts +1 -1
  54. package/dist/types/excalidraw/actions/types.d.ts +1 -1
  55. package/dist/types/excalidraw/components/AIChatPanel.d.ts +18 -2
  56. package/dist/types/excalidraw/components/App.d.ts +6 -0
  57. package/dist/types/excalidraw/components/FontSizeInput.d.ts +19 -0
  58. package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +946 -45
  59. package/dist/types/excalidraw/components/VideoGeneratorPanel.d.ts +67 -53
  60. package/dist/types/excalidraw/components/ai-chat/ActivityTimeline.d.ts +18 -0
  61. package/dist/types/excalidraw/components/ai-chat/activitySteps.d.ts +106 -0
  62. package/dist/types/excalidraw/components/ai-chat/agentLoop.d.ts +66 -3
  63. package/dist/types/excalidraw/components/ai-chat/canvasTools.d.ts +23 -8
  64. package/dist/types/excalidraw/components/ai-chat/htmlToExcalidraw.d.ts +3 -0
  65. package/dist/types/excalidraw/components/ai-chat/openRouterStream.d.ts +13 -0
  66. package/dist/types/excalidraw/components/auto-resize/ResizeActivityCard.d.ts +7 -0
  67. package/dist/types/excalidraw/components/auto-resize/autoResizeEngine.d.ts +5 -2
  68. package/dist/types/excalidraw/components/auto-resize/autoResizeStore.d.ts +23 -0
  69. package/dist/types/excalidraw/components/auto-resize/imageBounds.d.ts +11 -0
  70. package/dist/types/excalidraw/components/auto-resize/nativeResizeAgent.d.ts +5 -0
  71. package/dist/types/excalidraw/components/auto-resize/resizeFitting.d.ts +7 -0
  72. package/dist/types/excalidraw/components/auto-resize/resizeLayout.d.ts +33 -0
  73. package/dist/types/excalidraw/components/icons.d.ts +6 -0
  74. package/dist/types/excalidraw/components/template-builder/TemplateBuilderTray.d.ts +1 -0
  75. package/dist/types/excalidraw/index.d.ts +9 -0
  76. package/dist/types/excalidraw/modelCapabilities/index.d.ts +17 -0
  77. package/dist/types/excalidraw/modelCapabilities/registry.image.d.ts +2 -0
  78. package/dist/types/excalidraw/modelCapabilities/registry.video.d.ts +2 -0
  79. package/dist/types/excalidraw/modelCapabilities/shared.d.ts +24 -0
  80. package/dist/types/excalidraw/modelCapabilities/types.d.ts +276 -0
  81. package/dist/types/excalidraw/modelCapabilities/validate.d.ts +48 -0
  82. package/dist/types/excalidraw/types.d.ts +4 -4
  83. package/dist/types/excalidraw/utils/imageApi.d.ts +3 -0
  84. package/dist/types/excalidraw/utils/referenceBudget.d.ts +12 -23
  85. package/dist/types/excalidraw/utils/videoApi.d.ts +21 -1
  86. package/dist/types/excalidraw/wysiwyg/richTextEditable.d.ts +36 -0
  87. package/dist/types/excalidraw/wysiwyg/textSelectionStyles.d.ts +32 -0
  88. package/dist/types/excalidraw/wysiwyg/textWysiwyg.d.ts +6 -2
  89. package/package.json +10 -4
  90. package/dist/dev/chunk-5XP2WJAQ.js.map +0 -7
  91. package/dist/dev/chunk-LIELFD5T.js.map +0 -7
  92. package/dist/prod/chunk-T3ZLBHX4.js +0 -4
  93. package/dist/prod/data/image-AG3CTRSJ.js +0 -1
  94. /package/dist/dev/data/{image-PUPXCX7M.js.map → image-MTDE3WXB.js.map} +0 -0
  95. /package/dist/dev/locales/{en-VCZQBJ2N.js.map → en-3S2H3QGO.js.map} +0 -0
@@ -0,0 +1,2167 @@
1
+ import {
2
+ HOST_ASSET_BYTE_RESERVATION,
3
+ MAX_REFERENCE_IMAGES,
4
+ MAX_USER_REFERENCE_IMAGES,
5
+ REFERENCE_WIRE_BUDGET_BYTES
6
+ } from "./chunk-LWUV2KR2.js";
7
+
8
+ // components/auto-resize/frameSource.ts
9
+ import { getFrameChildren } from "@orangecatai/element";
10
+ function getLiveFrameChildren(elements, frame) {
11
+ return getFrameChildren(elements, frame.id).filter((el) => !el.isDeleted);
12
+ }
13
+ function isStructuredFrame(elements, frame) {
14
+ const children = getLiveFrameChildren(elements, frame);
15
+ if (children.length > 1) {
16
+ return true;
17
+ }
18
+ return children.some((el) => {
19
+ if (el.type === "text") {
20
+ return true;
21
+ }
22
+ const customData = el.customData;
23
+ return typeof customData?.decomposeType === "string";
24
+ });
25
+ }
26
+ var AUTO_RESIZE_FLAT_FRAME_REASON = "Make this ad editable first \u2014 Auto Resize needs separate layers to re-lay-out.";
27
+
28
+ // utils/imageCompression.ts
29
+ var MAX_EDGE_PX = 1280;
30
+ var JPEG_QUALITY = 0.78;
31
+ var PASSTHROUGH_BYTES = 200 * 1024;
32
+ var DECODE_TIMEOUT_MS = 1e4;
33
+ function dataUrlWireBytes(dataUrl) {
34
+ const commaIdx = dataUrl.indexOf(",");
35
+ return commaIdx >= 0 ? dataUrl.length - commaIdx - 1 : dataUrl.length;
36
+ }
37
+ function isRasterImageDataUrl(dataUrl) {
38
+ return /^data:image\/(png|jpe?g|gif|webp);base64,/i.test(dataUrl);
39
+ }
40
+ function loadImage(src) {
41
+ return new Promise((resolve, reject) => {
42
+ const img = new Image();
43
+ const timer = setTimeout(() => {
44
+ img.onload = null;
45
+ img.onerror = null;
46
+ reject(new Error("image decode timed out"));
47
+ }, DECODE_TIMEOUT_MS);
48
+ img.onload = () => {
49
+ clearTimeout(timer);
50
+ resolve(img);
51
+ };
52
+ img.onerror = () => {
53
+ clearTimeout(timer);
54
+ reject(new Error("image decode failed"));
55
+ };
56
+ img.src = src;
57
+ });
58
+ }
59
+ function encodeAt(img, w, h, quality) {
60
+ const canvas = document.createElement("canvas");
61
+ canvas.width = w;
62
+ canvas.height = h;
63
+ const ctx = canvas.getContext("2d");
64
+ if (!ctx) {
65
+ return null;
66
+ }
67
+ ctx.fillStyle = "#ffffff";
68
+ ctx.fillRect(0, 0, w, h);
69
+ ctx.drawImage(img, 0, 0, w, h);
70
+ return canvas.toDataURL("image/jpeg", quality);
71
+ }
72
+ async function shrinkImageDataUrl(dataUrl) {
73
+ if (!isRasterImageDataUrl(dataUrl)) {
74
+ return dataUrl;
75
+ }
76
+ if (dataUrlWireBytes(dataUrl) <= PASSTHROUGH_BYTES) {
77
+ return dataUrl;
78
+ }
79
+ try {
80
+ const img = await loadImage(dataUrl);
81
+ const { width, height } = img;
82
+ if (!width || !height) {
83
+ return dataUrl;
84
+ }
85
+ const scale = Math.min(1, MAX_EDGE_PX / Math.max(width, height));
86
+ const shrunk = encodeAt(
87
+ img,
88
+ Math.max(1, Math.round(width * scale)),
89
+ Math.max(1, Math.round(height * scale)),
90
+ JPEG_QUALITY
91
+ );
92
+ if (!shrunk) {
93
+ return dataUrl;
94
+ }
95
+ return dataUrlWireBytes(shrunk) < dataUrlWireBytes(dataUrl) ? shrunk : dataUrl;
96
+ } catch {
97
+ return dataUrl;
98
+ }
99
+ }
100
+ async function reencodeAtEdge(dataUrl, edge, quality = 0.7) {
101
+ if (!isRasterImageDataUrl(dataUrl)) {
102
+ return dataUrl;
103
+ }
104
+ try {
105
+ const img = await loadImage(dataUrl);
106
+ if (!img.width || !img.height) {
107
+ return dataUrl;
108
+ }
109
+ const scale = Math.min(1, edge / Math.max(img.width, img.height));
110
+ const out = encodeAt(
111
+ img,
112
+ Math.max(1, Math.round(img.width * scale)),
113
+ Math.max(1, Math.round(img.height * scale)),
114
+ quality
115
+ );
116
+ if (!out) {
117
+ return dataUrl;
118
+ }
119
+ return dataUrlWireBytes(out) < dataUrlWireBytes(dataUrl) ? out : dataUrl;
120
+ } catch {
121
+ return dataUrl;
122
+ }
123
+ }
124
+ var ESCALATION_EDGES = [960, 720, 512];
125
+
126
+ // utils/referenceBudget.ts
127
+ var REFERENCE_TIERS = [
128
+ "user",
129
+ "source",
130
+ "logo",
131
+ "brandAsset",
132
+ "adReference"
133
+ ];
134
+ var TIER_FLOOR = {
135
+ brandAsset: 1,
136
+ adReference: 1
137
+ };
138
+ var slotBytes = (slot) => slot.bytes ?? (slot.dataUrl ? dataUrlWireBytes(slot.dataUrl) : 0);
139
+ var totalBytes = (slots) => slots.reduce((sum, slot) => sum + slotBytes(slot), 0);
140
+ function orderByPriority(slots) {
141
+ return [...slots].sort(
142
+ (a, b) => REFERENCE_TIERS.indexOf(a.tier) - REFERENCE_TIERS.indexOf(b.tier)
143
+ );
144
+ }
145
+ function shedOrder(slots, protectLogo) {
146
+ const of = (tier) => slots.filter((s) => s.tier === tier);
147
+ const brandAssets = of("brandAsset");
148
+ const adReferences = of("adReference");
149
+ const surplus = (tier, list) => list.slice(TIER_FLOOR[tier] ?? 0).reverse();
150
+ return [
151
+ // Extras first, tail-first within each tier, lowest priority tier first.
152
+ ...surplus("adReference", adReferences),
153
+ ...surplus("brandAsset", brandAssets),
154
+ // Then whole tiers, still lowest priority first.
155
+ ...adReferences.slice(0, TIER_FLOOR.adReference ?? 0),
156
+ ...brandAssets.slice(0, TIER_FLOOR.brandAsset ?? 0),
157
+ // The logo outlives every brand asset, and on generate flows is never shed.
158
+ ...protectLogo ? [] : of("logo")
159
+ ];
160
+ }
161
+ function allocateReferences(slots, options = {}) {
162
+ const maxImages = options.maxImages ?? MAX_REFERENCE_IMAGES;
163
+ const maxBytes = options.maxBytes ?? REFERENCE_WIRE_BUDGET_BYTES;
164
+ const budget = maxBytes - (options.reservedBytes ?? 0);
165
+ const protectLogo = options.protectLogo ?? true;
166
+ const dropped = [];
167
+ let kept = [];
168
+ let userSeen = 0;
169
+ for (const slot of orderByPriority(slots)) {
170
+ if (slot.tier === "user") {
171
+ userSeen += 1;
172
+ if (userSeen > MAX_USER_REFERENCE_IMAGES) {
173
+ dropped.push({ tier: slot.tier, label: slot.label, reason: "count" });
174
+ continue;
175
+ }
176
+ }
177
+ kept.push(slot);
178
+ }
179
+ const shed = (reason, stillOver) => {
180
+ for (const victim of shedOrder(kept, protectLogo)) {
181
+ if (!stillOver()) {
182
+ return;
183
+ }
184
+ kept = kept.filter((s) => s !== victim);
185
+ dropped.push({ tier: victim.tier, label: victim.label, reason });
186
+ }
187
+ };
188
+ shed("count", () => kept.length > maxImages);
189
+ shed("bytes", () => totalBytes(kept) > budget);
190
+ return {
191
+ kept,
192
+ dropped,
193
+ bytes: totalBytes(kept),
194
+ overBudget: kept.length > maxImages || totalBytes(kept) > budget
195
+ };
196
+ }
197
+ async function prepareReferences(slots, options = {}) {
198
+ const maxBytes = options.maxBytes ?? REFERENCE_WIRE_BUDGET_BYTES;
199
+ const budget = maxBytes - (options.reservedBytes ?? 0);
200
+ const compressed = await Promise.all(
201
+ slots.map(
202
+ async (slot) => slot.dataUrl ? {
203
+ ...slot,
204
+ dataUrl: await shrinkImageDataUrl(slot.dataUrl),
205
+ bytes: void 0
206
+ } : slot
207
+ )
208
+ );
209
+ for (const edge of ESCALATION_EDGES) {
210
+ if (totalBytes(compressed) <= budget) {
211
+ break;
212
+ }
213
+ const largestFirst = compressed.map((slot, index) => ({ slot, index })).filter(({ slot }) => Boolean(slot.dataUrl)).sort((a, b) => slotBytes(b.slot) - slotBytes(a.slot));
214
+ for (const { slot, index } of largestFirst) {
215
+ if (totalBytes(compressed) <= budget) {
216
+ break;
217
+ }
218
+ compressed[index] = {
219
+ ...slot,
220
+ dataUrl: await reencodeAtEdge(slot.dataUrl, edge),
221
+ bytes: void 0
222
+ };
223
+ }
224
+ }
225
+ return allocateReferences(compressed, options);
226
+ }
227
+
228
+ // utils/imageApi.ts
229
+ var EXACT_PIXEL_SIZE = /^\d+x\d+$/;
230
+ var isOpenAIImageModel = (modelId) => modelId.startsWith("openai/gpt-image");
231
+ var PIXEL_STEP = 16;
232
+ var MAX_EDGE = 3840;
233
+ var MAX_EDGE_RATIO = 3;
234
+ var MIN_PIXELS = 655360;
235
+ var MAX_PIXELS = 8294400;
236
+ var PRESET_ASPECT_RATIOS = [
237
+ { label: "16:9", value: 16 / 9 },
238
+ { label: "4:3", value: 4 / 3 },
239
+ { label: "1:1", value: 1 },
240
+ { label: "3:4", value: 3 / 4 },
241
+ { label: "9:16", value: 9 / 16 }
242
+ ];
243
+ function nearestPresetAspectRatio(frameW, frameH) {
244
+ const target = frameW / frameH;
245
+ return PRESET_ASPECT_RATIOS.reduce(
246
+ (best, candidate) => Math.abs(candidate.value - target) < Math.abs(best.value - target) ? candidate : best
247
+ ).label;
248
+ }
249
+ function resolveImageOutputSize(modelId, frameW, frameH) {
250
+ const usable = Number.isFinite(frameW) && Number.isFinite(frameH) && frameW > 0 && frameH > 0;
251
+ if (!usable) {
252
+ return { aspectRatio: "1:1", imageSize: null };
253
+ }
254
+ if (!isOpenAIImageModel(modelId)) {
255
+ return {
256
+ aspectRatio: nearestPresetAspectRatio(frameW, frameH),
257
+ imageSize: null
258
+ };
259
+ }
260
+ const target = frameW / frameH;
261
+ const ratio = Math.max(frameW, frameH) / Math.min(frameW, frameH);
262
+ const clamp = ratio > MAX_EDGE_RATIO ? MAX_EDGE_RATIO / ratio : 1;
263
+ let w = frameW > frameH ? frameW * clamp : frameW;
264
+ let h = frameW > frameH ? frameH : frameH * clamp;
265
+ const edgeScale = Math.min(1, MAX_EDGE / Math.max(w, h));
266
+ w *= edgeScale;
267
+ h *= edgeScale;
268
+ const pixels = w * h;
269
+ const pixelScale = pixels < MIN_PIXELS ? Math.sqrt(MIN_PIXELS / pixels) : pixels > MAX_PIXELS ? Math.sqrt(MAX_PIXELS / pixels) : 1;
270
+ w *= pixelScale;
271
+ h *= pixelScale;
272
+ const snap = (v, up) => Math.max(
273
+ PIXEL_STEP,
274
+ (up ? Math.ceil(v / PIXEL_STEP) : Math.floor(v / PIXEL_STEP)) * PIXEL_STEP
275
+ );
276
+ let best = null;
277
+ let bestError = Infinity;
278
+ for (const upW of [false, true]) {
279
+ for (const upH of [false, true]) {
280
+ const cw = snap(w, upW);
281
+ const ch = snap(h, upH);
282
+ const area = cw * ch;
283
+ if (Math.max(cw, ch) > MAX_EDGE || Math.max(cw, ch) / Math.min(cw, ch) > MAX_EDGE_RATIO || area < MIN_PIXELS || area > MAX_PIXELS) {
284
+ continue;
285
+ }
286
+ const error = Math.abs(cw / ch - target);
287
+ if (error < bestError) {
288
+ bestError = error;
289
+ best = { w: cw, h: ch };
290
+ }
291
+ }
292
+ }
293
+ if (!best) {
294
+ return {
295
+ aspectRatio: nearestPresetAspectRatio(frameW, frameH),
296
+ imageSize: null
297
+ };
298
+ }
299
+ return { aspectRatio: null, imageSize: `${best.w}x${best.h}` };
300
+ }
301
+ var DEMOTED_BRAND_HEADER = "BRAND DIRECTION \u2014 OPTIONAL DEFAULTS. Everything in this section is subordinate to the user's instruction and the USER REFERENCE images above. Apply it only where they are silent, and discard any part of it that contradicts them \u2014 including colour values, typography, and layout.";
302
+ function buildReferenceManifest(userReferenceCount, brandLabels, logoIsAttached = false, hasSourceImage = false) {
303
+ if (userReferenceCount === 0 && !hasSourceImage && brandLabels.length === 0) {
304
+ return "";
305
+ }
306
+ const lines = [
307
+ "ATTACHED REFERENCE IMAGES (in the order supplied):"
308
+ ];
309
+ let slot = 0;
310
+ for (let i = 0; i < userReferenceCount; i++) {
311
+ lines.push(
312
+ ` ${++slot}. USER REFERENCE \u2014 supplied by the user for this specific request.`
313
+ );
314
+ }
315
+ if (hasSourceImage) {
316
+ lines.push(
317
+ ` ${++slot}. SOURCE IMAGE \u2014 the existing image being edited. Preserve it exactly except for the change the user asked for.`
318
+ );
319
+ }
320
+ brandLabels.forEach((label) => {
321
+ lines.push(` ${++slot}. Brand kit asset \u2014 ${label}.`);
322
+ });
323
+ const hasUserAuthority = userReferenceCount > 0 || hasSourceImage;
324
+ if (!hasUserAuthority) {
325
+ return lines.join("\n");
326
+ }
327
+ const refWord = userReferenceCount === 0 ? "SOURCE IMAGE" : userReferenceCount === 1 ? "USER REFERENCE image" : "USER REFERENCE images";
328
+ lines.push(
329
+ "",
330
+ `PRIORITY \u2014 ABSOLUTE: the user's written instruction and the ${refWord} above outrank every other input. Follow the user's instruction exactly and treat ${userReferenceCount === 0 ? "it" : "them"} as the governing reference for subject, composition, style, colour, and typography. Where they contradict the brand kit, the brand guidelines, or any brand reference ad \u2014 including instructions given earlier in this prompt \u2014 the user's instruction and references WIN and the brand direction is overruled. Apply brand direction only where the user's instruction and references are silent.`
331
+ );
332
+ if (logoIsAttached) {
333
+ lines.push(
334
+ "",
335
+ `LOGO \u2014 EXACT ARTWORK: the brand logo listed above is the ONLY logo permitted in this image. Wherever a logo appears, it must be that artwork reproduced pixel-for-pixel \u2014 no redrawing, simplification, restyling, or recolouring \u2014 and it replaces any logo, emblem, or wordmark visible in the other reference images. This is not overruled by the precedence rule above; the precedence rule governs subject, composition, and style, not the identity of the logo. If the user explicitly asked for no logo, honour that instead.`
336
+ );
337
+ }
338
+ return lines.join("\n");
339
+ }
340
+ async function callOpenRouterImageAPI(prompt, modelId, aspectRatio, imageSize, textOutput, imageGenUrl, referenceImageDataUrl, signal, brandImages, options) {
341
+ const referenceMode = options?.referenceMode ?? "legacy-positional";
342
+ const userReferences = (Array.isArray(referenceImageDataUrl) ? referenceImageDataUrl : referenceImageDataUrl ? [referenceImageDataUrl] : []).filter((url) => Boolean(url && url.trim()));
343
+ const hostBrandAssetSlots = options?.hostBrandAssetSlots ?? 0;
344
+ const hostAdReferenceSlots = options?.hostAdReferenceSlots ?? 0;
345
+ const placeholder = (tier, label) => ({
346
+ tier,
347
+ dataUrl: "",
348
+ label,
349
+ bytes: HOST_ASSET_BYTE_RESERVATION
350
+ });
351
+ const slots = [
352
+ ...userReferences.map(
353
+ (dataUrl) => ({ tier: "user", dataUrl })
354
+ ),
355
+ ...options?.sourceImageDataUrl ? [
356
+ {
357
+ tier: "source",
358
+ dataUrl: options.sourceImageDataUrl,
359
+ label: "the image being edited"
360
+ }
361
+ ] : [],
362
+ ...brandImages?.logo ? [
363
+ {
364
+ tier: "logo",
365
+ dataUrl: brandImages.logo,
366
+ label: "the exact brand logo graphic"
367
+ }
368
+ ] : [],
369
+ ...brandImages?.fontPreview ? [
370
+ {
371
+ tier: "brandAsset",
372
+ dataUrl: brandImages.fontPreview,
373
+ label: "a brand font specimen (headline typeface)"
374
+ }
375
+ ] : [],
376
+ ...brandImages?.fontPreviewBody ? [
377
+ {
378
+ tier: "brandAsset",
379
+ dataUrl: brandImages.fontPreviewBody,
380
+ label: "a brand font specimen (body typeface)"
381
+ }
382
+ ] : [],
383
+ ...Array.from(
384
+ { length: hostBrandAssetSlots },
385
+ () => placeholder("brandAsset", "a brand product/lifestyle photo")
386
+ ),
387
+ ...Array.from(
388
+ { length: hostAdReferenceSlots },
389
+ () => placeholder("adReference", "an existing ad creative or template")
390
+ )
391
+ ];
392
+ const allocation = await prepareReferences(slots, {
393
+ protectLogo: options?.protectLogo ?? true,
394
+ // The prompt, the brand context block, and the JSON envelope all ride along
395
+ // in the same body, so they are charged against the image budget.
396
+ reservedBytes: prompt.length + (brandImages?.brandContextText?.length ?? 0)
397
+ });
398
+ if (allocation.dropped.length > 0) {
399
+ console.warn(
400
+ `[imageApi] reference budget \u2014 dropped ${allocation.dropped.map((d) => `${d.tier}${d.label ? ` (${d.label})` : ""}:${d.reason}`).join(", ")}; kept ${allocation.kept.length} slot(s), ${Math.round(
401
+ allocation.bytes / 1024
402
+ )} KB`
403
+ );
404
+ }
405
+ const keptOfTier = (tier) => allocation.kept.filter((s) => s.tier === tier);
406
+ const attached = allocation.kept.filter((s) => Boolean(s.dataUrl));
407
+ const keptHostBrandAssets = keptOfTier("brandAsset").filter(
408
+ (s) => !s.dataUrl
409
+ ).length;
410
+ const keptHostAdReferences = keptOfTier("adReference").filter(
411
+ (s) => !s.dataUrl
412
+ ).length;
413
+ const keptUserReferenceCount = keptOfTier("user").length;
414
+ const keptSourceCount = keptOfTier("source").length;
415
+ const orderedReferences = referenceMode === "legacy-positional" ? [
416
+ ...attached.filter((s) => s.tier !== "user" && s.tier !== "source").map((s) => s.dataUrl),
417
+ ...attached.filter((s) => s.tier === "user" || s.tier === "source").map((s) => s.dataUrl)
418
+ ] : attached.map((s) => s.dataUrl);
419
+ const brandLabels = referenceMode === "user-priority" ? attached.filter((s) => s.tier !== "user" && s.tier !== "source").map((s) => s.label).filter((label) => Boolean(label)) : [];
420
+ const logoIsAttached = keptOfTier("logo").length > 0;
421
+ const referenceManifest = referenceMode === "user-priority" ? buildReferenceManifest(
422
+ keptUserReferenceCount,
423
+ brandLabels,
424
+ logoIsAttached,
425
+ keptSourceCount > 0
426
+ ) : "";
427
+ const userSuppliedReferences = referenceMode === "user-priority" && (keptUserReferenceCount > 0 || keptSourceCount > 0);
428
+ const referenceParts = orderedReferences.map((url) => ({
429
+ type: "image_url",
430
+ image_url: { url }
431
+ }));
432
+ const isGrok = modelId.startsWith("x-ai/");
433
+ const isSourceful = modelId.startsWith("sourceful/");
434
+ const isOpenAIImagesAPI = isOpenAIImageModel(modelId);
435
+ const brandContextText = brandImages?.brandContextText;
436
+ const promptWithBrandContext = !brandContextText ? prompt : userSuppliedReferences ? `${prompt}
437
+
438
+ ---
439
+
440
+ ${DEMOTED_BRAND_HEADER}
441
+
442
+ ${brandContextText}` : `${brandContextText}
443
+
444
+ ---
445
+
446
+ ${prompt}`;
447
+ const effectivePrompt = referenceManifest ? `${promptWithBrandContext}
448
+
449
+ ${referenceManifest}` : promptWithBrandContext;
450
+ let body;
451
+ if (isOpenAIImagesAPI) {
452
+ body = {
453
+ model: modelId,
454
+ prompt: effectivePrompt,
455
+ // Two mutually exclusive ways to ask for a size:
456
+ //
457
+ // • `size: "1088x1360"` — exact pixels. Authoritative, and OpenRouter
458
+ // 400s if `resolution` or `aspect_ratio` is sent alongside it.
459
+ // • `resolution: "1K" | "2K" | …` — a tier, which combines with
460
+ // `aspect_ratio`. `resolution` accepts ONLY that enum; passing
461
+ // pixels to it is a ZodError.
462
+ ...imageSize && EXACT_PIXEL_SIZE.test(imageSize) ? { size: imageSize } : {
463
+ ...aspectRatio ? { aspect_ratio: aspectRatio } : {},
464
+ ...imageSize ? { resolution: imageSize } : {}
465
+ },
466
+ ...referenceParts.length > 0 ? { input_references: referenceParts } : {},
467
+ ...options?.quality ? { quality: options.quality } : {},
468
+ ...options?.background ? { background: options.background } : {},
469
+ ...options?.background === "transparent" ? { output_format: options.outputFormat ?? "png" } : {}
470
+ };
471
+ } else if (isGrok) {
472
+ const content2 = referenceParts.length > 0 ? [...referenceParts, { type: "text", text: effectivePrompt }] : effectivePrompt;
473
+ body = {
474
+ model: modelId,
475
+ messages: [{ role: "user", content: content2 }],
476
+ stream: false
477
+ };
478
+ } else {
479
+ const contentParts = [
480
+ ...referenceParts,
481
+ { type: "text", text: effectivePrompt }
482
+ ];
483
+ const imageConfig = {
484
+ // These models only take preset ratios, so callers always supply one.
485
+ aspect_ratio: aspectRatio ?? "1:1"
486
+ };
487
+ if (imageSize) {
488
+ imageConfig.image_size = imageSize;
489
+ }
490
+ if (isSourceful && brandImages?.fontUrl) {
491
+ const fontSampleText = brandImages.fontText || "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz 0123456789";
492
+ imageConfig.font_inputs = [
493
+ {
494
+ font_url: brandImages.fontUrl,
495
+ text: fontSampleText
496
+ }
497
+ ];
498
+ }
499
+ body = {
500
+ model: modelId,
501
+ messages: [{ role: "user", content: contentParts }],
502
+ modalities: textOutput ? ["image", "text"] : ["image"],
503
+ stream: false,
504
+ image_config: imageConfig
505
+ };
506
+ }
507
+ if (referenceMode === "user-priority" && referenceParts.length > 0) {
508
+ body.x_reference_manifest = {
509
+ userReferenceCount: keptUserReferenceCount,
510
+ sourceReferenceCount: keptSourceCount,
511
+ totalReferenceCount: referenceParts.length,
512
+ logoAttached: logoIsAttached,
513
+ // How many of its own assets the host may append, decided by the budget
514
+ // above. The host compresses each one to maxHostAssetBytes so the
515
+ // reservation the SDK made for it stays honest.
516
+ brandAssetSlots: keptHostBrandAssets,
517
+ adReferenceSlots: keptHostAdReferences,
518
+ maxHostAssetBytes: HOST_ASSET_BYTE_RESERVATION
519
+ };
520
+ }
521
+ const response = await fetch(imageGenUrl, {
522
+ method: "POST",
523
+ signal,
524
+ headers: {
525
+ "Content-Type": "application/json"
526
+ },
527
+ body: JSON.stringify(body)
528
+ });
529
+ if (!response.ok) {
530
+ let message = `OpenRouter API error ${response.status}`;
531
+ try {
532
+ const err = await response.json();
533
+ if (err?.error?.message) {
534
+ message = err.error.message;
535
+ }
536
+ } catch {
537
+ }
538
+ throw new Error(message);
539
+ }
540
+ const data = await response.json();
541
+ const imagesApiResult = data?.data?.[0];
542
+ if (imagesApiResult?.b64_json) {
543
+ const mediaType = imagesApiResult.media_type ?? "image/png";
544
+ return `data:${mediaType};base64,${imagesApiResult.b64_json}`;
545
+ }
546
+ const images = data?.choices?.[0]?.message?.images;
547
+ if (images?.[0]?.image_url?.url) {
548
+ return images[0].image_url.url;
549
+ }
550
+ const content = data?.choices?.[0]?.message?.content;
551
+ if (typeof content === "string" && content.length > 0) {
552
+ return content;
553
+ }
554
+ if (Array.isArray(content)) {
555
+ const imgPart = content.find(
556
+ (p) => p?.type === "image_url" && p?.image_url?.url
557
+ );
558
+ if (imgPart?.image_url?.url) {
559
+ return imgPart.image_url.url;
560
+ }
561
+ }
562
+ throw new Error("No image was returned by the API.");
563
+ }
564
+
565
+ // components/auto-resize/autoResizeStore.ts
566
+ var activities = [];
567
+ var stepSequence = 0;
568
+ var loadingFrames = [];
569
+ var listeners = /* @__PURE__ */ new Set();
570
+ function notify() {
571
+ listeners.forEach((fn) => fn());
572
+ }
573
+ var autoResizeStore = {
574
+ startActivity(frame, cancel) {
575
+ activities = [
576
+ ...activities.filter((a) => a.frameId !== frame.frameId),
577
+ { ...frame, status: "running", steps: [], cancel }
578
+ ];
579
+ const completed = activities.filter((a) => a.status !== "running").slice(-20);
580
+ activities = activities.filter(
581
+ (a) => a.status === "running" || completed.includes(a)
582
+ );
583
+ notify();
584
+ },
585
+ startStep(frameId, kind, name, model) {
586
+ const id = ++stepSequence;
587
+ const activity = activities.find((a) => a.frameId === frameId);
588
+ activity?.steps.push({
589
+ id,
590
+ kind,
591
+ name,
592
+ model,
593
+ status: "running",
594
+ startedAt: Date.now()
595
+ });
596
+ notify();
597
+ return id;
598
+ },
599
+ endStep(frameId, id, status, detail) {
600
+ const step = activities.find((a) => a.frameId === frameId)?.steps.find((s) => s.id === id);
601
+ if (step) {
602
+ Object.assign(step, { status, detail, finishedAt: Date.now() });
603
+ }
604
+ notify();
605
+ },
606
+ finishActivity(frameId, status) {
607
+ const activity = activities.find((a) => a.frameId === frameId);
608
+ if (activity) {
609
+ activity.status = status;
610
+ activity.cancel = void 0;
611
+ }
612
+ notify();
613
+ },
614
+ cancelActivity(frameId) {
615
+ activities.find((a) => a.frameId === frameId)?.cancel?.();
616
+ },
617
+ dismissActivity(frameId) {
618
+ activities = activities.filter(
619
+ (a) => a.frameId !== frameId || a.status === "running"
620
+ );
621
+ notify();
622
+ },
623
+ getActivities() {
624
+ return activities;
625
+ },
626
+ resetActivity() {
627
+ activities.forEach((a) => a.cancel?.());
628
+ activities = [];
629
+ notify();
630
+ },
631
+ setLoadingFrames(frames) {
632
+ loadingFrames = [...frames];
633
+ notify();
634
+ },
635
+ markComplete(frameId) {
636
+ loadingFrames = loadingFrames.filter((f) => f.frameId !== frameId);
637
+ notify();
638
+ },
639
+ clear() {
640
+ loadingFrames = [];
641
+ notify();
642
+ },
643
+ getLoadingFrames() {
644
+ return loadingFrames;
645
+ },
646
+ subscribe(fn) {
647
+ listeners.add(fn);
648
+ return () => listeners.delete(fn);
649
+ }
650
+ };
651
+
652
+ // components/auto-resize/backgroundExtend.ts
653
+ var SOLID_TEXTURE_ENERGY = 4;
654
+ var SOLID_MEAN_SPREAD = 6;
655
+ var STATIONARY_MEAN_SPREAD = 20;
656
+ var GRADIENT_MIN_RANGE = 18;
657
+ var MONOTONIC_RATIO = 0.8;
658
+ var SPLIT_MIN_DELTA = 55;
659
+ var SPLIT_EDGE_MARGIN = 0.12;
660
+ var PATCH_GRID = 8;
661
+ function clampInt(v, lo, hi) {
662
+ return Math.max(lo, Math.min(hi, Math.round(v)));
663
+ }
664
+ function colorDistance(a, b) {
665
+ return Math.sqrt((a.r - b.r) ** 2 + (a.g - b.g) ** 2 + (a.b - b.b) ** 2);
666
+ }
667
+ function toHex({
668
+ r,
669
+ g,
670
+ b
671
+ }) {
672
+ const h = (v) => clampInt(v, 0, 255).toString(16).padStart(2, "0");
673
+ return `#${h(r)}${h(g)}${h(b)}`;
674
+ }
675
+ function regionMeans(buf, rect) {
676
+ const x0 = clampInt(rect.x, 0, buf.width);
677
+ const y0 = clampInt(rect.y, 0, buf.height);
678
+ const x1 = clampInt(rect.x + rect.w, 0, buf.width);
679
+ const y1 = clampInt(rect.y + rect.h, 0, buf.height);
680
+ let r = 0;
681
+ let g = 0;
682
+ let b = 0;
683
+ let n = 0;
684
+ const lumas = [];
685
+ for (let y = y0; y < y1; y++) {
686
+ for (let x = x0; x < x1; x++) {
687
+ const i = (y * buf.width + x) * 4;
688
+ const pr = buf.data[i];
689
+ const pg = buf.data[i + 1];
690
+ const pb = buf.data[i + 2];
691
+ r += pr;
692
+ g += pg;
693
+ b += pb;
694
+ lumas.push(0.299 * pr + 0.587 * pg + 0.114 * pb);
695
+ n++;
696
+ }
697
+ }
698
+ if (n === 0) {
699
+ return { mean: { r: 0, g: 0, b: 0 }, lumaStdev: 0 };
700
+ }
701
+ const mean = { r: r / n, g: g / n, b: b / n };
702
+ const lumaMean = lumas.reduce((s, v) => s + v, 0) / lumas.length;
703
+ const variance = lumas.reduce((s, v) => s + (v - lumaMean) ** 2, 0) / lumas.length;
704
+ return { mean, lumaStdev: Math.sqrt(variance) };
705
+ }
706
+ function lineMeans(buf, axis) {
707
+ const outer = axis === "x" ? buf.width : buf.height;
708
+ const inner = axis === "x" ? buf.height : buf.width;
709
+ const out = [];
710
+ for (let o = 0; o < outer; o++) {
711
+ let r = 0;
712
+ let g = 0;
713
+ let b = 0;
714
+ for (let i = 0; i < inner; i++) {
715
+ const x = axis === "x" ? o : i;
716
+ const y = axis === "x" ? i : o;
717
+ const idx = (y * buf.width + x) * 4;
718
+ r += buf.data[idx];
719
+ g += buf.data[idx + 1];
720
+ b += buf.data[idx + 2];
721
+ }
722
+ out.push({ r: r / inner, g: g / inner, b: b / inner });
723
+ }
724
+ return out;
725
+ }
726
+ function detectSplit(buf) {
727
+ let best = null;
728
+ for (const axis of ["x", "y"]) {
729
+ const means = lineMeans(buf, axis);
730
+ const len2 = means.length;
731
+ if (len2 < 8) {
732
+ continue;
733
+ }
734
+ const lo = Math.floor(len2 * SPLIT_EDGE_MARGIN);
735
+ const hi = Math.ceil(len2 * (1 - SPLIT_EDGE_MARGIN));
736
+ for (let i = lo; i < hi - 1; i++) {
737
+ const delta = colorDistance(means[i], means[i + 1]);
738
+ if (delta >= SPLIT_MIN_DELTA && (!best || delta > best.delta)) {
739
+ best = { axis, index: i + 1, delta };
740
+ }
741
+ }
742
+ }
743
+ if (!best) {
744
+ return null;
745
+ }
746
+ const len = best.axis === "x" ? buf.width : buf.height;
747
+ return { axis: best.axis, at: best.index / len };
748
+ }
749
+ function analyzeRegion(buf, rect) {
750
+ const patchW = rect.w / PATCH_GRID;
751
+ const patchH = rect.h / PATCH_GRID;
752
+ const means = [];
753
+ const energies = [];
754
+ for (let gy = 0; gy < PATCH_GRID; gy++) {
755
+ for (let gx = 0; gx < PATCH_GRID; gx++) {
756
+ const { mean, lumaStdev } = regionMeans(buf, {
757
+ x: rect.x + gx * patchW,
758
+ y: rect.y + gy * patchH,
759
+ w: patchW,
760
+ h: patchH
761
+ });
762
+ means.push(mean);
763
+ energies.push(lumaStdev);
764
+ }
765
+ }
766
+ const textureEnergy = energies.reduce((s, v) => s + v, 0) / energies.length;
767
+ const overall = means.reduce(
768
+ (acc, m) => ({ r: acc.r + m.r, g: acc.g + m.g, b: acc.b + m.b }),
769
+ { r: 0, g: 0, b: 0 }
770
+ );
771
+ const meanColor = {
772
+ r: overall.r / means.length,
773
+ g: overall.g / means.length,
774
+ b: overall.b / means.length
775
+ };
776
+ const meanSpread = Math.max(...means.map((m) => colorDistance(m, meanColor)));
777
+ const monotonic = isMonotonic(means);
778
+ let verdict;
779
+ if (textureEnergy < SOLID_TEXTURE_ENERGY && meanSpread < SOLID_MEAN_SPREAD) {
780
+ verdict = "solid";
781
+ } else if (meanSpread < STATIONARY_MEAN_SPREAD) {
782
+ verdict = "texture";
783
+ } else if (monotonic) {
784
+ verdict = "gradient";
785
+ } else {
786
+ verdict = "photo";
787
+ }
788
+ return { rect, textureEnergy, meanSpread, monotonic, meanColor, verdict };
789
+ }
790
+ function isMonotonic(means) {
791
+ const luma = (m) => 0.299 * m.r + 0.587 * m.g + 0.114 * m.b;
792
+ for (const axis of ["x", "y"]) {
793
+ const series = [];
794
+ for (let a = 0; a < PATCH_GRID; a++) {
795
+ let sum = 0;
796
+ for (let b = 0; b < PATCH_GRID; b++) {
797
+ const idx = axis === "x" ? b * PATCH_GRID + a : a * PATCH_GRID + b;
798
+ sum += luma(means[idx]);
799
+ }
800
+ series.push(sum / PATCH_GRID);
801
+ }
802
+ const range = Math.max(...series) - Math.min(...series);
803
+ if (range < GRADIENT_MIN_RANGE) {
804
+ continue;
805
+ }
806
+ let up = 0;
807
+ let down = 0;
808
+ for (let i = 1; i < series.length; i++) {
809
+ if (series[i] > series[i - 1]) {
810
+ up++;
811
+ } else if (series[i] < series[i - 1]) {
812
+ down++;
813
+ }
814
+ }
815
+ const steps = series.length - 1;
816
+ if (Math.max(up, down) / steps >= MONOTONIC_RATIO) {
817
+ return true;
818
+ }
819
+ }
820
+ return false;
821
+ }
822
+ function classifyBackground(buf) {
823
+ const split = detectSplit(buf);
824
+ if (!split) {
825
+ const stats = analyzeRegion(buf, {
826
+ x: 0,
827
+ y: 0,
828
+ w: buf.width,
829
+ h: buf.height
830
+ });
831
+ return {
832
+ kind: stats.verdict,
833
+ solidColor: stats.verdict === "solid" ? toHex(stats.meanColor) : void 0,
834
+ regions: [stats]
835
+ };
836
+ }
837
+ const rects = split.axis === "x" ? [
838
+ { x: 0, y: 0, w: buf.width * split.at, h: buf.height },
839
+ {
840
+ x: buf.width * split.at,
841
+ y: 0,
842
+ w: buf.width * (1 - split.at),
843
+ h: buf.height
844
+ }
845
+ ] : [
846
+ { x: 0, y: 0, w: buf.width, h: buf.height * split.at },
847
+ {
848
+ x: 0,
849
+ y: buf.height * split.at,
850
+ w: buf.width,
851
+ h: buf.height * (1 - split.at)
852
+ }
853
+ ];
854
+ const regions = rects.map((r) => analyzeRegion(buf, r));
855
+ const extendable = regions.every(
856
+ (r) => r.verdict === "solid" || r.verdict === "texture"
857
+ );
858
+ if (!extendable) {
859
+ const whole = analyzeRegion(buf, {
860
+ x: 0,
861
+ y: 0,
862
+ w: buf.width,
863
+ h: buf.height
864
+ });
865
+ return {
866
+ kind: whole.verdict === "solid" ? "texture" : whole.verdict,
867
+ regions
868
+ };
869
+ }
870
+ return { kind: "panels", split, regions };
871
+ }
872
+ function makeCanvas(w, h) {
873
+ const c = document.createElement("canvas");
874
+ c.width = Math.max(1, Math.round(w));
875
+ c.height = Math.max(1, Math.round(h));
876
+ return c;
877
+ }
878
+ async function loadImageElement(dataUrl) {
879
+ return new Promise((resolve, reject) => {
880
+ const img = new Image();
881
+ img.onload = () => resolve(img);
882
+ img.onerror = () => reject(new Error("Failed to load background image"));
883
+ img.src = dataUrl;
884
+ });
885
+ }
886
+ async function imageToPixelBuffer(dataUrl, maxSide = 512) {
887
+ const img = await loadImageElement(dataUrl);
888
+ const scale = Math.min(1, maxSide / Math.max(img.width, img.height));
889
+ const w = Math.max(1, Math.round(img.width * scale));
890
+ const h = Math.max(1, Math.round(img.height * scale));
891
+ const canvas = makeCanvas(w, h);
892
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
893
+ if (!ctx) {
894
+ throw new Error("2D canvas context unavailable");
895
+ }
896
+ ctx.drawImage(img, 0, 0, w, h);
897
+ const imageData = ctx.getImageData(0, 0, w, h);
898
+ return { data: imageData.data, width: w, height: h };
899
+ }
900
+ function mirrorTile(ctx, img, srcRect, dst) {
901
+ const tw = Math.max(1, srcRect.w);
902
+ const th = Math.max(1, srcRect.h);
903
+ const cols = Math.ceil(dst.w / tw);
904
+ const rows = Math.ceil(dst.h / th);
905
+ ctx.save();
906
+ ctx.beginPath();
907
+ ctx.rect(dst.x, dst.y, dst.w, dst.h);
908
+ ctx.clip();
909
+ for (let row = 0; row < rows; row++) {
910
+ for (let col = 0; col < cols; col++) {
911
+ const flipX = col % 2 === 1;
912
+ const flipY = row % 2 === 1;
913
+ ctx.save();
914
+ ctx.translate(
915
+ dst.x + col * tw + (flipX ? tw : 0),
916
+ dst.y + row * th + (flipY ? th : 0)
917
+ );
918
+ ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
919
+ ctx.drawImage(
920
+ img,
921
+ srcRect.x,
922
+ srcRect.y,
923
+ srcRect.w,
924
+ srcRect.h,
925
+ 0,
926
+ 0,
927
+ tw,
928
+ th
929
+ );
930
+ ctx.restore();
931
+ }
932
+ }
933
+ ctx.restore();
934
+ }
935
+ function edgeExtend(ctx, img, tgtW, tgtH) {
936
+ const scale = Math.min(tgtW / img.width, tgtH / img.height);
937
+ const coreW = img.width * scale;
938
+ const coreH = img.height * scale;
939
+ const coreX = (tgtW - coreW) / 2;
940
+ const coreY = (tgtH - coreH) / 2;
941
+ const STRIP = Math.max(1, Math.round(Math.min(img.width, img.height) * 0.04));
942
+ if (coreX > 0) {
943
+ ctx.drawImage(img, 0, 0, STRIP, img.height, 0, coreY, coreX, coreH);
944
+ ctx.drawImage(
945
+ img,
946
+ img.width - STRIP,
947
+ 0,
948
+ STRIP,
949
+ img.height,
950
+ coreX + coreW,
951
+ coreY,
952
+ tgtW - (coreX + coreW),
953
+ coreH
954
+ );
955
+ }
956
+ if (coreY > 0) {
957
+ ctx.drawImage(img, 0, 0, img.width, STRIP, 0, 0, tgtW, coreY);
958
+ ctx.drawImage(
959
+ img,
960
+ 0,
961
+ img.height - STRIP,
962
+ img.width,
963
+ STRIP,
964
+ 0,
965
+ coreY + coreH,
966
+ tgtW,
967
+ tgtH - (coreY + coreH)
968
+ );
969
+ }
970
+ ctx.drawImage(img, coreX, coreY, coreW, coreH);
971
+ }
972
+ function drawCover(ctx, img, tgtW, tgtH) {
973
+ const scale = Math.max(tgtW / img.width, tgtH / img.height);
974
+ const w = img.width * scale;
975
+ const h = img.height * scale;
976
+ ctx.drawImage(img, (tgtW - w) / 2, (tgtH - h) / 2, w, h);
977
+ }
978
+ async function buildTargetBackground(bg, tgtW, tgtH, opts) {
979
+ if (bg.type === "none") {
980
+ return { kind: "none" };
981
+ }
982
+ if (bg.type === "solid") {
983
+ return { kind: "solid", color: bg.color };
984
+ }
985
+ const img = await loadImageElement(bg.dataUrl);
986
+ if (!opts.needsExtension) {
987
+ const canvas2 = makeCanvas(tgtW, tgtH);
988
+ const ctx2 = canvas2.getContext("2d");
989
+ if (!ctx2) {
990
+ throw new Error("2D canvas context unavailable");
991
+ }
992
+ drawCover(ctx2, img, tgtW, tgtH);
993
+ return {
994
+ kind: "image",
995
+ dataUrl: canvas2.toDataURL("image/png"),
996
+ strategy: "scaled"
997
+ };
998
+ }
999
+ opts.onProgress?.("Analysing background\u2026");
1000
+ const buf = await imageToPixelBuffer(bg.dataUrl);
1001
+ const classification = classifyBackground(buf);
1002
+ const sx = img.width / buf.width;
1003
+ const sy = img.height / buf.height;
1004
+ if (classification.kind === "solid" && classification.solidColor) {
1005
+ return { kind: "solid", color: classification.solidColor };
1006
+ }
1007
+ const canvas = makeCanvas(tgtW, tgtH);
1008
+ const ctx = canvas.getContext("2d");
1009
+ if (!ctx) {
1010
+ throw new Error("2D canvas context unavailable");
1011
+ }
1012
+ if (classification.kind === "texture") {
1013
+ opts.onProgress?.("Tiling background texture\u2026");
1014
+ const patch = {
1015
+ x: img.width * 0.25,
1016
+ y: img.height * 0.25,
1017
+ w: img.width * 0.5,
1018
+ h: img.height * 0.5
1019
+ };
1020
+ mirrorTile(ctx, img, patch, { x: 0, y: 0, w: tgtW, h: tgtH });
1021
+ return {
1022
+ kind: "image",
1023
+ dataUrl: canvas.toDataURL("image/png"),
1024
+ strategy: "texture"
1025
+ };
1026
+ }
1027
+ if (classification.kind === "panels" && classification.split) {
1028
+ opts.onProgress?.("Rebuilding background panels\u2026");
1029
+ const { axis, at } = classification.split;
1030
+ for (let i = 0; i < 2; i++) {
1031
+ const region = classification.regions[i];
1032
+ const srcRect = {
1033
+ x: region.rect.x * sx,
1034
+ y: region.rect.y * sy,
1035
+ w: region.rect.w * sx,
1036
+ h: region.rect.h * sy
1037
+ };
1038
+ const dst = axis === "x" ? {
1039
+ x: i === 0 ? 0 : tgtW * at,
1040
+ y: 0,
1041
+ w: i === 0 ? tgtW * at : tgtW * (1 - at),
1042
+ h: tgtH
1043
+ } : {
1044
+ x: 0,
1045
+ y: i === 0 ? 0 : tgtH * at,
1046
+ w: tgtW,
1047
+ h: i === 0 ? tgtH * at : tgtH * (1 - at)
1048
+ };
1049
+ if (region.verdict === "solid") {
1050
+ ctx.fillStyle = toHex(region.meanColor);
1051
+ ctx.fillRect(dst.x, dst.y, dst.w, dst.h);
1052
+ } else {
1053
+ const inset = 0.15;
1054
+ mirrorTile(
1055
+ ctx,
1056
+ img,
1057
+ {
1058
+ x: srcRect.x + srcRect.w * inset,
1059
+ y: srcRect.y + srcRect.h * inset,
1060
+ w: srcRect.w * (1 - inset * 2),
1061
+ h: srcRect.h * (1 - inset * 2)
1062
+ },
1063
+ dst
1064
+ );
1065
+ }
1066
+ }
1067
+ return {
1068
+ kind: "image",
1069
+ dataUrl: canvas.toDataURL("image/png"),
1070
+ strategy: "panels"
1071
+ };
1072
+ }
1073
+ if (classification.kind === "photo") {
1074
+ if (img.width >= tgtW && img.height >= tgtH) {
1075
+ opts.onProgress?.("Re-cropping background\u2026");
1076
+ drawCover(ctx, img, tgtW, tgtH);
1077
+ return {
1078
+ kind: "image",
1079
+ dataUrl: canvas.toDataURL("image/png"),
1080
+ strategy: "scaled"
1081
+ };
1082
+ }
1083
+ if (opts.outpaint) {
1084
+ opts.onProgress?.("Extending background\u2026");
1085
+ try {
1086
+ const out = await opts.outpaint({
1087
+ dataUrl: bg.dataUrl,
1088
+ targetWidth: tgtW,
1089
+ targetHeight: tgtH
1090
+ });
1091
+ if (out) {
1092
+ const outImg = await loadImageElement(out);
1093
+ drawCover(ctx, outImg, tgtW, tgtH);
1094
+ return {
1095
+ kind: "image",
1096
+ dataUrl: canvas.toDataURL("image/png"),
1097
+ strategy: "photo"
1098
+ };
1099
+ }
1100
+ } catch {
1101
+ }
1102
+ }
1103
+ }
1104
+ opts.onProgress?.("Extending background edges\u2026");
1105
+ edgeExtend(ctx, img, tgtW, tgtH);
1106
+ return {
1107
+ kind: "image",
1108
+ dataUrl: canvas.toDataURL("image/png"),
1109
+ strategy: "gradient"
1110
+ };
1111
+ }
1112
+
1113
+ // components/auto-resize/autoResizeEngine.ts
1114
+ import { nanoid } from "nanoid";
1115
+ import {
1116
+ newFrameElement,
1117
+ newElement,
1118
+ newTextElement,
1119
+ newImageElement,
1120
+ getFrameChildren as getFrameChildren2,
1121
+ syncMovedIndices,
1122
+ styledTextToHtml
1123
+ } from "@orangecatai/element";
1124
+ import { FRAME_STYLE, arrayToMap } from "@orangecatai/common";
1125
+
1126
+ // components/ai-chat/htmlToExcalidraw.ts
1127
+ import { normalizeStyleRanges } from "@orangecatai/element";
1128
+ var IFRAME_TIMEOUT_MS = 6e3;
1129
+ var MIN_DIMENSION_PX = 2;
1130
+ function colorToHex(color) {
1131
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
1132
+ return "transparent";
1133
+ }
1134
+ if (color.startsWith("#")) {
1135
+ return color;
1136
+ }
1137
+ const match = color.match(
1138
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/
1139
+ );
1140
+ if (!match) {
1141
+ return color;
1142
+ }
1143
+ const r = parseInt(match[1], 10);
1144
+ const g = parseInt(match[2], 10);
1145
+ const b = parseInt(match[3], 10);
1146
+ const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
1147
+ return hex;
1148
+ }
1149
+ function cssAlphaToOpacity(color) {
1150
+ const match = color.match(/rgba\(\d+,\s*\d+,\s*\d+,\s*([0-9.]+)\)/);
1151
+ if (match) {
1152
+ return Math.round(parseFloat(match[1]) * 100);
1153
+ }
1154
+ return 100;
1155
+ }
1156
+ function mapFontFamily(fontFamily, customFontMap) {
1157
+ const lower = fontFamily.toLowerCase();
1158
+ if (customFontMap) {
1159
+ for (const [name, id] of Object.entries(customFontMap)) {
1160
+ if (lower.includes(name)) {
1161
+ return id;
1162
+ }
1163
+ }
1164
+ }
1165
+ if (lower.includes("sans-serif") || lower.includes("arial") || lower.includes("helvetica") || lower.includes("verdana") || lower.includes("impact") || lower.includes("nunito") || lower.includes("roboto") || lower.includes("inter") || lower.includes("liberation sans") || lower.includes("tahoma") || lower.includes("trebuchet")) {
1166
+ return 2;
1167
+ }
1168
+ if (lower.includes("georgia") || lower.includes("serif") || lower.includes("cursive") || lower.includes("script") || lower.includes("times") || lower.includes("playfair")) {
1169
+ return 1;
1170
+ }
1171
+ if (lower.includes("courier") || lower.includes("mono") || lower.includes("cascadia")) {
1172
+ return 3;
1173
+ }
1174
+ return 2;
1175
+ }
1176
+ function applyTextTransform(text, transform) {
1177
+ switch (transform) {
1178
+ case "uppercase":
1179
+ return text.toUpperCase();
1180
+ case "lowercase":
1181
+ return text.toLowerCase();
1182
+ case "capitalize":
1183
+ return text.replace(/\b\w/g, (c) => c.toUpperCase());
1184
+ default:
1185
+ return text;
1186
+ }
1187
+ }
1188
+ function parseFontSize(fontSize) {
1189
+ const px = parseFloat(fontSize);
1190
+ return isNaN(px) ? 16 : Math.round(px);
1191
+ }
1192
+ function parseBorderRadius(style) {
1193
+ const raw = style.borderTopLeftRadius || "0";
1194
+ const px = parseFloat(raw);
1195
+ return isNaN(px) ? 0 : Math.round(px);
1196
+ }
1197
+ function detectEllipse(style, width, height) {
1198
+ const raw = style.borderTopLeftRadius || "0";
1199
+ if (raw.includes("%")) {
1200
+ const pct = parseFloat(raw);
1201
+ if (!isNaN(pct) && pct >= 50) {
1202
+ return true;
1203
+ }
1204
+ } else {
1205
+ const px = parseFloat(raw);
1206
+ if (!isNaN(px) && px > 0 && px >= width / 2 && px >= height / 2) {
1207
+ return true;
1208
+ }
1209
+ }
1210
+ return false;
1211
+ }
1212
+ function isGradient(bgImage) {
1213
+ return bgImage !== "none" && (bgImage.includes("linear-gradient") || bgImage.includes("radial-gradient"));
1214
+ }
1215
+ function extractGradientColor(bgImage) {
1216
+ const matches = [...bgImage.matchAll(/(#[\da-fA-F]{3,8}|rgba?\([^)]+\))/g)];
1217
+ if (matches.length === 0) {
1218
+ return "#1a1a2e";
1219
+ }
1220
+ if (matches.length === 1) {
1221
+ return colorToHex(matches[0][1]);
1222
+ }
1223
+ if (bgImage.includes("repeating")) {
1224
+ return colorToHex(matches[0][1]);
1225
+ }
1226
+ const first = colorToHex(matches[0][1]).replace("#", "").padEnd(6, "0");
1227
+ const last = colorToHex(matches[matches.length - 1][1]).replace("#", "").padEnd(6, "0");
1228
+ const r1 = parseInt(first.slice(0, 2), 16);
1229
+ const g1 = parseInt(first.slice(2, 4), 16);
1230
+ const b1 = parseInt(first.slice(4, 6), 16);
1231
+ const r2 = parseInt(last.slice(0, 2), 16);
1232
+ const g2 = parseInt(last.slice(2, 4), 16);
1233
+ const b2 = parseInt(last.slice(4, 6), 16);
1234
+ const avg = (v1, v2) => Math.round((v1 + v2) / 2).toString(16).padStart(2, "0");
1235
+ return `#${avg(r1, r2)}${avg(g1, g2)}${avg(b1, b2)}`;
1236
+ }
1237
+ function extractDataUri(bgImage) {
1238
+ const match = bgImage.match(/url\("?(data:[^")]+)"?\)/);
1239
+ return match ? match[1] : null;
1240
+ }
1241
+ function getTextAlign(align) {
1242
+ if (align === "center") {
1243
+ return "center";
1244
+ }
1245
+ if (align === "right" || align === "end") {
1246
+ return "right";
1247
+ }
1248
+ return "left";
1249
+ }
1250
+ var INLINE_TAGS = /* @__PURE__ */ new Set(["SPAN", "EM", "STRONG", "B", "I", "A", "LABEL"]);
1251
+ function getFullText(el) {
1252
+ return getFullTextWithStyles(el)?.text ?? null;
1253
+ }
1254
+ function getFullTextWithStyles(el, getStyle, customFontMap) {
1255
+ let text = "";
1256
+ let hasInlineContent = false;
1257
+ const ranges = [];
1258
+ const parentStyle = getStyle?.(el);
1259
+ const inlineStyle = (child) => {
1260
+ if (!getStyle || !parentStyle) {
1261
+ return {};
1262
+ }
1263
+ const style = getStyle(child);
1264
+ const run = {};
1265
+ const weight = parseInt(style.fontWeight, 10);
1266
+ const parentWeight = parseInt(parentStyle.fontWeight, 10);
1267
+ if (weight >= 600 && !(parentWeight >= 600)) {
1268
+ run.bold = true;
1269
+ }
1270
+ if (style.fontStyle !== "normal" && parentStyle.fontStyle === "normal") {
1271
+ run.italic = true;
1272
+ }
1273
+ const decoration = `${style.textDecorationLine || style.textDecoration}`;
1274
+ if (decoration.includes("underline")) {
1275
+ run.underline = true;
1276
+ }
1277
+ if (decoration.includes("line-through")) {
1278
+ run.strikethrough = true;
1279
+ }
1280
+ if (style.color && style.color !== parentStyle.color) {
1281
+ run.color = colorToHex(style.color);
1282
+ }
1283
+ if (style.fontSize && style.fontSize !== parentStyle.fontSize) {
1284
+ run.fontSize = parseFontSize(style.fontSize);
1285
+ }
1286
+ if (style.fontFamily && style.fontFamily !== parentStyle.fontFamily && customFontMap) {
1287
+ run.fontFamily = mapFontFamily(
1288
+ style.fontFamily,
1289
+ customFontMap
1290
+ );
1291
+ }
1292
+ return run;
1293
+ };
1294
+ for (const node of el.childNodes) {
1295
+ if (node.nodeType === Node.TEXT_NODE) {
1296
+ const t = node.textContent || "";
1297
+ if (t.trim()) {
1298
+ hasInlineContent = true;
1299
+ }
1300
+ text += t;
1301
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
1302
+ const child = node;
1303
+ if (child.tagName === "BR") {
1304
+ text += "\n";
1305
+ hasInlineContent = true;
1306
+ } else if (INLINE_TAGS.has(child.tagName)) {
1307
+ const t = child.textContent || "";
1308
+ if (t.trim()) {
1309
+ hasInlineContent = true;
1310
+ }
1311
+ const start = text.length;
1312
+ text += t;
1313
+ const style = inlineStyle(child);
1314
+ if (Object.keys(style).length) {
1315
+ ranges.push({ start, end: text.length, style });
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ const leading = text.match(/^\s+/)?.[0].length ?? 0;
1321
+ const trimmed = text.replace(/^\s+|\s+$/g, "");
1322
+ if (!hasInlineContent || !trimmed) {
1323
+ return null;
1324
+ }
1325
+ return {
1326
+ text: trimmed,
1327
+ styleRanges: normalizeStyleRanges(
1328
+ ranges.map((range) => ({
1329
+ start: range.start - leading,
1330
+ end: range.end - leading,
1331
+ style: range.style
1332
+ })),
1333
+ trimmed.length
1334
+ )
1335
+ };
1336
+ }
1337
+ async function htmlToExcalidrawElements(html, frameX, frameY, frameWidth, frameHeight, customFontMap) {
1338
+ const iframe = document.createElement("iframe");
1339
+ iframe.style.cssText = `
1340
+ position: fixed;
1341
+ left: -${frameWidth + 200}px;
1342
+ top: 0;
1343
+ width: ${frameWidth}px;
1344
+ height: ${frameHeight}px;
1345
+ border: none;
1346
+ visibility: hidden;
1347
+ `;
1348
+ document.body.appendChild(iframe);
1349
+ try {
1350
+ await new Promise((resolve, reject) => {
1351
+ const timer = setTimeout(() => resolve(), IFRAME_TIMEOUT_MS);
1352
+ iframe.addEventListener(
1353
+ "load",
1354
+ () => {
1355
+ clearTimeout(timer);
1356
+ resolve();
1357
+ },
1358
+ { once: true }
1359
+ );
1360
+ iframe.addEventListener(
1361
+ "error",
1362
+ () => {
1363
+ clearTimeout(timer);
1364
+ reject(new Error("iframe load error"));
1365
+ },
1366
+ { once: true }
1367
+ );
1368
+ const baseStyles = `<style>
1369
+ * { box-sizing: border-box; font-family: Arial, Helvetica, sans-serif; }
1370
+ body { margin: 0; padding: 0; }
1371
+ </style>`;
1372
+ iframe.srcdoc = baseStyles + html;
1373
+ });
1374
+ const doc = iframe.contentDocument;
1375
+ if (!doc) {
1376
+ return [];
1377
+ }
1378
+ const root = doc.querySelector(".canvas");
1379
+ if (!root) {
1380
+ }
1381
+ const rootEl = root ?? doc.body;
1382
+ const rootRect = rootEl.getBoundingClientRect();
1383
+ const allEls = Array.from(rootEl.querySelectorAll("*"));
1384
+ const parsed = [];
1385
+ for (const el of allEls) {
1386
+ const rect = el.getBoundingClientRect();
1387
+ if (rect.width < MIN_DIMENSION_PX || rect.height < MIN_DIMENSION_PX) {
1388
+ continue;
1389
+ }
1390
+ const style = iframe.contentWindow.getComputedStyle(el);
1391
+ if (style.display === "none" || style.visibility === "hidden") {
1392
+ continue;
1393
+ }
1394
+ if (INLINE_TAGS.has(el.tagName) && el.parentElement && getFullText(el.parentElement) !== null) {
1395
+ continue;
1396
+ }
1397
+ const clipPath = style.clipPath || style.webkitClipPath || "none";
1398
+ if (clipPath !== "none" && clipPath.includes("polygon")) {
1399
+ continue;
1400
+ }
1401
+ const x = Math.round(frameX + (rect.left - rootRect.left));
1402
+ const y = Math.round(frameY + (rect.top - rootRect.top));
1403
+ const w = Math.round(rect.width);
1404
+ const h = Math.round(rect.height);
1405
+ const globalOpacity = Math.round(parseFloat(style.opacity ?? "1") * 100);
1406
+ const bgImage = style.backgroundImage || "none";
1407
+ const bgColor = style.backgroundColor || "transparent";
1408
+ const borderColor = style.borderColor || "transparent";
1409
+ const borderWidth = parseFloat(style.borderWidth || "0");
1410
+ const dataUri = extractDataUri(bgImage);
1411
+ if (dataUri) {
1412
+ parsed.push({
1413
+ kind: "image",
1414
+ x,
1415
+ y,
1416
+ width: w,
1417
+ height: h,
1418
+ dataUrl: dataUri
1419
+ });
1420
+ continue;
1421
+ }
1422
+ const directTextWithStyles = getFullTextWithStyles(
1423
+ el,
1424
+ (node) => iframe.contentWindow.getComputedStyle(node),
1425
+ customFontMap
1426
+ );
1427
+ let directText = directTextWithStyles?.text ?? null;
1428
+ if (directText) {
1429
+ const textTransform = style.textTransform || "none";
1430
+ directText = applyTextTransform(directText, textTransform);
1431
+ const textOpacity = globalOpacity < 100 ? globalOpacity : cssAlphaToOpacity(style.color || "rgb(0,0,0)");
1432
+ const align = getTextAlign(style.textAlign || "left");
1433
+ let textX = x;
1434
+ if (align === "center") {
1435
+ textX = x + w / 2;
1436
+ } else if (align === "right") {
1437
+ textX = x + w;
1438
+ }
1439
+ parsed.push({
1440
+ kind: "text",
1441
+ x: textX,
1442
+ y,
1443
+ width: w,
1444
+ height: h,
1445
+ text: directText,
1446
+ styleRanges: directTextWithStyles?.styleRanges,
1447
+ fontSize: parseFontSize(style.fontSize || "16px"),
1448
+ fontFamily: mapFontFamily(style.fontFamily || "", customFontMap),
1449
+ color: colorToHex(style.color || "#000000"),
1450
+ textAlign: align,
1451
+ opacity: Math.min(globalOpacity, textOpacity)
1452
+ });
1453
+ }
1454
+ const hasVisibleBg = bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
1455
+ const hasGradient = isGradient(bgImage);
1456
+ const hasVisibleBorder = borderWidth > 0 && borderColor !== "transparent" && borderColor !== "rgba(0, 0, 0, 0)";
1457
+ if (hasVisibleBg || hasGradient || hasVisibleBorder) {
1458
+ const hexBg = hasGradient ? extractGradientColor(bgImage) : colorToHex(bgColor);
1459
+ const shouldBeEllipse = detectEllipse(style, w, h);
1460
+ const shapeEl = shouldBeEllipse ? {
1461
+ kind: "ellipse",
1462
+ x,
1463
+ y,
1464
+ width: w,
1465
+ height: h,
1466
+ backgroundColor: hasVisibleBg || hasGradient ? hexBg : "transparent",
1467
+ strokeColor: hasVisibleBorder ? colorToHex(borderColor) : "transparent",
1468
+ strokeWidth: hasVisibleBorder ? Math.round(borderWidth) : 0,
1469
+ opacity: globalOpacity
1470
+ } : {
1471
+ kind: "rectangle",
1472
+ x,
1473
+ y,
1474
+ width: w,
1475
+ height: h,
1476
+ backgroundColor: hasVisibleBg || hasGradient ? hexBg : "transparent",
1477
+ strokeColor: hasVisibleBorder ? colorToHex(borderColor) : "transparent",
1478
+ strokeWidth: hasVisibleBorder ? Math.round(borderWidth) : 0,
1479
+ opacity: globalOpacity,
1480
+ borderRadius: parseBorderRadius(style)
1481
+ };
1482
+ if (hasGradient) {
1483
+ shapeEl.backgroundGradient = bgImage;
1484
+ }
1485
+ if (directText) {
1486
+ parsed.splice(parsed.length - 1, 0, shapeEl);
1487
+ } else {
1488
+ parsed.push(shapeEl);
1489
+ }
1490
+ }
1491
+ }
1492
+ return parsed;
1493
+ } finally {
1494
+ document.body.removeChild(iframe);
1495
+ }
1496
+ }
1497
+
1498
+ // components/ai-chat/imagePayload.ts
1499
+ var MAX_REQUEST_WIRE_BYTES = 3.5 * 1024 * 1024;
1500
+ function isImagePart(part) {
1501
+ return !!part && typeof part === "object" && part.type === "image_url" && typeof part.image_url?.url === "string";
1502
+ }
1503
+ function collectImageParts(messages) {
1504
+ const out = [];
1505
+ messages.forEach((m, msgIndex) => {
1506
+ if (!Array.isArray(m.content)) {
1507
+ return;
1508
+ }
1509
+ for (const part of m.content) {
1510
+ if (isImagePart(part)) {
1511
+ out.push({ msgIndex, part });
1512
+ }
1513
+ }
1514
+ });
1515
+ return out;
1516
+ }
1517
+ async function normalizeRequestImages(messages) {
1518
+ const loose = messages;
1519
+ const parts = collectImageParts(loose);
1520
+ if (parts.length === 0) {
1521
+ return messages;
1522
+ }
1523
+ const shrunk = await Promise.all(
1524
+ parts.map(({ part }) => shrinkImageDataUrl(part.image_url.url))
1525
+ );
1526
+ parts.forEach(({ part }, i) => {
1527
+ part.image_url.url = shrunk[i];
1528
+ });
1529
+ const bodySize = () => JSON.stringify(messages).length;
1530
+ if (bodySize() <= MAX_REQUEST_WIRE_BYTES) {
1531
+ return messages;
1532
+ }
1533
+ for (const edge of [960, 720, 512]) {
1534
+ const ordered = [...parts].sort(
1535
+ (a, b) => dataUrlWireBytes(b.part.image_url.url) - dataUrlWireBytes(a.part.image_url.url)
1536
+ );
1537
+ for (const { part } of ordered) {
1538
+ part.image_url.url = await reencodeAtEdge(part.image_url.url, edge);
1539
+ if (bodySize() <= MAX_REQUEST_WIRE_BYTES) {
1540
+ return messages;
1541
+ }
1542
+ }
1543
+ }
1544
+ const lastMsgIndex = loose.length - 1;
1545
+ for (const { msgIndex, part } of parts) {
1546
+ if (msgIndex === lastMsgIndex) {
1547
+ continue;
1548
+ }
1549
+ const owner = loose[msgIndex];
1550
+ if (!Array.isArray(owner.content)) {
1551
+ continue;
1552
+ }
1553
+ owner.content = owner.content.filter((p) => p !== part);
1554
+ console.warn(
1555
+ "[imagePayload] dropped an image from an earlier turn to fit the request cap"
1556
+ );
1557
+ if (bodySize() <= MAX_REQUEST_WIRE_BYTES) {
1558
+ return messages;
1559
+ }
1560
+ }
1561
+ return messages;
1562
+ }
1563
+ async function describeChatError(response) {
1564
+ const raw = await response.text().catch(() => "");
1565
+ try {
1566
+ const parsed = JSON.parse(raw);
1567
+ if (parsed?.error?.message) {
1568
+ return parsed.error.message;
1569
+ }
1570
+ if (typeof parsed?.error === "string") {
1571
+ return parsed.error;
1572
+ }
1573
+ } catch {
1574
+ }
1575
+ if (response.status === 413) {
1576
+ return "This request was too large to send. Try attaching fewer images, or start a new chat to clear older ones.";
1577
+ }
1578
+ if (response.status === 429) {
1579
+ return "Rate limited \u2014 please wait a moment and try again.";
1580
+ }
1581
+ if (response.status === 401 || response.status === 403) {
1582
+ return "This request was not authorised. Please refresh and try again.";
1583
+ }
1584
+ if (response.status >= 500) {
1585
+ return `The AI service is temporarily unavailable (${response.status}). Please try again.`;
1586
+ }
1587
+ const detail = raw.trim().slice(0, 200);
1588
+ return detail ? `Request failed (${response.status}): ${detail}` : `Request failed (${response.status}).`;
1589
+ }
1590
+
1591
+ // components/ai-chat/reviewerAgent.ts
1592
+ 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.
1593
+
1594
+ Your job is to evaluate every output before it moves forward, covering BOTH creative quality AND brand-governance compliance. Return precise, actionable fixes that the art director will fold into a REVISED IMAGE-GENERATION PROMPT (the ad is regenerated, not edited element-by-element).
1595
+
1596
+ ## What you check (in priority order)
1597
+
1598
+ ### A \u2014 Creative Quality
1599
+
1600
+ 1. **Copy correctness** \u2014 Is every word of on-image text spelled correctly and rendered cleanly? AI image models frequently garble, misspell, duplicate, or drop letters. Flag any misspelled, malformed, gibberish, or truncated text and state the exact words that must appear.
1601
+ 2. **Copy legibility** \u2014 Is the text large enough and high-contrast enough to read at a glance? Is it clear of busy areas of the image? Is the visual hierarchy right (headline dominant, CTA distinct)?
1602
+ 3. **CTA prominence** \u2014 Is the call-to-action clearly readable and styled as an actionable element?
1603
+ 4. **Composition & polish** \u2014 Balanced layout, adequate margins, no awkward cropping of subjects or text, professional advertising quality.
1604
+
1605
+ ### B \u2014 Brand Governance (check every dimension below)
1606
+
1607
+ 5. **Brand tone** \u2014 Does the copy register match the brand's prescribed tone-of-voice (e.g. friendly vs. authoritative, casual vs. formal, sentence length, vocabulary level, person/voice)? Flag tone drift even if individual words are correct.
1608
+ 6. **Logo usage** \u2014 If a brand logo is visible in the ad: Is it correctly placed (not distorted, not overlapping busy areas)? Is it at an appropriate size? Has it been cropped, recolored, or stretched in violation of brand standards? If a logo was expected and is absent, flag that too.
1609
+ 7. **Claim accuracy** \u2014 Are product or service claims in the copy factual and supportable? Flag superlatives ("best", "fastest", "#1") that are unqualified, exaggerated feature claims, and any copy that overpromises.
1610
+ 8. **Legal mandatories** \u2014 Are required legal elements present where the brief or brand guidelines demand them (e.g. trademark symbols \u2122/\xAE, copyright notices, required taglines, mandated disclosures)?
1611
+ 9. **Offer disclaimers** \u2014 If the ad contains a promotional offer, price, discount, or time-limited deal, is the appropriate disclaimer or terms-and-conditions reference visible (e.g. "T&Cs apply", "while stocks last", "offer ends DD/MM/YYYY")?
1612
+ 10. **Regulatory sensitivity** \u2014 Does the ad touch regulated categories (finance, health, alcohol, gambling, children, pharmaceuticals, food nutrition claims)? If so, flag any copy that would likely breach sector-specific advertising standards (e.g. health benefit claims without qualification, alcohol ads implying social success, financial promotions without risk warnings).
1613
+ 11. **Prohibited language** \u2014 Is there any language that is explicitly banned by the brand's guidelines or that is generally off-limits in advertising (hate speech, discriminatory language, demeaning stereotypes, shock tactics beyond what the brief permits)?
1614
+ 12. **Competitive claims** \u2014 Does the ad make comparative or disparaging claims about named competitors or competitor products? Unless the brief explicitly permits a named comparison, flag these as requiring legal review.
1615
+ 13. **Data/privacy concerns** \u2014 Does the copy contain, imply, or reference personally identifiable information, user data, or surveillance-style targeting language that could raise GDPR/CCPA concerns or erode consumer trust?
1616
+
1617
+ ## Rules for your critique
1618
+
1619
+ - **Express every fix as a prompt-level instruction**, e.g. "Regenerate with the headline reading exactly 'Fresh Pasta, Made Daily' in a bold sans-serif" or "Add 'T&Cs apply' in small text at the bottom of the ad." Do NOT describe CSS, element coordinates, or canvas operations \u2014 there is no editable layer; the whole ad is regenerated from the prompt.
1620
+ - **Quote exact copy.** When text is wrong or missing, state the precise words that must appear so the next prompt can render them correctly.
1621
+ - If something is fine, do NOT invent issues. Approve confidently.
1622
+ - **User brief overrides brand guidelines.** If the user explicitly requested a color, style, or layout, treat it as intentional. Brand guidelines apply only where the brief is silent.
1623
+ - **NEVER suggest changes to the logo.** The logo is a supplied brand asset. Do NOT flag it as needing to be altered, recoloured, repositioned based on taste, or replaced. If you believe the logo is absent or severely distorted (e.g. unreadable), you may note it under the "logo" element, but your fix must only ask to reproduce it faithfully \u2014 never to redesign, recolour, or remove it.
1624
+ - **NEVER suggest changing the visual content of image elements.** Your mandate covers text copy correctness, legibility, legal mandatories, and brand tone. Do NOT suggest changing what an image depicts, what a product looks like, what the background scene contains, or the composition of any image asset. Only flag content that breaks a named compliance rule.
1625
+ - **Severity guide** \u2014 critical: blocks legal/compliance sign-off or is factually wrong; major: clear brand or quality failure that needs fixing before launch; minor: polish or preference that would improve the creative.
1626
+
1627
+ ## Approval criteria
1628
+
1629
+ Set approved=true with an empty issues array only when ALL of the following are true:
1630
+ - All on-image copy is correctly spelled and matches the intended words
1631
+ - All text is legible (adequate size and contrast)
1632
+ - The CTA is clearly readable and distinct
1633
+ - Brand colors/typographic register are respected (where the brief is silent)
1634
+ - Brand tone-of-voice matches the prescribed register
1635
+ - Logo (if present) follows brand usage standards
1636
+ - No unqualified superlatives or unsupportable claims
1637
+ - All required legal mandatories and offer disclaimers are present
1638
+ - No regulatory red flags for the category
1639
+ - No prohibited language or inappropriate content
1640
+ - No unlicensed competitive claims or comparative disparagement
1641
+ - No data/privacy red flags in the copy
1642
+ - The composition looks intentional and professional
1643
+
1644
+ ## Output
1645
+
1646
+ Return ONLY a JSON object matching this schema exactly. No markdown, no prose, no code fences.
1647
+
1648
+ {
1649
+ "approved": boolean,
1650
+ "issues": [
1651
+ {
1652
+ "element": "headline" | "subheadline" | "cta" | "image" | "layout" | "brand" | "brand_tone" | "logo" | "claim" | "legal" | "disclaimer" | "regulatory" | "prohibited_language" | "competitive_claim" | "privacy",
1653
+ "severity": "critical" | "major" | "minor",
1654
+ "problem": "string \u2014 one sentence describing what is wrong",
1655
+ "fix": "string \u2014 one-line direction",
1656
+ "preciseInstruction": "string \u2014 a prompt-level instruction for the regenerated image, quoting exact copy where relevant"
1657
+ }
1658
+ ],
1659
+ "summary": "string \u2014 1-2 sentences overall verdict"
1660
+ }
1661
+
1662
+ ## Example output (issues found)
1663
+
1664
+ {
1665
+ "approved": false,
1666
+ "issues": [
1667
+ {
1668
+ "element": "headline",
1669
+ "severity": "critical",
1670
+ "problem": "The headline renders as 'Frsh Psata' \u2014 the AI image garbled the text.",
1671
+ "fix": "Regenerate so the headline is spelled correctly.",
1672
+ "preciseInstruction": "Regenerate the ad with the headline reading EXACTLY 'Fresh Pasta' in a bold sans-serif, crisp and legible, no misspellings."
1673
+ },
1674
+ {
1675
+ "element": "claim",
1676
+ "severity": "critical",
1677
+ "problem": "Copy reads 'The World's Best Pasta' \u2014 an unqualified superlative with no supporting evidence.",
1678
+ "fix": "Remove or qualify the superlative.",
1679
+ "preciseInstruction": "Replace 'The World's Best Pasta' with a specific, supportable claim such as 'Award-Winning Fresh Pasta' or 'Loved by 10,000 Customers'."
1680
+ },
1681
+ {
1682
+ "element": "disclaimer",
1683
+ "severity": "major",
1684
+ "problem": "The ad promotes '50% off today only' with no terms-and-conditions reference.",
1685
+ "fix": "Add a disclaimer line for the limited-time offer.",
1686
+ "preciseInstruction": "Add small text at the bottom of the ad reading 'Offer valid today only. T&Cs apply. While stocks last.'"
1687
+ },
1688
+ {
1689
+ "element": "brand_tone",
1690
+ "severity": "minor",
1691
+ "problem": "The copy uses formal third-person language ('One may enjoy...') against a brand voice that is casual and second-person.",
1692
+ "fix": "Rewrite in the brand's casual, direct voice.",
1693
+ "preciseInstruction": "Replace the body copy with second-person, conversational language matching the brand tone, e.g. 'You'll love every bite \u2014 made fresh for you, every day.'"
1694
+ }
1695
+ ],
1696
+ "summary": "Critical compliance and quality issues: misspelled headline, unqualified superlative, missing offer disclaimer, and tone-of-voice drift. Regenerate the image with the corrections above."
1697
+ }
1698
+
1699
+ ## Example output (approved)
1700
+
1701
+ {
1702
+ "approved": true,
1703
+ "issues": [],
1704
+ "summary": "Clean, compliant creative: copy is correctly spelled and legible, all claims are supportable, legal mandatories are present, brand tone and colors are on-point, and the composition looks polished and professional."
1705
+ }`;
1706
+ function buildReviewerBrandSummary(ctx) {
1707
+ const parts = [];
1708
+ if (ctx.colors) {
1709
+ const { primary, secondary, accent, background } = ctx.colors;
1710
+ const colorParts = [
1711
+ primary && `primary: ${primary}`,
1712
+ secondary && `secondary: ${secondary}`,
1713
+ accent && `CTA/accent: ${accent}`,
1714
+ background && `background: ${background}`
1715
+ ].filter(Boolean);
1716
+ if (colorParts.length) {
1717
+ parts.push(`Brand colors \u2014 ${colorParts.join(", ")}`);
1718
+ }
1719
+ }
1720
+ if (ctx.typography) {
1721
+ const { headline, body } = ctx.typography;
1722
+ if (headline?.family) {
1723
+ parts.push(
1724
+ `Headline font: ${headline.family}${headline.weight ? ` ${headline.weight}` : ""}`
1725
+ );
1726
+ }
1727
+ if (body?.family) {
1728
+ parts.push(
1729
+ `Body font: ${body.family}${body.weight ? ` ${body.weight}` : ""}`
1730
+ );
1731
+ }
1732
+ }
1733
+ if (ctx.visualStyle) {
1734
+ parts.push(`Visual style: ${ctx.visualStyle}`);
1735
+ }
1736
+ if (ctx.toneOfVoice) {
1737
+ const { register, vocabulary, ctaStyle, sentenceLength, person } = ctx.toneOfVoice;
1738
+ const toneParts = [
1739
+ register && `register: ${register}`,
1740
+ vocabulary && `vocabulary: ${vocabulary}`,
1741
+ person && `person/voice: ${person}`,
1742
+ sentenceLength && `sentence length: ${sentenceLength}`,
1743
+ ctaStyle && `CTA style: ${ctaStyle}`
1744
+ ].filter(Boolean);
1745
+ if (toneParts.length) {
1746
+ parts.push(`Tone-of-voice \u2014 ${toneParts.join(", ")}`);
1747
+ }
1748
+ }
1749
+ if (ctx.logoDataUrl) {
1750
+ const isBase64 = ctx.logoDataUrl.startsWith("data:");
1751
+ parts.push(
1752
+ `Logo: provided as ${isBase64 ? "base64 data URL" : "external URL"} \u2014 a reference image is attached separately; verify it appears correctly in the ad (correct placement, scale, no distortion or recolouring)`
1753
+ );
1754
+ }
1755
+ return parts.join("\n");
1756
+ }
1757
+ async function runReviewerAgent(opts) {
1758
+ const {
1759
+ screenshotDataUrl,
1760
+ htmlSource,
1761
+ frame,
1762
+ // imagePlacement retained in opts type for backward compatibility but unused
1763
+ // in the image-first flow (the ad is a single full-frame generated image).
1764
+ userBrief,
1765
+ originalImageDataUrl,
1766
+ brandContext,
1767
+ chatUrl,
1768
+ reviewerModel = "google/gemini-2.5-flash",
1769
+ signal,
1770
+ iteration
1771
+ } = opts;
1772
+ const fallback = {
1773
+ approved: true,
1774
+ issues: [],
1775
+ summary: "Reviewer unavailable; skipping.",
1776
+ iteration
1777
+ };
1778
+ const logoDataUrl = brandContext?.logoDataUrl;
1779
+ const logoIsBase64 = typeof logoDataUrl === "string" && logoDataUrl.startsWith("data:");
1780
+ const logoIsExternalUrl = typeof logoDataUrl === "string" && !logoIsBase64;
1781
+ const fontPreviewDataUrl = brandContext?.headlineFontPreviewDataUrl;
1782
+ const fontPreviewIsBase64 = typeof fontPreviewDataUrl === "string" && fontPreviewDataUrl.startsWith("data:");
1783
+ const bodyFontPreviewDataUrl = brandContext?.bodyFontPreviewDataUrl;
1784
+ const bodyFontPreviewIsBase64 = typeof bodyFontPreviewDataUrl === "string" && bodyFontPreviewDataUrl.startsWith("data:");
1785
+ const originalImageIsBase64 = typeof originalImageDataUrl === "string" && originalImageDataUrl.startsWith("data:");
1786
+ const contextLines = [
1787
+ `## Ad Brief (what the user requested)`,
1788
+ userBrief,
1789
+ ``,
1790
+ `## Frame dimensions`,
1791
+ `Width: ${frame.width}px, Height: ${frame.height}px`
1792
+ ];
1793
+ if (originalImageIsBase64) {
1794
+ contextLines.push(
1795
+ ``,
1796
+ `## Original reference image (what the user uploaded)`,
1797
+ `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).`
1798
+ );
1799
+ }
1800
+ if (brandContext) {
1801
+ const brandSummary = buildReviewerBrandSummary(brandContext);
1802
+ if (brandSummary) {
1803
+ contextLines.push(``, `## Brand guidelines`, brandSummary);
1804
+ }
1805
+ }
1806
+ if (logoIsBase64) {
1807
+ contextLines.push(
1808
+ ``,
1809
+ `## Brand logo (reference)`,
1810
+ `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.`
1811
+ );
1812
+ } else if (logoIsExternalUrl) {
1813
+ contextLines.push(
1814
+ ``,
1815
+ `## Brand logo (reference \u2014 URL, may not be visible)`,
1816
+ `A logo URL was provided but could not be embedded as an image. Evaluate logo usage from what is visible in the ad screenshot only.`
1817
+ );
1818
+ }
1819
+ if (fontPreviewIsBase64 || bodyFontPreviewIsBase64) {
1820
+ const multiple = fontPreviewIsBase64 && bodyFontPreviewIsBase64;
1821
+ contextLines.push(
1822
+ ``,
1823
+ `## Brand font preview${multiple ? "s" : ""} (reference)`,
1824
+ `The brand font preview${multiple ? "s are" : " is"} attached below. Use ${multiple ? "them" : "it"} to evaluate whether the on-image text matches the brand typeface \u2014 check letterforms, weight, and typographic style. The brand may use ${multiple ? "either font" : "this font"} for headlines or body copy; do NOT flag a font as an issue simply because of which role it is applied to \u2014 only flag text that does not match the brand fonts at all.`
1825
+ );
1826
+ }
1827
+ if (htmlSource && htmlSource.trim()) {
1828
+ contextLines.push(``, `## Source (legacy)`, "```", htmlSource, "```");
1829
+ }
1830
+ contextLines.push(
1831
+ ``,
1832
+ `## Your task`,
1833
+ `The screenshot of the generated ad is attached above. The whole ad (visuals + all copy) is a single AI-generated image. Evaluate it as the Brand Governance Agent \u2014 check creative quality (copy correctness, legibility, CTA, composition) AND all brand-governance dimensions (brand tone, logo usage, claim accuracy, legal mandatories, offer disclaimers, regulatory sensitivity, prohibited language, competitive claims, data/privacy concerns). Return your JSON critique. Every fix must be phrased as an instruction for regenerating the image.`
1834
+ );
1835
+ const userContent = [
1836
+ {
1837
+ type: "image_url",
1838
+ image_url: { url: screenshotDataUrl }
1839
+ }
1840
+ ];
1841
+ if (originalImageIsBase64 && originalImageDataUrl) {
1842
+ userContent.push({
1843
+ type: "image_url",
1844
+ image_url: { url: originalImageDataUrl }
1845
+ });
1846
+ }
1847
+ if (logoIsBase64 && logoDataUrl) {
1848
+ userContent.push({
1849
+ type: "image_url",
1850
+ image_url: { url: logoDataUrl }
1851
+ });
1852
+ }
1853
+ if (fontPreviewIsBase64 && fontPreviewDataUrl) {
1854
+ userContent.push({
1855
+ type: "image_url",
1856
+ image_url: { url: fontPreviewDataUrl }
1857
+ });
1858
+ }
1859
+ if (bodyFontPreviewIsBase64 && bodyFontPreviewDataUrl) {
1860
+ userContent.push({
1861
+ type: "image_url",
1862
+ image_url: { url: bodyFontPreviewDataUrl }
1863
+ });
1864
+ }
1865
+ userContent.push({
1866
+ type: "text",
1867
+ text: contextLines.join("\n")
1868
+ });
1869
+ const payload = {
1870
+ model: reviewerModel,
1871
+ messages: await normalizeRequestImages([
1872
+ { role: "system", content: REVIEWER_SYSTEM_PROMPT },
1873
+ { role: "user", content: userContent }
1874
+ ]),
1875
+ response_format: { type: "json_object" },
1876
+ stream: false,
1877
+ temperature: 0.2
1878
+ // low temperature for consistent, precise critiques
1879
+ };
1880
+ let response;
1881
+ try {
1882
+ response = await fetch(chatUrl, {
1883
+ method: "POST",
1884
+ headers: {
1885
+ "Content-Type": "application/json",
1886
+ "HTTP-Referer": "https://dughub.app",
1887
+ "X-Title": "ADGEN Reviewer"
1888
+ },
1889
+ body: JSON.stringify(payload),
1890
+ signal
1891
+ });
1892
+ } catch (err) {
1893
+ if (err instanceof Error && err.name === "AbortError") {
1894
+ throw err;
1895
+ }
1896
+ return fallback;
1897
+ }
1898
+ if (!response.ok) {
1899
+ return fallback;
1900
+ }
1901
+ let raw;
1902
+ try {
1903
+ const data = await response.json();
1904
+ raw = data?.choices?.[0]?.message?.content ?? "";
1905
+ } catch {
1906
+ return fallback;
1907
+ }
1908
+ try {
1909
+ const parsed = JSON.parse(raw);
1910
+ const issues = Array.isArray(parsed.issues) ? parsed.issues.filter(
1911
+ (iss) => iss && typeof iss.element === "string" && typeof iss.severity === "string" && typeof iss.problem === "string" && typeof iss.preciseInstruction === "string"
1912
+ ) : [];
1913
+ const approved = typeof parsed.approved === "boolean" ? parsed.approved : issues.length === 0;
1914
+ return {
1915
+ approved,
1916
+ issues,
1917
+ summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary : approved ? "Banner looks good." : `${issues.length} issue(s) found.`,
1918
+ iteration
1919
+ };
1920
+ } catch {
1921
+ return fallback;
1922
+ }
1923
+ }
1924
+
1925
+ // components/auto-resize/autoResizeEngine.ts
1926
+ var GEMINI_RATIOS = {
1927
+ "21:9": 21 / 9,
1928
+ "16:9": 16 / 9,
1929
+ "4:3": 4 / 3,
1930
+ "3:2": 3 / 2,
1931
+ "1:1": 1,
1932
+ "9:16": 9 / 16,
1933
+ "3:4": 3 / 4,
1934
+ "2:3": 2 / 3,
1935
+ "5:4": 5 / 4,
1936
+ "4:5": 4 / 5,
1937
+ "4:1": 4 / 1,
1938
+ "1:4": 1 / 4,
1939
+ "8:1": 8 / 1,
1940
+ "1:8": 1 / 8
1941
+ };
1942
+ var AR_DIFF_TOLERANCE = 0.12;
1943
+ var CROP_LOSS_THRESHOLD = 0.35;
1944
+ function computeCropLoss(sourceAR, targetAR) {
1945
+ if (sourceAR >= targetAR) {
1946
+ const usedWidth = targetAR * 1;
1947
+ return 1 - usedWidth / sourceAR;
1948
+ }
1949
+ const usedHeight = sourceAR / targetAR;
1950
+ return 1 - usedHeight;
1951
+ }
1952
+ function needsBackgroundRegeneration(srcW, srcH, tgtW, tgtH) {
1953
+ const srcAR = srcW / srcH;
1954
+ const tgtAR = tgtW / tgtH;
1955
+ const arDiff = Math.abs(srcAR - tgtAR);
1956
+ if (arDiff <= AR_DIFF_TOLERANCE) {
1957
+ return false;
1958
+ }
1959
+ return computeCropLoss(srcAR, tgtAR) > CROP_LOSS_THRESHOLD;
1960
+ }
1961
+ function nearestGeminiAspectRatio(w, h) {
1962
+ const target = w / h;
1963
+ let best = "1:1";
1964
+ let bestDiff = Infinity;
1965
+ for (const [label, ratio] of Object.entries(GEMINI_RATIOS)) {
1966
+ const diff = Math.abs(target - ratio);
1967
+ if (diff < bestDiff) {
1968
+ bestDiff = diff;
1969
+ best = label;
1970
+ }
1971
+ }
1972
+ return best;
1973
+ }
1974
+ var GEMINI_RESOLUTION_TABLE = {
1975
+ "1:1": {
1976
+ "0.5K": [512, 512],
1977
+ "1K": [1024, 1024],
1978
+ "2K": [2048, 2048],
1979
+ "4K": [4096, 4096]
1980
+ },
1981
+ "1:4": {
1982
+ "0.5K": [256, 1024],
1983
+ "1K": [512, 2048],
1984
+ "2K": [1024, 4096],
1985
+ "4K": [2048, 8192]
1986
+ },
1987
+ "1:8": {
1988
+ "0.5K": [192, 1536],
1989
+ "1K": [384, 3072],
1990
+ "2K": [768, 6144],
1991
+ "4K": [1536, 12288]
1992
+ },
1993
+ "2:3": {
1994
+ "0.5K": [424, 632],
1995
+ "1K": [848, 1264],
1996
+ "2K": [1696, 2528],
1997
+ "4K": [3392, 5056]
1998
+ },
1999
+ "3:2": {
2000
+ "0.5K": [632, 424],
2001
+ "1K": [1264, 848],
2002
+ "2K": [2528, 1696],
2003
+ "4K": [5056, 3392]
2004
+ },
2005
+ "3:4": {
2006
+ "0.5K": [448, 600],
2007
+ "1K": [896, 1200],
2008
+ "2K": [1792, 2400],
2009
+ "4K": [3584, 4800]
2010
+ },
2011
+ "4:1": {
2012
+ "0.5K": [1024, 256],
2013
+ "1K": [2048, 512],
2014
+ "2K": [4096, 1024],
2015
+ "4K": [8192, 2048]
2016
+ },
2017
+ "4:3": {
2018
+ "0.5K": [600, 448],
2019
+ "1K": [1200, 896],
2020
+ "2K": [2400, 1792],
2021
+ "4K": [4800, 3584]
2022
+ },
2023
+ "4:5": {
2024
+ "0.5K": [464, 576],
2025
+ "1K": [928, 1152],
2026
+ "2K": [1856, 2304],
2027
+ "4K": [3712, 4608]
2028
+ },
2029
+ "5:4": {
2030
+ "0.5K": [576, 464],
2031
+ "1K": [1152, 928],
2032
+ "2K": [2304, 1856],
2033
+ "4K": [4608, 3712]
2034
+ },
2035
+ "8:1": {
2036
+ "0.5K": [1536, 192],
2037
+ "1K": [3072, 384],
2038
+ "2K": [6144, 768],
2039
+ "4K": [12288, 1536]
2040
+ },
2041
+ "9:16": {
2042
+ "0.5K": [384, 688],
2043
+ "1K": [768, 1376],
2044
+ "2K": [1536, 2752],
2045
+ "4K": [3072, 5504]
2046
+ },
2047
+ "16:9": {
2048
+ "0.5K": [688, 384],
2049
+ "1K": [1376, 768],
2050
+ "2K": [2752, 1536],
2051
+ "4K": [5504, 3072]
2052
+ },
2053
+ "21:9": {
2054
+ "0.5K": [792, 168],
2055
+ "1K": [1584, 672],
2056
+ "2K": [3168, 1344],
2057
+ "4K": [6336, 2688]
2058
+ }
2059
+ };
2060
+ var AUTO_RESIZE_RESOLUTIONS = ["0.5K", "1K", "2K", "4K"];
2061
+ var AUTO_RESIZE_ASPECT_RATIOS = [
2062
+ { ratio: "1:1", desc: "Square" },
2063
+ { ratio: "16:9", desc: "Landscape" },
2064
+ { ratio: "9:16", desc: "Portrait / Story" },
2065
+ { ratio: "4:3", desc: "Landscape" },
2066
+ { ratio: "3:4", desc: "Portrait" },
2067
+ { ratio: "3:2", desc: "Landscape" },
2068
+ { ratio: "2:3", desc: "Portrait" },
2069
+ { ratio: "5:4", desc: "Landscape" },
2070
+ { ratio: "4:5", desc: "Portrait" },
2071
+ { ratio: "21:9", desc: "Ultrawide" },
2072
+ { ratio: "4:1", desc: "Wide strip" },
2073
+ { ratio: "1:4", desc: "Tall strip" },
2074
+ { ratio: "8:1", desc: "Banner" },
2075
+ { ratio: "1:8", desc: "Skyscraper" }
2076
+ ];
2077
+ function geminiDimsFor(ratio, resolution) {
2078
+ const row = GEMINI_RESOLUTION_TABLE[ratio] ?? GEMINI_RESOLUTION_TABLE["1:1"];
2079
+ const dims = row[resolution] ?? row["1K"] ?? [1024, 1024];
2080
+ return { width: dims[0], height: dims[1] };
2081
+ }
2082
+ function selectOptimalResolution(targetW, targetH, aspectRatio) {
2083
+ const resolutions = GEMINI_RESOLUTION_TABLE[aspectRatio];
2084
+ if (!resolutions) {
2085
+ return "1K";
2086
+ }
2087
+ const targetArea = targetW * targetH;
2088
+ let best = "1K";
2089
+ let bestDiff = Infinity;
2090
+ for (const [resLevel, [w, h]] of Object.entries(resolutions)) {
2091
+ const resArea = w * h;
2092
+ const diff = Math.abs(resArea - targetArea);
2093
+ if (resArea >= targetArea) {
2094
+ if (diff < bestDiff) {
2095
+ bestDiff = diff;
2096
+ best = resLevel;
2097
+ }
2098
+ } else if (bestDiff === Infinity || diff < bestDiff) {
2099
+ bestDiff = diff;
2100
+ best = resLevel;
2101
+ }
2102
+ }
2103
+ return best;
2104
+ }
2105
+ function preCreateAllFrames(targetDimensions, sourceFrame, app) {
2106
+ const startX = sourceFrame.x + sourceFrame.width + 80;
2107
+ const frameInfos = targetDimensions.map((dim, i) => {
2108
+ const y = targetDimensions.slice(0, i).reduce((acc, d) => acc + d.height + 40, sourceFrame.y);
2109
+ const newFrame = newFrameElement({
2110
+ ...FRAME_STYLE,
2111
+ x: startX,
2112
+ y,
2113
+ width: dim.width,
2114
+ height: dim.height,
2115
+ opacity: 100,
2116
+ locked: false,
2117
+ name: dim.label ?? `${dim.width}\xD7${dim.height}`
2118
+ });
2119
+ return {
2120
+ frame: newFrame,
2121
+ x: startX,
2122
+ y,
2123
+ width: dim.width,
2124
+ height: dim.height
2125
+ };
2126
+ });
2127
+ const currentEls = app.getSceneElements();
2128
+ const newFrameEls = frameInfos.map((fi) => fi.frame);
2129
+ const withFrames = [...currentEls, ...newFrameEls];
2130
+ syncMovedIndices(withFrames, arrayToMap(newFrameEls));
2131
+ app.updateScene({ elements: withFrames });
2132
+ return frameInfos.map((fi) => ({
2133
+ frameId: fi.frame.id,
2134
+ x: fi.x,
2135
+ y: fi.y,
2136
+ width: fi.width,
2137
+ height: fi.height
2138
+ }));
2139
+ }
2140
+ async function runAutoResize(opts) {
2141
+ const { runNativeAutoResize } = await import("./components/auto-resize/nativeResizeAgent-5XWJJ74W.js");
2142
+ return runNativeAutoResize(opts);
2143
+ }
2144
+
2145
+ export {
2146
+ isStructuredFrame,
2147
+ AUTO_RESIZE_FLAT_FRAME_REASON,
2148
+ isOpenAIImageModel,
2149
+ resolveImageOutputSize,
2150
+ buildReferenceManifest,
2151
+ callOpenRouterImageAPI,
2152
+ autoResizeStore,
2153
+ htmlToExcalidrawElements,
2154
+ normalizeRequestImages,
2155
+ describeChatError,
2156
+ runReviewerAgent,
2157
+ buildTargetBackground,
2158
+ needsBackgroundRegeneration,
2159
+ nearestGeminiAspectRatio,
2160
+ AUTO_RESIZE_RESOLUTIONS,
2161
+ AUTO_RESIZE_ASPECT_RATIOS,
2162
+ geminiDimsFor,
2163
+ selectOptimalResolution,
2164
+ preCreateAllFrames,
2165
+ runAutoResize
2166
+ };
2167
+ //# sourceMappingURL=chunk-T57LHRZV.js.map