@orangecatai/adgen-canvas 0.0.39 → 0.0.40

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-5XP2WJAQ.js";
69
+ } from "./chunk-XWMZCKIQ.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-E7VZGEUJ.js";
72
+ } from "./chunk-JWNDQ5YS.js";
73
73
  import {
74
74
  en_default
75
75
  } from "./chunk-LIELFD5T.js";
@@ -9733,7 +9733,7 @@ var exportCanvas = async (type, elements, appState, files, {
9733
9733
  let blob = canvasToBlob(tempCanvas);
9734
9734
  if (appState.exportEmbedScene) {
9735
9735
  blob = blob.then(
9736
- (blob2) => import("./data/image-PUPXCX7M.js").then(
9736
+ (blob2) => import("./data/image-CHTDICZP.js").then(
9737
9737
  ({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
9738
9738
  blob: blob2,
9739
9739
  metadata: serializeAsJSON(elements, appState, files, "local")
@@ -22814,7 +22814,7 @@ function hasPendingGeneration(elementId) {
22814
22814
  import { useCallback as useCallback17, useEffect as useEffect33, useRef as useRef29, useState as useState31 } from "react";
22815
22815
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords6 } from "@orangecatai/common";
22816
22816
  import { getElementAbsoluteCoords as getElementAbsoluteCoords9 } from "@orangecatai/element";
22817
- import { ArrowUp as ArrowUp2 } from "lucide-react";
22817
+ import { ArrowUp as ArrowUp2, ImagePlus as ImagePlus2, X as X2 } from "lucide-react";
22818
22818
 
22819
22819
  // utils/imageCompression.ts
22820
22820
  var MAX_EDGE_PX = 1280;
@@ -23354,243 +23354,6 @@ ${referenceManifest}` : promptWithBrandContext;
23354
23354
  throw new Error("No image was returned by the API.");
23355
23355
  }
23356
23356
 
23357
- // utils/brandContextUtils.ts
23358
- var SIZE_SCALE_MAP = {
23359
- display: "48\u201372px",
23360
- large: "32\u201348px",
23361
- medium: "20\u201328px",
23362
- small: "14\u201318px"
23363
- };
23364
- function buildBrandContextMessage(ctx) {
23365
- const lines = [
23366
- "## Brand Identity \u2014 you MUST apply every guideline below to all ads you create, by default"
23367
- ];
23368
- if (ctx.colors) {
23369
- const { primary, secondary, accent, background } = ctx.colors;
23370
- const parts = [
23371
- primary && `Primary: ${primary}`,
23372
- secondary && `Secondary: ${secondary}`,
23373
- accent && `Accent/CTA: ${accent}`,
23374
- background && `Background: ${background}`
23375
- ].filter(Boolean);
23376
- if (parts.length) {
23377
- lines.push(`**Colors**: ${parts.join(", ")}`);
23378
- }
23379
- }
23380
- if (ctx.typography) {
23381
- const { headline, body } = ctx.typography;
23382
- if (headline?.family) {
23383
- const sizeHint = headline.sizeScale ? ` (suggested size: ${SIZE_SCALE_MAP[headline.sizeScale] ?? headline.sizeScale})` : "";
23384
- const fontFileNote = headline.fontFamilyId ? ` \u2014 a brand font; match this typeface closely for the on-image text (use your judgment for which brand font suits headlines vs body copy)` : ` \u2014 render on-image text in a style that matches ${headline.family}`;
23385
- lines.push(
23386
- `**Headline font**: ${headline.family}${headline.weight ? ` weight ${headline.weight}` : ""}${sizeHint}${fontFileNote}`
23387
- );
23388
- }
23389
- if (body?.family) {
23390
- const sizeHint = body.sizeScale ? ` (suggested size: ${SIZE_SCALE_MAP[body.sizeScale] ?? body.sizeScale})` : "";
23391
- const fontFileNote = body.fontFamilyId ? ` \u2014 a brand font; match this typeface closely for the on-image text (use your judgment for which brand font suits headlines vs body copy)` : ` \u2014 render on-image text in a style that matches ${body.family}`;
23392
- lines.push(
23393
- `**Body font**: ${body.family}${body.weight ? ` weight ${body.weight}` : ""}${sizeHint}${fontFileNote}`
23394
- );
23395
- }
23396
- }
23397
- if (ctx.visualStyle) {
23398
- lines.push(`**Visual style**: ${ctx.visualStyle}`);
23399
- }
23400
- if (ctx.layoutPattern) {
23401
- lines.push(`**Layout pattern**: ${ctx.layoutPattern}`);
23402
- }
23403
- if (ctx.toneOfVoice) {
23404
- const { register: register2, vocabulary, ctaStyle, sentenceLength, person } = ctx.toneOfVoice;
23405
- const parts = [
23406
- register2 && `register: ${register2}`,
23407
- vocabulary && `vocabulary: ${vocabulary}`,
23408
- ctaStyle && `CTA style: ${ctaStyle}`,
23409
- sentenceLength && `sentence length: ${sentenceLength}`,
23410
- person && `person: ${person}`
23411
- ].filter(Boolean);
23412
- if (parts.length) {
23413
- lines.push(`**Tone of voice**: ${parts.join(", ")}`);
23414
- }
23415
- }
23416
- if (ctx.imagery) {
23417
- const {
23418
- type,
23419
- lighting,
23420
- composition,
23421
- subjects,
23422
- grading,
23423
- depthOfField,
23424
- environment,
23425
- negativeSpace
23426
- } = ctx.imagery;
23427
- const parts = [
23428
- type && `type: ${type}`,
23429
- subjects && `subjects: ${subjects}`,
23430
- lighting && `lighting: ${lighting}`,
23431
- grading && `grading: ${grading}`,
23432
- depthOfField && `depth of field: ${depthOfField}`,
23433
- environment && `environment: ${environment}`,
23434
- negativeSpace && `negative space: ${negativeSpace}`,
23435
- composition && `composition: ${composition}`
23436
- ].filter(Boolean);
23437
- if (parts.length) {
23438
- lines.push(`**Imagery direction**: ${parts.join(", ")}`);
23439
- }
23440
- }
23441
- lines.push(
23442
- "",
23443
- "**Precedence**: these are defaults, not absolutes. If the user's request explicitly states or clearly implies something different from a guideline above \u2014 a different color, mood, style, subject treatment, or tone \u2014 follow the user's explicit instruction for that ad instead of the brand guideline it conflicts with. Do not silently override what the user asked for in favor of brand consistency; only apply a guideline where the user's request is silent on that point."
23444
- );
23445
- return lines.join("\n");
23446
- }
23447
- function buildImageGenSafePrompt(prompt, brandContext, opts) {
23448
- const hasUserReferences = Boolean(opts?.hasUserReferences);
23449
- const headlineFontName = brandContext.typography?.headline?.family;
23450
- const bodyFontName = brandContext.typography?.body?.family;
23451
- const fontNames = [headlineFontName, bodyFontName].filter(
23452
- (n, i, arr) => Boolean(n) && arr.indexOf(n) === i
23453
- );
23454
- const fontHint = fontNames.length ? ` Brand typography: the brand font${fontNames.length > 1 ? "s are" : " is"} ${fontNames.join(
23455
- " and "
23456
- )} \u2014 you MUST render ALL on-image text exclusively in this exact typeface as shown in the font reference image${fontNames.length > 1 ? "s" : ""}. Do NOT use any generic, default, or substitute font.` : "";
23457
- const brandImageInstructions = [];
23458
- if (brandContext.logoDataUrl) {
23459
- brandImageInstructions.push(
23460
- `BRAND LOGO: One of the attached reference images is the exact brand logo graphic \u2014 the reference manifest at the end of this prompt identifies which. Embed it VERBATIM in the ad \u2014 do not redraw, simplify, stylize, or alter it in any way. Do not overlap it with other copy or elements. The logo must appear exactly as it does in the reference image.`
23461
- );
23462
- }
23463
- if (brandContext.headlineFontPreviewDataUrl || brandContext.bodyFontPreviewDataUrl) {
23464
- const multiple = Boolean(
23465
- brandContext.headlineFontPreviewDataUrl && brandContext.bodyFontPreviewDataUrl
23466
- );
23467
- brandImageInstructions.push(
23468
- `BRAND FONT: The attached brand font specimen ${multiple ? "images are" : "image is"} the brand's exact typeface${multiple ? "s" : ""}. You MUST render EVERY piece of on-image text \u2014 headlines, subheadlines, CTA, sign-off, and body copy \u2014 EXCLUSIVELY in ${multiple ? "these exact fonts as shown (use the headline specimen for headlines, the body specimen for body/supporting copy)" : "this exact font as shown in the image"}. Do NOT use any generic, default, or substitute font anywhere in the ad.`
23469
- );
23470
- }
23471
- const brandImageHint = brandImageInstructions.length > 0 ? ` ${brandImageInstructions.join(" ")}` : "";
23472
- const brandNameHint = brandContext.name && !hasUserReferences ? ` This ad is for the brand "${brandContext.name}" \u2014 all visual elements, copy, and style must reflect this brand exclusively.` : "";
23473
- return `${prompt}${brandNameHint}${fontHint}${brandImageHint} Render all specified copy as crisp, correctly-spelled, legible text within the image.
23474
-
23475
- ${PRECEDENCE_LINE}`;
23476
- }
23477
- var clean = (value) => {
23478
- const trimmed = value?.trim();
23479
- return trimmed ? trimmed : void 0;
23480
- };
23481
- var PRECEDENCE_LINE = "The subject, action, and setting described at the top are authoritative. Apply this brand direction only where the prompt is silent; where they conflict, the prompt wins.";
23482
- function buildVideoBrandPrompt(prompt, ctx, opts) {
23483
- const lines = [];
23484
- const brandName = clean(ctx.name);
23485
- if (brandName) {
23486
- lines.push(`This is a brand film for ${brandName}.`);
23487
- }
23488
- if (ctx.colors) {
23489
- const primary = clean(ctx.colors.primary);
23490
- const secondary = clean(ctx.colors.secondary);
23491
- const accent = clean(ctx.colors.accent);
23492
- const background = clean(ctx.colors.background);
23493
- const parts = [
23494
- primary && `dominant ${primary}`,
23495
- secondary && `supporting ${secondary}`,
23496
- accent && `${accent} reserved for accents and CTA moments`,
23497
- background && `${background} for backgrounds and negative space`
23498
- ].filter(Boolean);
23499
- if (parts.length) {
23500
- lines.push(
23501
- `Palette: ${parts.join(
23502
- "; "
23503
- )}. Grade the footage so these read as the natural colour story of the scene rather than as an overlay.`
23504
- );
23505
- }
23506
- }
23507
- const visualStyle = clean(ctx.visualStyle);
23508
- if (visualStyle) {
23509
- lines.push(`Look: ${visualStyle}.`);
23510
- }
23511
- if (ctx.imagery) {
23512
- const {
23513
- type,
23514
- subjects,
23515
- lighting,
23516
- grading,
23517
- depthOfField,
23518
- environment,
23519
- composition
23520
- } = ctx.imagery;
23521
- const parts = [
23522
- clean(type) && `${clean(type)} footage`,
23523
- clean(subjects) && `subjects: ${clean(subjects)}`,
23524
- clean(lighting) && `lighting: ${clean(lighting)}`,
23525
- clean(grading) && `grade: ${clean(grading)}`,
23526
- clean(depthOfField) && `depth of field: ${clean(depthOfField)}`,
23527
- clean(environment) && `environment: ${clean(environment)}`,
23528
- clean(composition) && `framing: ${clean(composition)}`
23529
- ].filter(Boolean);
23530
- if (parts.length) {
23531
- lines.push(`Cinematography: ${parts.join(", ")}.`);
23532
- }
23533
- }
23534
- if (ctx.toneOfVoice) {
23535
- const { register: register2, vocabulary, ctaStyle, person } = ctx.toneOfVoice;
23536
- const parts = [
23537
- clean(register2),
23538
- clean(vocabulary) && `${clean(vocabulary)} vocabulary`,
23539
- clean(ctaStyle) && `${clean(ctaStyle)} calls to action`,
23540
- clean(person)
23541
- ].filter(Boolean);
23542
- if (parts.length) {
23543
- lines.push(
23544
- `Tone: ${parts.join(
23545
- ", "
23546
- )} \u2014 applies to any narration, voiceover, or on-screen copy.`
23547
- );
23548
- }
23549
- }
23550
- const typefaces = [
23551
- clean(ctx.typography?.headline?.family),
23552
- clean(ctx.typography?.body?.family)
23553
- ].filter(
23554
- (name, i, arr) => Boolean(name) && arr.indexOf(name) === i
23555
- );
23556
- if (typefaces.length) {
23557
- lines.push(
23558
- `On-screen text: keep to a minimum; if any appears it should read as ${typefaces.join(
23559
- " or "
23560
- )}, or a close match in spirit. No paragraphs of text.`
23561
- );
23562
- }
23563
- const userReferenceCount = Math.max(0, opts?.userReferenceCount ?? 0);
23564
- const hasLogo = Boolean(opts?.hasLogoReference && clean(ctx.logoDataUrl));
23565
- if (hasLogo) {
23566
- lines.push(
23567
- userReferenceCount > 0 ? (
23568
- // With user references present the logo is no longer first — the
23569
- // manifest appended below states its actual position.
23570
- "Brand logo: the brand logo is supplied as one of the attached reference images (see the manifest below). If it appears in frame, reproduce it verbatim \u2014 never redraw, restyle, recolour, or animate its glyphs."
23571
- ) : "Brand logo: the first reference image is the exact brand logo. If it appears in frame, reproduce it verbatim \u2014 never redraw, restyle, recolour, or animate its glyphs."
23572
- );
23573
- }
23574
- const manifest = userReferenceCount > 0 ? `
23575
-
23576
- ${buildReferenceManifest(
23577
- userReferenceCount,
23578
- hasLogo ? ["the exact brand logo"] : []
23579
- )}` : "";
23580
- if (!lines.length) {
23581
- return manifest ? `${prompt}${manifest}` : prompt;
23582
- }
23583
- const header = brandName ? `## Brand direction \u2014 ${brandName}` : "## Brand direction";
23584
- return `${prompt}
23585
-
23586
- ${header}
23587
- ${lines.join(
23588
- "\n"
23589
- )}
23590
-
23591
- ${PRECEDENCE_LINE}${manifest}`;
23592
- }
23593
-
23594
23357
  // components/ImageGeneratorPanel.tsx
23595
23358
  import { useCallback as useCallback16, useEffect as useEffect32, useRef as useRef28, useState as useState30 } from "react";
23596
23359
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords5 } from "@orangecatai/common";
@@ -25040,7 +24803,6 @@ var ImageGeneratorPanel = ({
25040
24803
  };
25041
24804
  const appState = app.state;
25042
24805
  const imageGenUrl = app.props.imageGenUrl;
25043
- const brandContext = app.props.brandContext;
25044
24806
  const [prompt, setPrompt] = useState30(() => getPendingPrompt(element.id));
25045
24807
  const [selectedModel, setSelectedModel] = useState30(() => {
25046
24808
  const stored = getStoredSettings(element);
@@ -25319,39 +25081,20 @@ var ImageGeneratorPanel = ({
25319
25081
  let genError = null;
25320
25082
  try {
25321
25083
  const cfg = MODEL_CONFIG[selectedModel];
25322
- const brandImages = brandContext ? {
25323
- logo: brandContext.logoDataUrl,
25324
- fontPreview: brandContext.headlineFontPreviewDataUrl,
25325
- fontPreviewBody: brandContext.bodyFontPreviewDataUrl,
25326
- fontUrl: brandContext.headlineFontUrl,
25327
- brandContextText: buildBrandContextMessage(brandContext)
25328
- } : void 0;
25329
- const safePrompt = brandContext ? buildImageGenSafePrompt(prompt, brandContext, {
25330
- hasUserReferences: referenceImages.length > 0
25331
- }) : prompt;
25332
25084
  const { aspectRatio, imageSize } = resolveRequestSize(
25333
25085
  selectedModel,
25334
25086
  selectedRatio,
25335
25087
  selectedResolution
25336
25088
  );
25337
25089
  const imageDataUrl = await callOpenRouterImageAPI(
25338
- safePrompt,
25090
+ prompt,
25339
25091
  cfg.modelId,
25340
25092
  aspectRatio,
25341
25093
  imageSize,
25342
25094
  cfg.textOutput,
25343
25095
  imageGenUrl,
25344
25096
  referenceImages,
25345
- controller.signal,
25346
- brandImages,
25347
- {
25348
- // The user picked these references — they lead the array and outrank
25349
- // the brand kit, which still travels with them behind the logo.
25350
- referenceMode: "user-priority",
25351
- protectLogo: true,
25352
- hostBrandAssetSlots: 2,
25353
- hostAdReferenceSlots: 2
25354
- }
25097
+ controller.signal
25355
25098
  );
25356
25099
  await app.insertGeneratedImageIntoFrame(imageDataUrl, element);
25357
25100
  onAfterImageGen?.();
@@ -25826,7 +25569,6 @@ var ImageQuickEditPanel = ({
25826
25569
  }) => {
25827
25570
  const appState = app.state;
25828
25571
  const imageGenUrl = app.props.imageGenUrl;
25829
- const brandContext = app.props.brandContext;
25830
25572
  const [isOpen, setIsOpen] = useState31(
25831
25573
  () => forceOpen || hasPendingGeneration(element.id)
25832
25574
  );
@@ -25876,7 +25618,10 @@ var ImageQuickEditPanel = ({
25876
25618
  const ratioDropdownRef = useRef29(null);
25877
25619
  const resolutionDropdownRef = useRef29(null);
25878
25620
  const modelDropdownRef = useRef29(null);
25621
+ const fileInputRef = useRef29(null);
25879
25622
  const abortControllerRef = useRef29(null);
25623
+ const [referenceImages, setReferenceImages] = useState31([]);
25624
+ const [chipMenu, setChipMenu] = useState31(false);
25880
25625
  const applyRatio = useCallback17((ratioLabel) => {
25881
25626
  setSelectedRatio(ratioLabel);
25882
25627
  setRatioOpen(false);
@@ -25912,6 +25657,36 @@ var ImageQuickEditPanel = ({
25912
25657
  const handleStop = useCallback17(() => {
25913
25658
  abortPendingGeneration(element.id);
25914
25659
  }, [element.id]);
25660
+ const handleImageUpload = useCallback17(
25661
+ (e) => {
25662
+ const files = Array.from(e.target.files ?? []);
25663
+ e.target.value = "";
25664
+ Promise.all(
25665
+ files.map(
25666
+ (file2) => new Promise((resolve) => {
25667
+ const reader = new FileReader();
25668
+ reader.onload = () => resolve(reader.result);
25669
+ reader.onerror = () => resolve(null);
25670
+ reader.readAsDataURL(file2);
25671
+ })
25672
+ )
25673
+ ).then((results) => {
25674
+ const added = results.filter(
25675
+ (result) => Boolean(result)
25676
+ );
25677
+ setReferenceImages(
25678
+ (current) => [...current, ...added].slice(0, MAX_USER_REFERENCE_IMAGES)
25679
+ );
25680
+ });
25681
+ },
25682
+ []
25683
+ );
25684
+ const handlePickResolved = useCallback17((_slot, dataUrl) => {
25685
+ setReferenceImages(
25686
+ (current) => [...current, dataUrl].slice(0, MAX_USER_REFERENCE_IMAGES)
25687
+ );
25688
+ }, []);
25689
+ const picker = useCanvasFramePicker(app, handlePickResolved);
25915
25690
  const handleGenerate = useCallback17(async () => {
25916
25691
  if (!prompt.trim()) {
25917
25692
  setGenerateError("Please enter a prompt.");
@@ -25955,42 +25730,22 @@ var ImageQuickEditPanel = ({
25955
25730
  try {
25956
25731
  const cfg = MODEL_CONFIG[selectedModel];
25957
25732
  const refDataUrl = element.fileId ? app.files[element.fileId]?.dataURL : void 0;
25958
- const brandImages = brandContext ? {
25959
- logo: brandContext.logoDataUrl,
25960
- fontPreview: brandContext.headlineFontPreviewDataUrl,
25961
- fontPreviewBody: brandContext.bodyFontPreviewDataUrl,
25962
- fontUrl: brandContext.headlineFontUrl,
25963
- brandContextText: buildBrandContextMessage(brandContext)
25964
- } : void 0;
25965
- const basePrompt = brandContext ? buildImageGenSafePrompt(prompt, brandContext, {
25966
- hasUserReferences: Boolean(refDataUrl)
25967
- }) : prompt;
25968
- const safePrompt = `${basePrompt} CRITICAL PRESERVATION RULE: Apply ONLY the specific change described above. The number of visual elements (text blocks, logos, images, shapes, buttons) must be IDENTICAL before and after this edit \u2014 do NOT add any new element, do NOT remove any existing element, do NOT rearrange the layout or composition. Every element that exists in the source image must appear exactly once in the output, in the same position and arrangement. Only the specific requested change may differ.`;
25969
25733
  const { aspectRatio, imageSize } = resolveRequestSize(
25970
25734
  selectedModel,
25971
25735
  selectedRatio,
25972
25736
  selectedResolution
25973
25737
  );
25974
25738
  const imageDataUrl = await callOpenRouterImageAPI(
25975
- safePrompt,
25739
+ prompt,
25976
25740
  cfg.modelId,
25977
25741
  aspectRatio,
25978
25742
  imageSize,
25979
25743
  cfg.textOutput,
25980
25744
  imageGenUrl,
25981
- void 0,
25982
- controller.signal,
25983
- brandImages,
25984
- {
25985
- referenceMode: "user-priority",
25986
- // The image under edit is the subject — it ranks above the brand kit
25987
- // and is never shed. The logo rides along below it and, uniquely on
25988
- // this surface, may be dropped if the payload runs out of room.
25989
- sourceImageDataUrl: refDataUrl,
25990
- protectLogo: false,
25991
- hostBrandAssetSlots: 2,
25992
- hostAdReferenceSlots: 2
25993
- }
25745
+ [refDataUrl, ...referenceImages].filter(
25746
+ (dataUrl) => Boolean(dataUrl)
25747
+ ),
25748
+ controller.signal
25994
25749
  );
25995
25750
  const dims = getDimensions(
25996
25751
  selectedModel,
@@ -26031,6 +25786,7 @@ var ImageQuickEditPanel = ({
26031
25786
  selectedModel,
26032
25787
  selectedRatio,
26033
25788
  selectedResolution,
25789
+ referenceImages,
26034
25790
  app,
26035
25791
  element,
26036
25792
  onBeforeImageGen,
@@ -26054,6 +25810,13 @@ var ImageQuickEditPanel = ({
26054
25810
  const panelX = panelVpX - appState.offsetLeft;
26055
25811
  const panelY = panelVpY - appState.offsetTop + 16;
26056
25812
  return /* @__PURE__ */ jsxs49(Fragment12, { children: [
25813
+ /* @__PURE__ */ jsx89(
25814
+ ElementPickHighlight,
25815
+ {
25816
+ element: picker.pickingSlot ? picker.hoveredElement : null,
25817
+ appState
25818
+ }
25819
+ ),
26057
25820
  !forceOpen && /* @__PURE__ */ jsx89(
26058
25821
  "div",
26059
25822
  {
@@ -26075,172 +25838,250 @@ var ImageQuickEditPanel = ({
26075
25838
  )
26076
25839
  }
26077
25840
  ),
26078
- isOpen && /* @__PURE__ */ jsx89(
25841
+ isOpen && /* @__PURE__ */ jsxs49(
26079
25842
  "div",
26080
25843
  {
26081
25844
  className: "igp iqep__panel",
26082
25845
  style: { left: `${panelX}px`, top: `${panelY}px` },
26083
25846
  onPointerDown: (e) => e.stopPropagation(),
26084
25847
  onClick: (e) => e.stopPropagation(),
26085
- children: /* @__PURE__ */ jsxs49(
26086
- PromptInput,
26087
- {
26088
- isLoading: isGenerating,
26089
- value: prompt,
26090
- onValueChange: setPrompt,
26091
- onSubmit: handleGenerate,
26092
- className: "igp-container",
26093
- children: [
26094
- /* @__PURE__ */ jsx89(
26095
- PromptInputTextarea,
26096
- {
26097
- placeholder: "Describe your edit\u2026",
26098
- className: "igp-textarea",
26099
- disableAutosize: false
26100
- }
26101
- ),
26102
- generateError && /* @__PURE__ */ jsx89("div", { className: "igp-error", children: generateError }),
26103
- /* @__PURE__ */ jsxs49(PromptInputActions, { className: "igp-actions", children: [
26104
- /* @__PURE__ */ jsx89("div", { className: "igp-actions-left", children: /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-root", ref: modelDropdownRef, children: [
26105
- /* @__PURE__ */ jsx89(PromptInputAction, { tooltip: "Select model", side: "top", children: /* @__PURE__ */ jsxs49(
25848
+ children: [
25849
+ /* @__PURE__ */ jsx89(
25850
+ "input",
25851
+ {
25852
+ ref: fileInputRef,
25853
+ type: "file",
25854
+ accept: "image/*",
25855
+ multiple: true,
25856
+ style: { display: "none" },
25857
+ onChange: handleImageUpload
25858
+ }
25859
+ ),
25860
+ /* @__PURE__ */ jsxs49(
25861
+ PromptInput,
25862
+ {
25863
+ isLoading: isGenerating,
25864
+ value: prompt,
25865
+ onValueChange: setPrompt,
25866
+ onSubmit: handleGenerate,
25867
+ className: "igp-container",
25868
+ children: [
25869
+ referenceImages.length > 0 && /* @__PURE__ */ jsx89("div", { className: "igp-ref-chips", children: referenceImages.map((src, i) => /* @__PURE__ */ jsxs49("div", { className: "igp-ref-chip", children: [
25870
+ /* @__PURE__ */ jsx89("img", { src, alt: `Reference ${i + 1}` }),
25871
+ /* @__PURE__ */ jsxs49("span", { children: [
25872
+ "Reference ",
25873
+ i + 1
25874
+ ] }),
25875
+ /* @__PURE__ */ jsx89(
26106
25876
  "button",
26107
25877
  {
26108
- className: "igp-btn",
26109
- onClick: () => {
26110
- setModelOpen((v) => !v);
26111
- setRatioOpen(false);
26112
- setResolutionOpen(false);
26113
- },
26114
- disabled: isGenerating,
26115
- children: [
26116
- /* @__PURE__ */ jsx89(ModelIcon2, {}),
26117
- /* @__PURE__ */ jsx89("span", { children: selectedModel }),
26118
- /* @__PURE__ */ jsx89(ChevronDownIcon2, {})
26119
- ]
25878
+ onClick: () => setReferenceImages(
25879
+ (current) => current.filter((_, index) => index !== i)
25880
+ ),
25881
+ title: "Remove",
25882
+ children: /* @__PURE__ */ jsx89(X2, { size: 10 })
26120
25883
  }
26121
- ) }),
26122
- modelOpen && /* @__PURE__ */ jsx89("div", { className: "igp-dropdown igp-dropdown--up igp-dropdown--left", children: MODEL_NAMES.map((m) => /* @__PURE__ */ jsxs49(
25884
+ )
25885
+ ] }, `${i}-${src.slice(-24)}`)) }),
25886
+ picker.pickingSlot && /* @__PURE__ */ jsxs49("div", { className: "chip-pick-hint", children: [
25887
+ /* @__PURE__ */ jsx89("span", { children: "Click an image or video on the canvas" }),
25888
+ /* @__PURE__ */ jsx89(
26123
25889
  "button",
26124
25890
  {
26125
- className: "igp-dropdown-item",
26126
- onClick: () => {
26127
- setSelectedModel(m);
26128
- setModelOpen(false);
26129
- },
26130
- children: [
26131
- /* @__PURE__ */ jsx89("span", { style: { flex: 1 }, children: m }),
26132
- m === selectedModel && /* @__PURE__ */ jsx89("span", { className: "igp-check", children: /* @__PURE__ */ jsx89(CheckIcon3, {}) })
26133
- ]
26134
- },
26135
- m
26136
- )) })
26137
- ] }) }),
26138
- /* @__PURE__ */ jsxs49("div", { className: "igp-actions-right", children: [
26139
- /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-root", ref: resolutionDropdownRef, children: [
26140
- /* @__PURE__ */ jsxs49(
26141
- "button",
26142
- {
26143
- className: "igp-btn igp-btn--compact",
26144
- onClick: () => {
26145
- if (availableResolutions.length <= 1) {
26146
- return;
26147
- }
26148
- setResolutionOpen((v) => !v);
26149
- setRatioOpen(false);
26150
- setModelOpen(false);
26151
- },
26152
- disabled: isGenerating || availableResolutions.length <= 1,
26153
- title: availableResolutions.length <= 1 ? "Fixed resolution for this model" : void 0,
26154
- children: [
26155
- /* @__PURE__ */ jsx89("span", { children: selectedResolution }),
26156
- availableResolutions.length > 1 && /* @__PURE__ */ jsx89(ChevronDownIcon2, {})
26157
- ]
26158
- }
26159
- ),
26160
- resolutionOpen && availableResolutions.length > 1 && /* @__PURE__ */ jsx89("div", { className: "igp-dropdown igp-dropdown--up igp-dropdown--right", children: availableResolutions.map(
26161
- (r) => /* @__PURE__ */ jsxs49(
25891
+ className: "chip-pick-hint__cancel",
25892
+ onClick: picker.cancel,
25893
+ children: "Cancel"
25894
+ }
25895
+ )
25896
+ ] }),
25897
+ picker.isResolving && /* @__PURE__ */ jsx89("div", { className: "chip-pick-hint", children: "Reading video frame\u2026" }),
25898
+ picker.error && /* @__PURE__ */ jsx89("div", { className: "chip-pick-error", children: picker.error }),
25899
+ /* @__PURE__ */ jsx89(
25900
+ PromptInputTextarea,
25901
+ {
25902
+ placeholder: "Describe your edit\u2026",
25903
+ className: "igp-textarea",
25904
+ disableAutosize: false
25905
+ }
25906
+ ),
25907
+ generateError && /* @__PURE__ */ jsx89("div", { className: "igp-error", children: generateError }),
25908
+ /* @__PURE__ */ jsxs49(PromptInputActions, { className: "igp-actions", children: [
25909
+ /* @__PURE__ */ jsxs49("div", { className: "igp-actions-left", children: [
25910
+ /* @__PURE__ */ jsxs49("div", { className: "chip-slot", children: [
25911
+ /* @__PURE__ */ jsx89(
25912
+ PromptInputAction,
25913
+ {
25914
+ tooltip: referenceImages.length >= MAX_USER_REFERENCE_IMAGES ? `Maximum ${MAX_USER_REFERENCE_IMAGES} reference images` : "Add reference images",
25915
+ side: "top",
25916
+ children: /* @__PURE__ */ jsx89(
25917
+ "button",
25918
+ {
25919
+ className: `igp-btn igp-btn--icon${referenceImages.length > 0 ? " igp-btn--active" : ""}`,
25920
+ onClick: () => setChipMenu((open) => !open),
25921
+ disabled: isGenerating || referenceImages.length >= MAX_USER_REFERENCE_IMAGES,
25922
+ children: /* @__PURE__ */ jsx89(ImagePlus2, { size: 15 })
25923
+ }
25924
+ )
25925
+ }
25926
+ ),
25927
+ chipMenu && /* @__PURE__ */ jsx89(
25928
+ ChipSourceMenu,
25929
+ {
25930
+ onClose: () => setChipMenu(false),
25931
+ onUpload: () => fileInputRef.current?.click(),
25932
+ onPickFromCanvas: () => picker.begin("reference", "imageOrVideo")
25933
+ }
25934
+ ),
25935
+ picker.pendingVideo && /* @__PURE__ */ jsx89(
25936
+ VideoFrameChoiceMenu,
25937
+ {
25938
+ onChoose: picker.chooseVideoFrame,
25939
+ onCancel: picker.cancel
25940
+ }
25941
+ )
25942
+ ] }),
25943
+ /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-root", ref: modelDropdownRef, children: [
25944
+ /* @__PURE__ */ jsx89(PromptInputAction, { tooltip: "Select model", side: "top", children: /* @__PURE__ */ jsxs49(
26162
25945
  "button",
26163
25946
  {
26164
- className: "igp-dropdown-item",
25947
+ className: "igp-btn",
26165
25948
  onClick: () => {
26166
- setSelectedResolution(r);
25949
+ setModelOpen((v) => !v);
25950
+ setRatioOpen(false);
26167
25951
  setResolutionOpen(false);
26168
25952
  },
25953
+ disabled: isGenerating,
25954
+ children: [
25955
+ /* @__PURE__ */ jsx89(ModelIcon2, {}),
25956
+ /* @__PURE__ */ jsx89("span", { children: selectedModel }),
25957
+ /* @__PURE__ */ jsx89(ChevronDownIcon2, {})
25958
+ ]
25959
+ }
25960
+ ) }),
25961
+ modelOpen && /* @__PURE__ */ jsx89("div", { className: "igp-dropdown igp-dropdown--up igp-dropdown--left", children: MODEL_NAMES.map((m) => /* @__PURE__ */ jsxs49(
25962
+ "button",
25963
+ {
25964
+ className: "igp-dropdown-item",
25965
+ onClick: () => {
25966
+ setSelectedModel(m);
25967
+ setModelOpen(false);
25968
+ },
26169
25969
  children: [
26170
- /* @__PURE__ */ jsx89("span", { style: { flex: 1 }, children: r }),
26171
- r === selectedResolution && /* @__PURE__ */ jsx89("span", { className: "igp-check", children: /* @__PURE__ */ jsx89(CheckIcon3, {}) })
25970
+ /* @__PURE__ */ jsx89("span", { style: { flex: 1 }, children: m }),
25971
+ m === selectedModel && /* @__PURE__ */ jsx89("span", { className: "igp-check", children: /* @__PURE__ */ jsx89(CheckIcon3, {}) })
26172
25972
  ]
26173
25973
  },
26174
- r
26175
- )
26176
- ) })
25974
+ m
25975
+ )) })
25976
+ ] })
26177
25977
  ] }),
26178
- /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-root", ref: ratioDropdownRef, children: [
26179
- /* @__PURE__ */ jsxs49(
25978
+ /* @__PURE__ */ jsxs49("div", { className: "igp-actions-right", children: [
25979
+ /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-root", ref: resolutionDropdownRef, children: [
25980
+ /* @__PURE__ */ jsxs49(
25981
+ "button",
25982
+ {
25983
+ className: "igp-btn igp-btn--compact",
25984
+ onClick: () => {
25985
+ if (availableResolutions.length <= 1) {
25986
+ return;
25987
+ }
25988
+ setResolutionOpen((v) => !v);
25989
+ setRatioOpen(false);
25990
+ setModelOpen(false);
25991
+ },
25992
+ disabled: isGenerating || availableResolutions.length <= 1,
25993
+ title: availableResolutions.length <= 1 ? "Fixed resolution for this model" : void 0,
25994
+ children: [
25995
+ /* @__PURE__ */ jsx89("span", { children: selectedResolution }),
25996
+ availableResolutions.length > 1 && /* @__PURE__ */ jsx89(ChevronDownIcon2, {})
25997
+ ]
25998
+ }
25999
+ ),
26000
+ resolutionOpen && availableResolutions.length > 1 && /* @__PURE__ */ jsx89("div", { className: "igp-dropdown igp-dropdown--up igp-dropdown--right", children: availableResolutions.map(
26001
+ (r) => /* @__PURE__ */ jsxs49(
26002
+ "button",
26003
+ {
26004
+ className: "igp-dropdown-item",
26005
+ onClick: () => {
26006
+ setSelectedResolution(r);
26007
+ setResolutionOpen(false);
26008
+ },
26009
+ children: [
26010
+ /* @__PURE__ */ jsx89("span", { style: { flex: 1 }, children: r }),
26011
+ r === selectedResolution && /* @__PURE__ */ jsx89("span", { className: "igp-check", children: /* @__PURE__ */ jsx89(CheckIcon3, {}) })
26012
+ ]
26013
+ },
26014
+ r
26015
+ )
26016
+ ) })
26017
+ ] }),
26018
+ /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-root", ref: ratioDropdownRef, children: [
26019
+ /* @__PURE__ */ jsxs49(
26020
+ "button",
26021
+ {
26022
+ className: "igp-btn igp-btn--compact",
26023
+ onClick: () => {
26024
+ setRatioOpen((v) => !v);
26025
+ setResolutionOpen(false);
26026
+ setModelOpen(false);
26027
+ },
26028
+ disabled: isGenerating,
26029
+ children: [
26030
+ /* @__PURE__ */ jsx89("span", { children: selectedRatio }),
26031
+ /* @__PURE__ */ jsx89(ChevronDownIcon2, {})
26032
+ ]
26033
+ }
26034
+ ),
26035
+ ratioOpen && /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown igp-dropdown--up igp-dropdown--right igp-dropdown--ratio", children: [
26036
+ /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-header", children: [
26037
+ /* @__PURE__ */ jsx89("span", { children: "Output format" }),
26038
+ /* @__PURE__ */ jsx89(InfoIcon2, {})
26039
+ ] }),
26040
+ availableRatios.map(
26041
+ (ratioLabel) => {
26042
+ const dims = getDimensions(
26043
+ selectedModel,
26044
+ ratioLabel,
26045
+ selectedResolution
26046
+ );
26047
+ return /* @__PURE__ */ jsxs49(
26048
+ "button",
26049
+ {
26050
+ className: "igp-dropdown-item",
26051
+ onClick: () => applyRatio(ratioLabel),
26052
+ children: [
26053
+ /* @__PURE__ */ jsx89(RatioShapeIcon2, { label: ratioLabel }),
26054
+ /* @__PURE__ */ jsx89("span", { style: { flex: 1 }, children: ratioLabel }),
26055
+ /* @__PURE__ */ jsxs49("span", { className: "igp-dims", children: [
26056
+ dims.width,
26057
+ "\xD7",
26058
+ dims.height
26059
+ ] }),
26060
+ ratioLabel === selectedRatio && /* @__PURE__ */ jsx89("span", { className: "igp-check", children: /* @__PURE__ */ jsx89(CheckIcon3, {}) })
26061
+ ]
26062
+ },
26063
+ ratioLabel
26064
+ );
26065
+ }
26066
+ )
26067
+ ] })
26068
+ ] }),
26069
+ /* @__PURE__ */ jsx89(
26180
26070
  "button",
26181
26071
  {
26182
- className: "igp-btn igp-btn--compact",
26183
- onClick: () => {
26184
- setRatioOpen((v) => !v);
26185
- setResolutionOpen(false);
26186
- setModelOpen(false);
26187
- },
26188
- disabled: isGenerating,
26189
- children: [
26190
- /* @__PURE__ */ jsx89("span", { children: selectedRatio }),
26191
- /* @__PURE__ */ jsx89(ChevronDownIcon2, {})
26192
- ]
26072
+ className: "igp-generate",
26073
+ onClick: isGenerating ? handleStop : handleGenerate,
26074
+ disabled: !isGenerating && !prompt.trim(),
26075
+ title: isGenerating ? "Stop generation" : "Generate edit",
26076
+ children: isGenerating ? /* @__PURE__ */ jsx89(SpinnerIcon3, {}) : /* @__PURE__ */ jsx89(ArrowUp2, { size: 16 })
26193
26077
  }
26194
- ),
26195
- ratioOpen && /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown igp-dropdown--up igp-dropdown--right igp-dropdown--ratio", children: [
26196
- /* @__PURE__ */ jsxs49("div", { className: "igp-dropdown-header", children: [
26197
- /* @__PURE__ */ jsx89("span", { children: "Output format" }),
26198
- /* @__PURE__ */ jsx89(InfoIcon2, {})
26199
- ] }),
26200
- availableRatios.map(
26201
- (ratioLabel) => {
26202
- const dims = getDimensions(
26203
- selectedModel,
26204
- ratioLabel,
26205
- selectedResolution
26206
- );
26207
- return /* @__PURE__ */ jsxs49(
26208
- "button",
26209
- {
26210
- className: "igp-dropdown-item",
26211
- onClick: () => applyRatio(ratioLabel),
26212
- children: [
26213
- /* @__PURE__ */ jsx89(RatioShapeIcon2, { label: ratioLabel }),
26214
- /* @__PURE__ */ jsx89("span", { style: { flex: 1 }, children: ratioLabel }),
26215
- /* @__PURE__ */ jsxs49("span", { className: "igp-dims", children: [
26216
- dims.width,
26217
- "\xD7",
26218
- dims.height
26219
- ] }),
26220
- ratioLabel === selectedRatio && /* @__PURE__ */ jsx89("span", { className: "igp-check", children: /* @__PURE__ */ jsx89(CheckIcon3, {}) })
26221
- ]
26222
- },
26223
- ratioLabel
26224
- );
26225
- }
26226
- )
26227
- ] })
26228
- ] }),
26229
- /* @__PURE__ */ jsx89(
26230
- "button",
26231
- {
26232
- className: "igp-generate",
26233
- onClick: isGenerating ? handleStop : handleGenerate,
26234
- disabled: !isGenerating && !prompt.trim(),
26235
- title: isGenerating ? "Stop generation" : "Generate edit",
26236
- children: isGenerating ? /* @__PURE__ */ jsx89(SpinnerIcon3, {}) : /* @__PURE__ */ jsx89(ArrowUp2, { size: 16 })
26237
- }
26238
- )
26078
+ )
26079
+ ] })
26239
26080
  ] })
26240
- ] })
26241
- ]
26242
- }
26243
- )
26081
+ ]
26082
+ }
26083
+ )
26084
+ ]
26244
26085
  }
26245
26086
  )
26246
26087
  ] });
@@ -30703,7 +30544,214 @@ import {
30703
30544
  CaptureUpdateAction as CaptureUpdateAction39,
30704
30545
  getElementAbsoluteCoords as getElementAbsoluteCoords12
30705
30546
  } from "@orangecatai/element";
30706
- import { ArrowUp as ArrowUp3, Volume2, X as X3 } from "lucide-react";
30547
+ import { ArrowUp as ArrowUp3, Volume2, X as X4 } from "lucide-react";
30548
+
30549
+ // utils/brandContextUtils.ts
30550
+ var SIZE_SCALE_MAP = {
30551
+ display: "48\u201372px",
30552
+ large: "32\u201348px",
30553
+ medium: "20\u201328px",
30554
+ small: "14\u201318px"
30555
+ };
30556
+ function buildBrandContextMessage(ctx) {
30557
+ const lines = [
30558
+ "## Brand Identity \u2014 you MUST apply every guideline below to all ads you create, by default"
30559
+ ];
30560
+ if (ctx.colors) {
30561
+ const { primary, secondary, accent, background } = ctx.colors;
30562
+ const parts = [
30563
+ primary && `Primary: ${primary}`,
30564
+ secondary && `Secondary: ${secondary}`,
30565
+ accent && `Accent/CTA: ${accent}`,
30566
+ background && `Background: ${background}`
30567
+ ].filter(Boolean);
30568
+ if (parts.length) {
30569
+ lines.push(`**Colors**: ${parts.join(", ")}`);
30570
+ }
30571
+ }
30572
+ if (ctx.typography) {
30573
+ const { headline, body } = ctx.typography;
30574
+ if (headline?.family) {
30575
+ const sizeHint = headline.sizeScale ? ` (suggested size: ${SIZE_SCALE_MAP[headline.sizeScale] ?? headline.sizeScale})` : "";
30576
+ const fontFileNote = headline.fontFamilyId ? ` \u2014 a brand font; match this typeface closely for the on-image text (use your judgment for which brand font suits headlines vs body copy)` : ` \u2014 render on-image text in a style that matches ${headline.family}`;
30577
+ lines.push(
30578
+ `**Headline font**: ${headline.family}${headline.weight ? ` weight ${headline.weight}` : ""}${sizeHint}${fontFileNote}`
30579
+ );
30580
+ }
30581
+ if (body?.family) {
30582
+ const sizeHint = body.sizeScale ? ` (suggested size: ${SIZE_SCALE_MAP[body.sizeScale] ?? body.sizeScale})` : "";
30583
+ const fontFileNote = body.fontFamilyId ? ` \u2014 a brand font; match this typeface closely for the on-image text (use your judgment for which brand font suits headlines vs body copy)` : ` \u2014 render on-image text in a style that matches ${body.family}`;
30584
+ lines.push(
30585
+ `**Body font**: ${body.family}${body.weight ? ` weight ${body.weight}` : ""}${sizeHint}${fontFileNote}`
30586
+ );
30587
+ }
30588
+ }
30589
+ if (ctx.visualStyle) {
30590
+ lines.push(`**Visual style**: ${ctx.visualStyle}`);
30591
+ }
30592
+ if (ctx.layoutPattern) {
30593
+ lines.push(`**Layout pattern**: ${ctx.layoutPattern}`);
30594
+ }
30595
+ if (ctx.toneOfVoice) {
30596
+ const { register: register2, vocabulary, ctaStyle, sentenceLength, person } = ctx.toneOfVoice;
30597
+ const parts = [
30598
+ register2 && `register: ${register2}`,
30599
+ vocabulary && `vocabulary: ${vocabulary}`,
30600
+ ctaStyle && `CTA style: ${ctaStyle}`,
30601
+ sentenceLength && `sentence length: ${sentenceLength}`,
30602
+ person && `person: ${person}`
30603
+ ].filter(Boolean);
30604
+ if (parts.length) {
30605
+ lines.push(`**Tone of voice**: ${parts.join(", ")}`);
30606
+ }
30607
+ }
30608
+ if (ctx.imagery) {
30609
+ const {
30610
+ type,
30611
+ lighting,
30612
+ composition,
30613
+ subjects,
30614
+ grading,
30615
+ depthOfField,
30616
+ environment,
30617
+ negativeSpace
30618
+ } = ctx.imagery;
30619
+ const parts = [
30620
+ type && `type: ${type}`,
30621
+ subjects && `subjects: ${subjects}`,
30622
+ lighting && `lighting: ${lighting}`,
30623
+ grading && `grading: ${grading}`,
30624
+ depthOfField && `depth of field: ${depthOfField}`,
30625
+ environment && `environment: ${environment}`,
30626
+ negativeSpace && `negative space: ${negativeSpace}`,
30627
+ composition && `composition: ${composition}`
30628
+ ].filter(Boolean);
30629
+ if (parts.length) {
30630
+ lines.push(`**Imagery direction**: ${parts.join(", ")}`);
30631
+ }
30632
+ }
30633
+ lines.push(
30634
+ "",
30635
+ "**Precedence**: these are defaults, not absolutes. If the user's request explicitly states or clearly implies something different from a guideline above \u2014 a different color, mood, style, subject treatment, or tone \u2014 follow the user's explicit instruction for that ad instead of the brand guideline it conflicts with. Do not silently override what the user asked for in favor of brand consistency; only apply a guideline where the user's request is silent on that point."
30636
+ );
30637
+ return lines.join("\n");
30638
+ }
30639
+ var clean = (value) => {
30640
+ const trimmed = value?.trim();
30641
+ return trimmed ? trimmed : void 0;
30642
+ };
30643
+ var PRECEDENCE_LINE = "The subject, action, and setting described at the top are authoritative. Apply this brand direction only where the prompt is silent; where they conflict, the prompt wins.";
30644
+ function buildVideoBrandPrompt(prompt, ctx, opts) {
30645
+ const lines = [];
30646
+ const brandName = clean(ctx.name);
30647
+ if (brandName) {
30648
+ lines.push(`This is a brand film for ${brandName}.`);
30649
+ }
30650
+ if (ctx.colors) {
30651
+ const primary = clean(ctx.colors.primary);
30652
+ const secondary = clean(ctx.colors.secondary);
30653
+ const accent = clean(ctx.colors.accent);
30654
+ const background = clean(ctx.colors.background);
30655
+ const parts = [
30656
+ primary && `dominant ${primary}`,
30657
+ secondary && `supporting ${secondary}`,
30658
+ accent && `${accent} reserved for accents and CTA moments`,
30659
+ background && `${background} for backgrounds and negative space`
30660
+ ].filter(Boolean);
30661
+ if (parts.length) {
30662
+ lines.push(
30663
+ `Palette: ${parts.join(
30664
+ "; "
30665
+ )}. Grade the footage so these read as the natural colour story of the scene rather than as an overlay.`
30666
+ );
30667
+ }
30668
+ }
30669
+ const visualStyle = clean(ctx.visualStyle);
30670
+ if (visualStyle) {
30671
+ lines.push(`Look: ${visualStyle}.`);
30672
+ }
30673
+ if (ctx.imagery) {
30674
+ const {
30675
+ type,
30676
+ subjects,
30677
+ lighting,
30678
+ grading,
30679
+ depthOfField,
30680
+ environment,
30681
+ composition
30682
+ } = ctx.imagery;
30683
+ const parts = [
30684
+ clean(type) && `${clean(type)} footage`,
30685
+ clean(subjects) && `subjects: ${clean(subjects)}`,
30686
+ clean(lighting) && `lighting: ${clean(lighting)}`,
30687
+ clean(grading) && `grade: ${clean(grading)}`,
30688
+ clean(depthOfField) && `depth of field: ${clean(depthOfField)}`,
30689
+ clean(environment) && `environment: ${clean(environment)}`,
30690
+ clean(composition) && `framing: ${clean(composition)}`
30691
+ ].filter(Boolean);
30692
+ if (parts.length) {
30693
+ lines.push(`Cinematography: ${parts.join(", ")}.`);
30694
+ }
30695
+ }
30696
+ if (ctx.toneOfVoice) {
30697
+ const { register: register2, vocabulary, ctaStyle, person } = ctx.toneOfVoice;
30698
+ const parts = [
30699
+ clean(register2),
30700
+ clean(vocabulary) && `${clean(vocabulary)} vocabulary`,
30701
+ clean(ctaStyle) && `${clean(ctaStyle)} calls to action`,
30702
+ clean(person)
30703
+ ].filter(Boolean);
30704
+ if (parts.length) {
30705
+ lines.push(
30706
+ `Tone: ${parts.join(
30707
+ ", "
30708
+ )} \u2014 applies to any narration, voiceover, or on-screen copy.`
30709
+ );
30710
+ }
30711
+ }
30712
+ const typefaces = [
30713
+ clean(ctx.typography?.headline?.family),
30714
+ clean(ctx.typography?.body?.family)
30715
+ ].filter(
30716
+ (name, i, arr) => Boolean(name) && arr.indexOf(name) === i
30717
+ );
30718
+ if (typefaces.length) {
30719
+ lines.push(
30720
+ `On-screen text: keep to a minimum; if any appears it should read as ${typefaces.join(
30721
+ " or "
30722
+ )}, or a close match in spirit. No paragraphs of text.`
30723
+ );
30724
+ }
30725
+ const userReferenceCount = Math.max(0, opts?.userReferenceCount ?? 0);
30726
+ const hasLogo = Boolean(opts?.hasLogoReference && clean(ctx.logoDataUrl));
30727
+ if (hasLogo) {
30728
+ lines.push(
30729
+ userReferenceCount > 0 ? (
30730
+ // With user references present the logo is no longer first — the
30731
+ // manifest appended below states its actual position.
30732
+ "Brand logo: the brand logo is supplied as one of the attached reference images (see the manifest below). If it appears in frame, reproduce it verbatim \u2014 never redraw, restyle, recolour, or animate its glyphs."
30733
+ ) : "Brand logo: the first reference image is the exact brand logo. If it appears in frame, reproduce it verbatim \u2014 never redraw, restyle, recolour, or animate its glyphs."
30734
+ );
30735
+ }
30736
+ const manifest = userReferenceCount > 0 ? `
30737
+
30738
+ ${buildReferenceManifest(
30739
+ userReferenceCount,
30740
+ hasLogo ? ["the exact brand logo"] : []
30741
+ )}` : "";
30742
+ if (!lines.length) {
30743
+ return manifest ? `${prompt}${manifest}` : prompt;
30744
+ }
30745
+ const header = brandName ? `## Brand direction \u2014 ${brandName}` : "## Brand direction";
30746
+ return `${prompt}
30747
+
30748
+ ${header}
30749
+ ${lines.join(
30750
+ "\n"
30751
+ )}
30752
+
30753
+ ${PRECEDENCE_LINE}${manifest}`;
30754
+ }
30707
30755
 
30708
30756
  // utils/videoApi.ts
30709
30757
  async function submitVideoJob(params, videoGenUrl) {
@@ -30948,7 +30996,7 @@ import {
30948
30996
  useRef as useRef33,
30949
30997
  useState as useState36
30950
30998
  } from "react";
30951
- import { Check, ChevronDown, Loader2, Square, X as X2 } from "lucide-react";
30999
+ import { Check, ChevronDown, Loader2, Square, X as X3 } from "lucide-react";
30952
31000
  import { CaptureUpdateAction as CaptureUpdateAction38 } from "@orangecatai/element";
30953
31001
  import { Fragment as Fragment17, jsx as jsx97, jsxs as jsxs54 } from "react/jsx-runtime";
30954
31002
  var BEAT_LABELS = {
@@ -31296,7 +31344,7 @@ var VideoChainPanel = forwardRef6(
31296
31344
  children: [
31297
31345
  isRunning && /* @__PURE__ */ jsx97(Loader2, { className: "vgp-spinner", size: 13 }),
31298
31346
  phase === "done" && /* @__PURE__ */ jsx97(Check, { size: 13 }),
31299
- phase === "error" && /* @__PURE__ */ jsx97(X2, { size: 13 }),
31347
+ phase === "error" && /* @__PURE__ */ jsx97(X3, { size: 13 }),
31300
31348
  /* @__PURE__ */ jsx97("span", { className: "vgp-chain-summary-text", children: summaryText }),
31301
31349
  isRunning && /* @__PURE__ */ jsx97(
31302
31350
  "span",
@@ -31368,7 +31416,7 @@ var VideoChainPanel = forwardRef6(
31368
31416
  "%"
31369
31417
  ] }),
31370
31418
  segment.status === "failed" && /* @__PURE__ */ jsxs54(Fragment17, { children: [
31371
- /* @__PURE__ */ jsx97(X2, { size: 13 }),
31419
+ /* @__PURE__ */ jsx97(X3, { size: 13 }),
31372
31420
  " Failed"
31373
31421
  ] }),
31374
31422
  segment.status === "queued" && "Queued"
@@ -32538,7 +32586,7 @@ var VideoGeneratorPanel = ({
32538
32586
  className: "vgp-chip-remove",
32539
32587
  onClick: () => setFirstFrame(null),
32540
32588
  disabled: isGenerating,
32541
- children: /* @__PURE__ */ jsx98(X3, { size: 9 })
32589
+ children: /* @__PURE__ */ jsx98(X4, { size: 9 })
32542
32590
  }
32543
32591
  )
32544
32592
  ] }) : /* @__PURE__ */ jsxs55("div", { className: "chip-slot", children: [
@@ -32580,7 +32628,7 @@ var VideoGeneratorPanel = ({
32580
32628
  className: "vgp-chip-remove",
32581
32629
  onClick: () => setLastFrame(null),
32582
32630
  disabled: isGenerating,
32583
- children: /* @__PURE__ */ jsx98(X3, { size: 9 })
32631
+ children: /* @__PURE__ */ jsx98(X4, { size: 9 })
32584
32632
  }
32585
32633
  )
32586
32634
  ] }) : /* @__PURE__ */ jsxs55("div", { className: "chip-slot", children: [
@@ -32634,7 +32682,7 @@ var VideoGeneratorPanel = ({
32634
32682
  (prev) => prev.filter((_, j) => j !== i)
32635
32683
  ),
32636
32684
  disabled: isGenerating,
32637
- children: /* @__PURE__ */ jsx98(X3, { size: 9 })
32685
+ children: /* @__PURE__ */ jsx98(X4, { size: 9 })
32638
32686
  }
32639
32687
  )
32640
32688
  ]
@@ -57227,7 +57275,7 @@ var polyfill_default = polyfill;
57227
57275
 
57228
57276
  // components/StockImagePanel.tsx
57229
57277
  import { useCallback as useCallback33, useEffect as useEffect67, useRef as useRef59, useState as useState59 } from "react";
57230
- import { Loader2 as Loader22, Search, X as X4 } from "lucide-react";
57278
+ import { Loader2 as Loader22, Search, X as X5 } from "lucide-react";
57231
57279
  import { nanoid as nanoid3 } from "nanoid";
57232
57280
  import { newImageElement as newImageElement3 } from "@orangecatai/element";
57233
57281
  import { Fragment as Fragment40, jsx as jsx190, jsxs as jsxs110 } from "react/jsx-runtime";
@@ -57595,7 +57643,7 @@ var StockImagePanel = ({
57595
57643
  return /* @__PURE__ */ jsx190("div", { className: "sip", children: /* @__PURE__ */ jsxs110("div", { className: "sip-panel", children: [
57596
57644
  /* @__PURE__ */ jsxs110("div", { className: "sip-header", children: [
57597
57645
  /* @__PURE__ */ jsx190("span", { className: "sip-title", children: "Images" }),
57598
- /* @__PURE__ */ jsx190("button", { className: "sip-icon-btn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx190(X4, { size: 15 }) })
57646
+ /* @__PURE__ */ jsx190("button", { className: "sip-icon-btn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx190(X5, { size: 15 }) })
57599
57647
  ] }),
57600
57648
  /* @__PURE__ */ jsxs110("div", { className: "sip-tabs", children: [
57601
57649
  /* @__PURE__ */ jsxs110(
@@ -57670,7 +57718,7 @@ var StockImagePanel = ({
57670
57718
  className: "sip-search-clear",
57671
57719
  onClick: handleClearQuery,
57672
57720
  "aria-label": "Clear search",
57673
- children: /* @__PURE__ */ jsx190(X4, { size: 11 })
57721
+ children: /* @__PURE__ */ jsx190(X5, { size: 11 })
57674
57722
  }
57675
57723
  )
57676
57724
  ] }),
@@ -57748,7 +57796,7 @@ var StockImagePanel = ({
57748
57796
  className: "sip-search-clear",
57749
57797
  onClick: handleLibraryClearQuery,
57750
57798
  "aria-label": "Clear search",
57751
- children: /* @__PURE__ */ jsx190(X4, { size: 11 })
57799
+ children: /* @__PURE__ */ jsx190(X5, { size: 11 })
57752
57800
  }
57753
57801
  )
57754
57802
  ] }),
@@ -58138,7 +58186,7 @@ import {
58138
58186
  Plus,
58139
58187
  Search as Search2,
58140
58188
  Wrench,
58141
- X as X5
58189
+ X as X6
58142
58190
  } from "lucide-react";
58143
58191
 
58144
58192
  // components/ai-chat/canvasTools.ts
@@ -63251,7 +63299,7 @@ var AIChatPanel = React70.forwardRef(
63251
63299
  children: /* @__PURE__ */ jsx199(Plus, { size: 15 })
63252
63300
  }
63253
63301
  ),
63254
- /* @__PURE__ */ jsx199("button", { className: "acp-icon-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsx199(X5, { size: 15 }) })
63302
+ /* @__PURE__ */ jsx199("button", { className: "acp-icon-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsx199(X6, { size: 15 }) })
63255
63303
  ] }),
63256
63304
  historyOpen && /* @__PURE__ */ jsxs112("div", { className: "acp-history-dropdown", children: [
63257
63305
  /* @__PURE__ */ jsxs112("div", { className: "acp-history-search", children: [
@@ -63435,7 +63483,7 @@ var AIChatPanel = React70.forwardRef(
63435
63483
  className: "acp-ref-remove",
63436
63484
  onClick: handleRemoveRef,
63437
63485
  title: "Remove reference",
63438
- children: /* @__PURE__ */ jsx199(X5, { size: 10 })
63486
+ children: /* @__PURE__ */ jsx199(X6, { size: 10 })
63439
63487
  }
63440
63488
  )
63441
63489
  ] }) }),
@@ -63455,7 +63503,7 @@ var AIChatPanel = React70.forwardRef(
63455
63503
  className: "acp-attachment-remove",
63456
63504
  onClick: () => handleRemoveFile(f.id),
63457
63505
  title: "Remove file",
63458
- children: /* @__PURE__ */ jsx199(X5, { size: 10 })
63506
+ children: /* @__PURE__ */ jsx199(X6, { size: 10 })
63459
63507
  }
63460
63508
  )
63461
63509
  ] }, f.id)) }),