@orangecatai/adgen-canvas 0.0.2 → 0.0.4

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 (52) hide show
  1. package/dist/dev/{chunk-UV7ECD7A.js → chunk-3FGOYDLK.js} +3 -3
  2. package/dist/dev/{chunk-X2WWSILD.js → chunk-IRIUFXMO.js} +2 -2
  3. package/dist/dev/data/{image-H4O52A73.js → image-HH4XNQRO.js} +3 -3
  4. package/dist/dev/index.css +880 -1
  5. package/dist/dev/index.css.map +3 -3
  6. package/dist/dev/index.js +2970 -232
  7. package/dist/dev/index.js.map +4 -4
  8. package/dist/dev/subset-shared.chunk.js +1 -1
  9. package/dist/dev/subset-worker.chunk.js +1 -1
  10. package/dist/prod/{chunk-DOJFO2UO.js → chunk-LUZI7MFZ.js} +2 -2
  11. package/dist/prod/{chunk-O3WJMHIE.js → chunk-SG4RCQVC.js} +1 -1
  12. package/dist/prod/data/image-J2ZJZU4A.js +1 -0
  13. package/dist/prod/index.css +1 -1
  14. package/dist/prod/index.js +84 -29
  15. package/dist/prod/subset-shared.chunk.js +1 -1
  16. package/dist/prod/subset-worker.chunk.js +1 -1
  17. package/dist/types/excalidraw/actions/actionAddToLibrary.d.ts +1 -1
  18. package/dist/types/excalidraw/actions/actionCanvas.d.ts +177 -0
  19. package/dist/types/excalidraw/actions/actionClipboard.d.ts +7 -7
  20. package/dist/types/excalidraw/actions/actionCropEditor.d.ts +2 -2
  21. package/dist/types/excalidraw/actions/actionDuplicateSelection.d.ts +2 -2
  22. package/dist/types/excalidraw/actions/actionElementLink.d.ts +6 -6
  23. package/dist/types/excalidraw/actions/actionEmbeddable.d.ts +1 -1
  24. package/dist/types/excalidraw/actions/actionLinearEditor.d.ts +7 -7
  25. package/dist/types/excalidraw/actions/actionLink.d.ts +3 -3
  26. package/dist/types/excalidraw/actions/actionMenu.d.ts +1 -1
  27. package/dist/types/excalidraw/actions/actionSelectAll.d.ts +4 -4
  28. package/dist/types/excalidraw/actions/actionStyles.d.ts +2 -2
  29. package/dist/types/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +4 -4
  30. package/dist/types/excalidraw/actions/actionToggleShapeSwitch.d.ts +3 -3
  31. package/dist/types/excalidraw/actions/actionToggleStats.d.ts +2 -2
  32. package/dist/types/excalidraw/actions/actionToggleViewMode.d.ts +4 -4
  33. package/dist/types/excalidraw/actions/actionToggleZenMode.d.ts +4 -4
  34. package/dist/types/excalidraw/actions/actionZindex.d.ts +8 -8
  35. package/dist/types/excalidraw/actions/index.d.ts +1 -1
  36. package/dist/types/excalidraw/actions/types.d.ts +1 -1
  37. package/dist/types/excalidraw/components/AIChatPanel.d.ts +49 -3
  38. package/dist/types/excalidraw/components/Actions.d.ts +3 -0
  39. package/dist/types/excalidraw/components/DefaultSidebar.d.ts +1 -1
  40. package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +6 -1
  41. package/dist/types/excalidraw/components/ImageQuickEditPanel.d.ts +6 -1
  42. package/dist/types/excalidraw/components/Sidebar/Sidebar.d.ts +2 -2
  43. package/dist/types/excalidraw/components/ai-chat/agentLoop.d.ts +13 -0
  44. package/dist/types/excalidraw/components/ai-chat/audioUtils.d.ts +10 -0
  45. package/dist/types/excalidraw/index.d.ts +5 -0
  46. package/dist/types/excalidraw/types.d.ts +14 -0
  47. package/dist/types/excalidraw/utils/openRouterApiKey.d.ts +1 -0
  48. package/package.json +1 -1
  49. package/dist/prod/data/image-43FV5SMF.js +0 -1
  50. /package/dist/dev/{chunk-UV7ECD7A.js.map → chunk-3FGOYDLK.js.map} +0 -0
  51. /package/dist/dev/{chunk-X2WWSILD.js.map → chunk-IRIUFXMO.js.map} +0 -0
  52. /package/dist/dev/data/{image-H4O52A73.js.map → image-HH4XNQRO.js.map} +0 -0
package/dist/dev/index.js CHANGED
@@ -66,10 +66,10 @@ import {
66
66
  serializeAsJSON,
67
67
  serializeLibraryAsJSON,
68
68
  strokeRectWithRotation_simple
69
- } from "./chunk-UV7ECD7A.js";
69
+ } from "./chunk-3FGOYDLK.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-X2WWSILD.js";
72
+ } from "./chunk-IRIUFXMO.js";
73
73
  import {
74
74
  en_default
75
75
  } from "./chunk-IFMURN5W.js";
@@ -83,7 +83,7 @@ import {
83
83
  } from "./chunk-XDFCUUT6.js";
84
84
 
85
85
  // index.tsx
86
- import React54, { useEffect as useEffect54 } from "react";
86
+ import React58, { useEffect as useEffect56 } from "react";
87
87
  import { DEFAULT_UI_OPTIONS, isShallowEqual as isShallowEqual10 } from "@orangecatai/common";
88
88
 
89
89
  // components/App.tsx
@@ -158,7 +158,7 @@ import {
158
158
  isWritableElement as isWritableElement5,
159
159
  sceneCoordsToViewportCoords as sceneCoordsToViewportCoords10,
160
160
  tupleToCoors,
161
- viewportCoordsToSceneCoords as viewportCoordsToSceneCoords3,
161
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords4,
162
162
  wrapEvent as wrapEvent2,
163
163
  updateObject as updateObject2,
164
164
  updateActiveTool as updateActiveTool7,
@@ -197,7 +197,7 @@ import {
197
197
  import {
198
198
  getObservedAppState,
199
199
  getCommonBounds as getCommonBounds11,
200
- getElementAbsoluteCoords as getElementAbsoluteCoords11,
200
+ getElementAbsoluteCoords as getElementAbsoluteCoords12,
201
201
  bindOrUnbindBindingElements as bindOrUnbindBindingElements2,
202
202
  fixBindingsAfterDeletion as fixBindingsAfterDeletion2,
203
203
  getHoveredElementForBinding as getHoveredElementForBinding2,
@@ -6242,20 +6242,20 @@ var changeFontSize = (elements, appState, app, getNewFontSize, fallbackValue) =>
6242
6242
  if (isTextElement2(oldElement)) {
6243
6243
  const newFontSize = getNewFontSize(oldElement);
6244
6244
  newFontSizes.add(newFontSize);
6245
- let newElement5 = newElementWith2(oldElement, {
6245
+ let newElement6 = newElementWith2(oldElement, {
6246
6246
  fontSize: newFontSize
6247
6247
  });
6248
6248
  redrawTextBoundingBox(
6249
- newElement5,
6249
+ newElement6,
6250
6250
  app.scene.getContainerElement(oldElement),
6251
6251
  app.scene
6252
6252
  );
6253
- newElement5 = offsetElementAfterFontResize(
6253
+ newElement6 = offsetElementAfterFontResize(
6254
6254
  oldElement,
6255
- newElement5,
6255
+ newElement6,
6256
6256
  app.scene
6257
6257
  );
6258
- return newElement5;
6258
+ return newElement6;
6259
6259
  }
6260
6260
  return oldElement;
6261
6261
  },
@@ -6794,10 +6794,10 @@ var actionChangeFontFamily = register({
6794
6794
  (element) => {
6795
6795
  const cachedElement = cachedElements?.get(element.id);
6796
6796
  if (cachedElement) {
6797
- const newElement5 = newElementWith2(element, {
6797
+ const newElement6 = newElementWith2(element, {
6798
6798
  ...cachedElement
6799
6799
  });
6800
- return newElement5;
6800
+ return newElement6;
6801
6801
  }
6802
6802
  return element;
6803
6803
  },
@@ -6865,7 +6865,7 @@ var actionChangeFontFamily = register({
6865
6865
  appState,
6866
6866
  (oldElement) => {
6867
6867
  if (isTextElement2(oldElement) && (oldElement.fontFamily !== nextFontFamily || currentItemFontFamily)) {
6868
- const newElement5 = newElementWith2(
6868
+ const newElement6 = newElementWith2(
6869
6869
  oldElement,
6870
6870
  {
6871
6871
  fontFamily: nextFontFamily,
@@ -6880,11 +6880,11 @@ var actionChangeFontFamily = register({
6880
6880
  if (!skipFontFaceCheck) {
6881
6881
  uniqueChars = /* @__PURE__ */ new Set([
6882
6882
  ...uniqueChars,
6883
- ...Array.from(newElement5.originalText)
6883
+ ...Array.from(newElement6.originalText)
6884
6884
  ]);
6885
6885
  }
6886
- elementContainerMapping.set(newElement5, container);
6887
- return newElement5;
6886
+ elementContainerMapping.set(newElement6, container);
6887
+ return newElement6;
6888
6888
  }
6889
6889
  return oldElement;
6890
6890
  },
@@ -7070,16 +7070,16 @@ var actionChangeTextAlign = register({
7070
7070
  appState,
7071
7071
  (oldElement) => {
7072
7072
  if (isTextElement2(oldElement)) {
7073
- const newElement5 = newElementWith2(
7073
+ const newElement6 = newElementWith2(
7074
7074
  oldElement,
7075
7075
  { textAlign: value }
7076
7076
  );
7077
7077
  redrawTextBoundingBox(
7078
- newElement5,
7078
+ newElement6,
7079
7079
  app.scene.getContainerElement(oldElement),
7080
7080
  app.scene
7081
7081
  );
7082
- return newElement5;
7082
+ return newElement6;
7083
7083
  }
7084
7084
  return oldElement;
7085
7085
  },
@@ -7164,16 +7164,16 @@ var actionChangeVerticalAlign = register({
7164
7164
  appState,
7165
7165
  (oldElement) => {
7166
7166
  if (isTextElement2(oldElement)) {
7167
- const newElement5 = newElementWith2(
7167
+ const newElement6 = newElementWith2(
7168
7168
  oldElement,
7169
7169
  { verticalAlign: value }
7170
7170
  );
7171
7171
  redrawTextBoundingBox(
7172
- newElement5,
7172
+ newElement6,
7173
7173
  app.scene.getContainerElement(oldElement),
7174
7174
  app.scene
7175
7175
  );
7176
- return newElement5;
7176
+ return newElement6;
7177
7177
  }
7178
7178
  return oldElement;
7179
7179
  },
@@ -7493,7 +7493,7 @@ var actionChangeArrowType = register({
7493
7493
  -1,
7494
7494
  elementsMap
7495
7495
  );
7496
- let newElement5 = newElementWith2(el, {
7496
+ let newElement6 = newElementWith2(el, {
7497
7497
  x: value === ARROW_TYPE.elbow ? startPoint[0] : el.x,
7498
7498
  y: value === ARROW_TYPE.elbow ? startPoint[1] : el.y,
7499
7499
  roundness: value === ARROW_TYPE.round ? {
@@ -7524,53 +7524,53 @@ var actionChangeArrowType = register({
7524
7524
  )
7525
7525
  ] : el.points
7526
7526
  });
7527
- if (isElbowArrow2(newElement5)) {
7528
- newElement5.fixedSegments = null;
7527
+ if (isElbowArrow2(newElement6)) {
7528
+ newElement6.fixedSegments = null;
7529
7529
  const elementsMap2 = app.scene.getNonDeletedElementsMap();
7530
7530
  app.dismissLinearEditor();
7531
7531
  const startGlobalPoint = LinearElementEditor4.getPointAtIndexGlobalCoordinates(
7532
- newElement5,
7532
+ newElement6,
7533
7533
  0,
7534
7534
  elementsMap2
7535
7535
  );
7536
7536
  const endGlobalPoint = LinearElementEditor4.getPointAtIndexGlobalCoordinates(
7537
- newElement5,
7537
+ newElement6,
7538
7538
  -1,
7539
7539
  elementsMap2
7540
7540
  );
7541
- const startElement = newElement5.startBinding && elementsMap2.get(
7542
- newElement5.startBinding.elementId
7541
+ const startElement = newElement6.startBinding && elementsMap2.get(
7542
+ newElement6.startBinding.elementId
7543
7543
  );
7544
- const endElement = newElement5.endBinding && elementsMap2.get(
7545
- newElement5.endBinding.elementId
7544
+ const endElement = newElement6.endBinding && elementsMap2.get(
7545
+ newElement6.endBinding.elementId
7546
7546
  );
7547
- const startBinding = startElement && newElement5.startBinding ? {
7547
+ const startBinding = startElement && newElement6.startBinding ? {
7548
7548
  // @ts-ignore TS cannot discern check above
7549
- ...newElement5.startBinding,
7549
+ ...newElement6.startBinding,
7550
7550
  ...calculateFixedPointForElbowArrowBinding(
7551
- newElement5,
7551
+ newElement6,
7552
7552
  startElement,
7553
7553
  "start",
7554
7554
  elementsMap2
7555
7555
  )
7556
7556
  } : null;
7557
- const endBinding = endElement && newElement5.endBinding ? {
7557
+ const endBinding = endElement && newElement6.endBinding ? {
7558
7558
  // @ts-ignore TS cannot discern check above
7559
- ...newElement5.endBinding,
7559
+ ...newElement6.endBinding,
7560
7560
  ...calculateFixedPointForElbowArrowBinding(
7561
- newElement5,
7561
+ newElement6,
7562
7562
  endElement,
7563
7563
  "end",
7564
7564
  elementsMap2
7565
7565
  )
7566
7566
  } : null;
7567
- newElement5 = {
7568
- ...newElement5,
7567
+ newElement6 = {
7568
+ ...newElement6,
7569
7569
  startBinding,
7570
7570
  endBinding,
7571
- ...updateElbowArrowPoints(newElement5, elementsMap2, {
7571
+ ...updateElbowArrowPoints(newElement6, elementsMap2, {
7572
7572
  points: [startGlobalPoint, endGlobalPoint].map(
7573
- (p) => pointFrom(p[0] - newElement5.x, p[1] - newElement5.y)
7573
+ (p) => pointFrom(p[0] - newElement6.x, p[1] - newElement6.y)
7574
7574
  ),
7575
7575
  startBinding,
7576
7576
  endBinding,
@@ -7579,13 +7579,13 @@ var actionChangeArrowType = register({
7579
7579
  };
7580
7580
  } else {
7581
7581
  const elementsMap2 = app.scene.getNonDeletedElementsMap();
7582
- if (newElement5.startBinding) {
7582
+ if (newElement6.startBinding) {
7583
7583
  const startElement = elementsMap2.get(
7584
- newElement5.startBinding.elementId
7584
+ newElement6.startBinding.elementId
7585
7585
  );
7586
7586
  if (startElement) {
7587
7587
  bindBindingElement(
7588
- newElement5,
7588
+ newElement6,
7589
7589
  startElement,
7590
7590
  appState.bindMode === "inside" ? "inside" : "orbit",
7591
7591
  "start",
@@ -7593,13 +7593,13 @@ var actionChangeArrowType = register({
7593
7593
  );
7594
7594
  }
7595
7595
  }
7596
- if (newElement5.endBinding) {
7596
+ if (newElement6.endBinding) {
7597
7597
  const endElement = elementsMap2.get(
7598
- newElement5.endBinding.elementId
7598
+ newElement6.endBinding.elementId
7599
7599
  );
7600
7600
  if (endElement) {
7601
7601
  bindBindingElement(
7602
- newElement5,
7602
+ newElement6,
7603
7603
  endElement,
7604
7604
  appState.bindMode === "inside" ? "inside" : "orbit",
7605
7605
  "end",
@@ -7608,7 +7608,7 @@ var actionChangeArrowType = register({
7608
7608
  }
7609
7609
  }
7610
7610
  }
7611
- return newElement5;
7611
+ return newElement6;
7612
7612
  });
7613
7613
  const newState = {
7614
7614
  ...appState,
@@ -7687,9 +7687,14 @@ import {
7687
7687
  ZOOM_STEP,
7688
7688
  updateActiveTool as updateActiveTool2,
7689
7689
  CODES as CODES2,
7690
- KEYS as KEYS12
7690
+ KEYS as KEYS12,
7691
+ viewportCoordsToSceneCoords
7691
7692
  } from "@orangecatai/common";
7692
- import { getNonDeletedElements as getNonDeletedElements5 } from "@orangecatai/element";
7693
+ import {
7694
+ getElementAbsoluteCoords,
7695
+ getNonDeletedElements as getNonDeletedElements5,
7696
+ getVisibleElements
7697
+ } from "@orangecatai/element";
7693
7698
  import { newElementWith as newElementWith3 } from "@orangecatai/element";
7694
7699
  import { getCommonBounds } from "@orangecatai/element";
7695
7700
  import { CaptureUpdateAction as CaptureUpdateAction6 } from "@orangecatai/element";
@@ -8070,6 +8075,77 @@ var zoomValueToFitBoundsOnViewport = (bounds, viewportDimensions, viewportZoomFa
8070
8075
  const adjustedZoomValue = smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
8071
8076
  return Math.min(adjustedZoomValue, 1);
8072
8077
  };
8078
+ var COMFORTABLE_VIEWPORT_ZOOM_FACTOR = 0.8;
8079
+ var intersectsBounds = (a, b) => a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
8080
+ var expandBounds = (bounds, margin) => [
8081
+ bounds[0] - margin,
8082
+ bounds[1] - margin,
8083
+ bounds[2] + margin,
8084
+ bounds[3] + margin
8085
+ ];
8086
+ var getBoundsCenterDistance = (bounds, point) => {
8087
+ const centerX = (bounds[0] + bounds[2]) / 2;
8088
+ const centerY = (bounds[1] + bounds[3]) / 2;
8089
+ return Math.hypot(centerX - point.x, centerY - point.y);
8090
+ };
8091
+ var toSceneBounds = (bounds) => [bounds[0], bounds[1], bounds[2], bounds[3]];
8092
+ var getNearestContentCluster = (elements, appState) => {
8093
+ const nonDeletedElements = getNonDeletedElements5(elements);
8094
+ if (!nonDeletedElements.length) {
8095
+ return [];
8096
+ }
8097
+ const candidateElements = getVisibleElements(nonDeletedElements);
8098
+ const clusterCandidates = candidateElements.length ? candidateElements : nonDeletedElements;
8099
+ const elementsMap = new Map(
8100
+ nonDeletedElements.map((element) => [element.id, element])
8101
+ );
8102
+ const viewportCenter = viewportCoordsToSceneCoords(
8103
+ {
8104
+ clientX: appState.offsetLeft + appState.width / 2,
8105
+ clientY: appState.offsetTop + appState.height / 2
8106
+ },
8107
+ appState
8108
+ );
8109
+ const anchor = clusterCandidates.reduce((closest, element) => {
8110
+ const bounds = toSceneBounds(
8111
+ getElementAbsoluteCoords(element, elementsMap)
8112
+ );
8113
+ if (!closest) {
8114
+ return { element, bounds };
8115
+ }
8116
+ return getBoundsCenterDistance(bounds, viewportCenter) < getBoundsCenterDistance(closest.bounds, viewportCenter) ? { element, bounds } : closest;
8117
+ }, null);
8118
+ if (!anchor) {
8119
+ return [];
8120
+ }
8121
+ const proximityMargin = Math.max(
8122
+ 160 / appState.zoom.value,
8123
+ 0.12 * Math.min(appState.width, appState.height) / appState.zoom.value
8124
+ );
8125
+ const cluster = /* @__PURE__ */ new Map([
8126
+ [anchor.element.id, anchor.element]
8127
+ ]);
8128
+ let clusterBounds = anchor.bounds;
8129
+ let changed = true;
8130
+ while (changed) {
8131
+ changed = false;
8132
+ const expandedBounds = expandBounds(clusterBounds, proximityMargin);
8133
+ for (const element of clusterCandidates) {
8134
+ if (cluster.has(element.id)) {
8135
+ continue;
8136
+ }
8137
+ const elementBounds = toSceneBounds(
8138
+ getElementAbsoluteCoords(element, elementsMap)
8139
+ );
8140
+ if (intersectsBounds(elementBounds, expandedBounds)) {
8141
+ cluster.set(element.id, element);
8142
+ clusterBounds = getCommonBounds([...cluster.values()]);
8143
+ changed = true;
8144
+ }
8145
+ }
8146
+ }
8147
+ return [...cluster.values()];
8148
+ };
8073
8149
  var zoomToFitBounds = ({
8074
8150
  bounds,
8075
8151
  appState,
@@ -8207,6 +8283,50 @@ var actionZoomToFit = register({
8207
8283
  }),
8208
8284
  keyTest: (event) => event.code === CODES2.ONE && event.shiftKey && !event.altKey && !event[KEYS12.CTRL_OR_CMD]
8209
8285
  });
8286
+ var actionScrollBackToContent = register({
8287
+ name: "scrollBackToContent",
8288
+ label: "buttons.scrollBackToContent",
8289
+ icon: zoomAreaIcon,
8290
+ viewMode: true,
8291
+ trackEvent: { category: "canvas" },
8292
+ perform: (elements, appState, _, app) => {
8293
+ const selectedElements = app.scene.getSelectedElements(appState);
8294
+ const targetElements = selectedElements.length ? selectedElements : getNearestContentCluster(elements, appState);
8295
+ if (!targetElements.length) {
8296
+ return false;
8297
+ }
8298
+ return zoomToFit({
8299
+ targetElements,
8300
+ appState: {
8301
+ ...appState,
8302
+ userToFollow: null
8303
+ },
8304
+ fitToViewport: true,
8305
+ viewportZoomFactor: COMFORTABLE_VIEWPORT_ZOOM_FACTOR,
8306
+ canvasOffsets: app.getEditorUIOffsets()
8307
+ });
8308
+ },
8309
+ PanelComponent: ({ updateData }) => /* @__PURE__ */ jsx36(
8310
+ Tooltip,
8311
+ {
8312
+ label: t("buttons.scrollBackToContent"),
8313
+ style: { height: "100%" },
8314
+ children: /* @__PURE__ */ jsx36(
8315
+ ToolButton,
8316
+ {
8317
+ type: "button",
8318
+ className: "scroll-back-to-content-button zoom-button",
8319
+ icon: zoomAreaIcon,
8320
+ title: t("buttons.scrollBackToContent"),
8321
+ "aria-label": t("buttons.scrollBackToContent"),
8322
+ onClick: () => {
8323
+ updateData(null);
8324
+ }
8325
+ }
8326
+ )
8327
+ }
8328
+ )
8329
+ });
8210
8330
  var actionToggleTheme = register({
8211
8331
  name: "toggleTheme",
8212
8332
  label: (_, appState) => {
@@ -9626,7 +9746,7 @@ var exportCanvas = async (type, elements, appState, files, {
9626
9746
  let blob = canvasToBlob(tempCanvas);
9627
9747
  if (appState.exportEmbedScene) {
9628
9748
  blob = blob.then(
9629
- (blob2) => import("./data/image-H4O52A73.js").then(
9749
+ (blob2) => import("./data/image-HH4XNQRO.js").then(
9630
9750
  ({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
9631
9751
  blob: blob2,
9632
9752
  metadata: serializeAsJSON(elements, appState, files, "local")
@@ -10038,7 +10158,7 @@ var actionPasteStyles = register({
10038
10158
  if (!elementStylesToCopyFrom) {
10039
10159
  return element;
10040
10160
  }
10041
- let newElement5 = newElementWith5(element, {
10161
+ let newElement6 = newElementWith5(element, {
10042
10162
  backgroundColor: elementStylesToCopyFrom?.backgroundColor,
10043
10163
  strokeWidth: elementStylesToCopyFrom?.strokeWidth,
10044
10164
  strokeColor: elementStylesToCopyFrom?.strokeColor,
@@ -10051,36 +10171,36 @@ var actionPasteStyles = register({
10051
10171
  element
10052
10172
  ) ? elementStylesToCopyFrom.roundness : getDefaultRoundnessTypeForElement(element) : null
10053
10173
  });
10054
- if (isTextElement3(newElement5)) {
10174
+ if (isTextElement3(newElement6)) {
10055
10175
  const fontSize = elementStylesToCopyFrom.fontSize || DEFAULT_FONT_SIZE3;
10056
10176
  const fontFamily = elementStylesToCopyFrom.fontFamily || DEFAULT_FONT_FAMILY3;
10057
- newElement5 = newElementWith5(newElement5, {
10177
+ newElement6 = newElementWith5(newElement6, {
10058
10178
  fontSize,
10059
10179
  fontFamily,
10060
10180
  textAlign: elementStylesToCopyFrom.textAlign || DEFAULT_TEXT_ALIGN,
10061
10181
  lineHeight: elementStylesToCopyFrom.lineHeight || getLineHeight2(fontFamily)
10062
10182
  });
10063
10183
  let container = null;
10064
- if (newElement5.containerId) {
10184
+ if (newElement6.containerId) {
10065
10185
  container = selectedElements.find(
10066
- (element2) => isTextElement3(newElement5) && element2.id === newElement5.containerId
10186
+ (element2) => isTextElement3(newElement6) && element2.id === newElement6.containerId
10067
10187
  ) || null;
10068
10188
  }
10069
- redrawTextBoundingBox2(newElement5, container, app.scene);
10189
+ redrawTextBoundingBox2(newElement6, container, app.scene);
10070
10190
  }
10071
- if (newElement5.type === "arrow" && isArrowElement2(elementStylesToCopyFrom)) {
10072
- newElement5 = newElementWith5(newElement5, {
10191
+ if (newElement6.type === "arrow" && isArrowElement2(elementStylesToCopyFrom)) {
10192
+ newElement6 = newElementWith5(newElement6, {
10073
10193
  startArrowhead: elementStylesToCopyFrom.startArrowhead,
10074
10194
  endArrowhead: elementStylesToCopyFrom.endArrowhead
10075
10195
  });
10076
10196
  }
10077
10197
  if (isFrameLikeElement4(element)) {
10078
- newElement5 = newElementWith5(newElement5, {
10198
+ newElement6 = newElementWith5(newElement6, {
10079
10199
  roundness: null,
10080
10200
  backgroundColor: "transparent"
10081
10201
  });
10082
10202
  }
10083
- return newElement5;
10203
+ return newElement6;
10084
10204
  }
10085
10205
  return element;
10086
10206
  }),
@@ -11863,13 +11983,13 @@ import {
11863
11983
  useState as useState10
11864
11984
  } from "react";
11865
11985
  import { EVENT as EVENT5, HYPERLINK_TOOLTIP_DELAY, KEYS as KEYS27 } from "@orangecatai/common";
11866
- import { getElementAbsoluteCoords } from "@orangecatai/element";
11986
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords2 } from "@orangecatai/element";
11867
11987
  import { hitElementBoundingBox } from "@orangecatai/element";
11868
11988
  import { isElementLink } from "@orangecatai/element";
11869
11989
  import { getEmbedLink, embeddableURLValidator } from "@orangecatai/element";
11870
11990
  import {
11871
11991
  sceneCoordsToViewportCoords,
11872
- viewportCoordsToSceneCoords,
11992
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords2,
11873
11993
  wrapEvent,
11874
11994
  isLocalLink,
11875
11995
  normalizeLink
@@ -12122,7 +12242,7 @@ var Hyperlink = ({
12122
12242
  );
12123
12243
  };
12124
12244
  var getCoordsForPopover = (element, appState, elementsMap) => {
12125
- const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
12245
+ const [x1, y1] = getElementAbsoluteCoords2(element, elementsMap);
12126
12246
  const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
12127
12247
  { sceneX: x1 + element.width / 2, sceneY: y1 },
12128
12248
  appState
@@ -12154,7 +12274,7 @@ var renderTooltip = (element, appState, elementsMap) => {
12154
12274
  tooltipDiv.classList.add("excalidraw-tooltip--visible");
12155
12275
  tooltipDiv.style.maxWidth = "20rem";
12156
12276
  tooltipDiv.textContent = isElementLink(element.link) ? t("labels.link.goToElement") : element.link;
12157
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
12277
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords2(element, elementsMap);
12158
12278
  const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
12159
12279
  [x1, y1, x2, y2],
12160
12280
  element.angle,
@@ -12187,7 +12307,7 @@ var hideHyperlinkToolip = () => {
12187
12307
  }
12188
12308
  };
12189
12309
  var shouldHideLinkPopup = (element, elementsMap, appState, [clientX, clientY]) => {
12190
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
12310
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords2(
12191
12311
  { clientX, clientY },
12192
12312
  appState
12193
12313
  );
@@ -12195,7 +12315,7 @@ var shouldHideLinkPopup = (element, elementsMap, appState, [clientX, clientY]) =
12195
12315
  if (hitElementBoundingBox(pointFrom4(sceneX, sceneY), element, elementsMap)) {
12196
12316
  return false;
12197
12317
  }
12198
- const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
12318
+ const [x1, y1, x2] = getElementAbsoluteCoords2(element, elementsMap);
12199
12319
  if (sceneX >= x1 && sceneX <= x2 && sceneY >= y1 - SPACE_BOTTOM && sceneY <= y1) {
12200
12320
  return false;
12201
12321
  }
@@ -12444,7 +12564,7 @@ import {
12444
12564
  } from "@orangecatai/element";
12445
12565
  import {
12446
12566
  getCommonBoundingBox as getCommonBoundingBox2,
12447
- getElementAbsoluteCoords as getElementAbsoluteCoords2
12567
+ getElementAbsoluteCoords as getElementAbsoluteCoords3
12448
12568
  } from "@orangecatai/element";
12449
12569
  import {
12450
12570
  getBoundTextElement as getBoundTextElement4,
@@ -12547,7 +12667,7 @@ var Panel = ({
12547
12667
  positionRef.current = newPositionRef;
12548
12668
  let bottomLeft;
12549
12669
  if (elements2.length === 1) {
12550
- const [x1, , , y2, cx, cy] = getElementAbsoluteCoords2(
12670
+ const [x1, , , y2, cx, cy] = getElementAbsoluteCoords3(
12551
12671
  elements2[0],
12552
12672
  app.scene.getNonDeletedElementsMap()
12553
12673
  );
@@ -17769,14 +17889,17 @@ var ShapesSwitcher = ({
17769
17889
  ] })
17770
17890
  ] });
17771
17891
  };
17892
+ var ScrollBackToContentAction = ({
17893
+ renderAction
17894
+ }) => renderAction("scrollBackToContent");
17772
17895
  var ZoomActions = ({
17773
17896
  renderAction,
17774
17897
  zoom
17775
- }) => /* @__PURE__ */ jsx75(Stack_default.Col, { gap: 1, className: CLASSES5.ZOOM_ACTIONS, children: /* @__PURE__ */ jsxs42(Stack_default.Row, { align: "center", children: [
17898
+ }) => /* @__PURE__ */ jsxs42(Stack_default.Row, { align: "center", className: CLASSES5.ZOOM_ACTIONS, children: [
17776
17899
  renderAction("zoomOut"),
17777
17900
  renderAction("resetZoom"),
17778
17901
  renderAction("zoomIn")
17779
- ] }) });
17902
+ ] });
17780
17903
  var UndoRedoActions = ({
17781
17904
  renderAction,
17782
17905
  className
@@ -18679,7 +18802,7 @@ import {
18679
18802
  vectorFromPoint,
18680
18803
  vectorScale
18681
18804
  } from "@orangecatai/math";
18682
- import { getElementAbsoluteCoords as getElementAbsoluteCoords3 } from "@orangecatai/element";
18805
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords4 } from "@orangecatai/element";
18683
18806
  var getCurvePathOps = (shape) => {
18684
18807
  if (!shape) {
18685
18808
  return [];
@@ -19011,7 +19134,7 @@ var FOCUS_POINT_SIZE = 10 / 1.5;
19011
19134
  // ../element/src/sizeHelpers.ts
19012
19135
  import {
19013
19136
  SHIFT_LOCKING_ANGLE,
19014
- viewportCoordsToSceneCoords as viewportCoordsToSceneCoords2
19137
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords3
19015
19138
  } from "@orangecatai/common";
19016
19139
  import {
19017
19140
  normalizeRadians,
@@ -20988,7 +21111,7 @@ import { TOOL_TYPE, KEYS as KEYS43 } from "@orangecatai/common";
20988
21111
  import {
20989
21112
  getCommonBounds as getCommonBounds4,
20990
21113
  getDraggedElementsBounds,
20991
- getElementAbsoluteCoords as getElementAbsoluteCoords5
21114
+ getElementAbsoluteCoords as getElementAbsoluteCoords6
20992
21115
  } from "@orangecatai/element";
20993
21116
  import { isBoundToContainer as isBoundToContainer6 } from "@orangecatai/element";
20994
21117
  import { getMaximumGroups } from "@orangecatai/element";
@@ -21051,7 +21174,7 @@ var getElementsCorners = (elements, elementsMap, {
21051
21174
  let result = [];
21052
21175
  if (elements.length === 1) {
21053
21176
  const element = elements[0];
21054
- let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords5(
21177
+ let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords6(
21055
21178
  element,
21056
21179
  elementsMap
21057
21180
  );
@@ -21838,8 +21961,8 @@ var snapResizingElements = (selectedElements, selectedOriginalElements, app, eve
21838
21961
  snapLines: pointSnapLines
21839
21962
  };
21840
21963
  };
21841
- var snapNewElement = (newElement5, app, event, origin, dragOffset, elementsMap) => {
21842
- if (!isSnappingEnabled({ event, selectedElements: [newElement5], app })) {
21964
+ var snapNewElement = (newElement6, app, event, origin, dragOffset, elementsMap) => {
21965
+ if (!isSnappingEnabled({ event, selectedElements: [newElement6], app })) {
21843
21966
  return {
21844
21967
  snapOffset: { x: 0, y: 0 },
21845
21968
  snapLines: []
@@ -21856,7 +21979,7 @@ var snapNewElement = (newElement5, app, event, origin, dragOffset, elementsMap)
21856
21979
  const nearestSnapsX = [];
21857
21980
  const nearestSnapsY = [];
21858
21981
  getPointSnaps(
21859
- [newElement5],
21982
+ [newElement6],
21860
21983
  selectionSnapPoints,
21861
21984
  app,
21862
21985
  event,
@@ -21872,12 +21995,12 @@ var snapNewElement = (newElement5, app, event, origin, dragOffset, elementsMap)
21872
21995
  minOffset.y = 0;
21873
21996
  nearestSnapsX.length = 0;
21874
21997
  nearestSnapsY.length = 0;
21875
- const corners = getElementsCorners([newElement5], elementsMap, {
21998
+ const corners = getElementsCorners([newElement6], elementsMap, {
21876
21999
  boundingBoxCorners: true,
21877
22000
  omitCenter: true
21878
22001
  });
21879
22002
  getPointSnaps(
21880
- [newElement5],
22003
+ [newElement6],
21881
22004
  corners,
21882
22005
  app,
21883
22006
  event,
@@ -22051,11 +22174,11 @@ var Renderer = class {
22051
22174
 
22052
22175
  // components/ElementCanvasButtons.tsx
22053
22176
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords3 } from "@orangecatai/common";
22054
- import { getElementAbsoluteCoords as getElementAbsoluteCoords6 } from "@orangecatai/element";
22177
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords7 } from "@orangecatai/element";
22055
22178
  import { jsx as jsx82 } from "react/jsx-runtime";
22056
22179
  var CONTAINER_PADDING = 5;
22057
22180
  var getContainerCoords2 = (element, appState, elementsMap) => {
22058
- const [x1, y1] = getElementAbsoluteCoords6(element, elementsMap);
22181
+ const [x1, y1] = getElementAbsoluteCoords7(element, elementsMap);
22059
22182
  const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords3(
22060
22183
  { sceneX: x1 + element.width, sceneY: y1 },
22061
22184
  appState
@@ -22092,7 +22215,7 @@ var ElementCanvasButtons = ({
22092
22215
  // components/FrameToolbar.tsx
22093
22216
  import { useCallback as useCallback14, useEffect as useEffect29, useRef as useRef24, useState as useState27 } from "react";
22094
22217
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords4 } from "@orangecatai/common";
22095
- import { getElementAbsoluteCoords as getElementAbsoluteCoords7 } from "@orangecatai/element";
22218
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords8 } from "@orangecatai/element";
22096
22219
  import { isFrameLikeElement as isFrameLikeElement9 } from "@orangecatai/element";
22097
22220
  import { Fragment as Fragment11, jsx as jsx83, jsxs as jsxs45 } from "react/jsx-runtime";
22098
22221
  var ASPECT_RATIOS = [
@@ -22289,7 +22412,7 @@ var FrameToolbar = ({
22289
22412
  Math.round(element.width),
22290
22413
  Math.round(element.height)
22291
22414
  );
22292
- const [x1, y1] = getElementAbsoluteCoords7(
22415
+ const [x1, y1] = getElementAbsoluteCoords8(
22293
22416
  element,
22294
22417
  app.scene.getNonDeletedElementsMap()
22295
22418
  );
@@ -22482,13 +22605,14 @@ import { useCallback as useCallback15, useEffect as useEffect30, useRef as useRe
22482
22605
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords5 } from "@orangecatai/common";
22483
22606
  import {
22484
22607
  CaptureUpdateAction as CaptureUpdateAction36,
22485
- getElementAbsoluteCoords as getElementAbsoluteCoords8
22608
+ getElementAbsoluteCoords as getElementAbsoluteCoords9
22486
22609
  } from "@orangecatai/element";
22487
22610
  import { ArrowUp, ImagePlus, X } from "lucide-react";
22488
22611
 
22489
22612
  // utils/geminiApiKey.ts
22490
22613
  function resolveGeminiApiKey(propKey) {
22491
- return propKey ?? (typeof import.meta !== "undefined" && define_import_meta_env_default?.VITE_APP_GEMINI_API_KEY ? define_import_meta_env_default.VITE_APP_GEMINI_API_KEY : "") ?? "";
22614
+ const normalizedPropKey = propKey?.trim();
22615
+ return (normalizedPropKey ? normalizedPropKey : void 0) ?? (typeof import.meta !== "undefined" && define_import_meta_env_default?.VITE_APP_GEMINI_API_KEY ? define_import_meta_env_default.VITE_APP_GEMINI_API_KEY : "") ?? "";
22492
22616
  }
22493
22617
 
22494
22618
  // components/image-generation/pendingGenerations.ts
@@ -23144,14 +23268,28 @@ var RatioShapeIcon = ({ label }) => {
23144
23268
  };
23145
23269
  var ImageGeneratorPanel = ({
23146
23270
  element,
23147
- app
23271
+ app,
23272
+ onBeforeImageGen,
23273
+ onAfterImageGen
23148
23274
  }) => {
23275
+ const getStoredSettings = (frame) => {
23276
+ const stored = frame.customData?.imageGenerator;
23277
+ if (!stored) {
23278
+ return null;
23279
+ }
23280
+ return {
23281
+ model: stored.model,
23282
+ ratio: stored.ratio,
23283
+ resolution: stored.resolution
23284
+ };
23285
+ };
23149
23286
  const appState = app.state;
23150
23287
  const resolvedApiKey = resolveGeminiApiKey(app.props.geminiApiKey);
23151
23288
  const [prompt, setPrompt] = useState29(() => getPendingPrompt(element.id));
23152
23289
  const [selectedModel, setSelectedModel] = useState29(() => {
23153
- const s = getPendingSettings(element.id);
23154
- return s?.model ?? "Gemini 3.1 Flash";
23290
+ const stored = getStoredSettings(element);
23291
+ const pending = getPendingSettings(element.id);
23292
+ return stored?.model ?? pending?.model ?? "Gemini 3.1 Flash";
23155
23293
  });
23156
23294
  const modelCfg = MODEL_CONFIG[selectedModel];
23157
23295
  const availableRatios = modelCfg.supportedRatios;
@@ -23159,15 +23297,39 @@ var ImageGeneratorPanel = ({
23159
23297
  const defaultRatio = availableRatios.includes("2:3") ? "2:3" : availableRatios[0];
23160
23298
  const defaultResolution = availableResolutions.includes("2K") ? "2K" : availableResolutions[availableResolutions.length - 1];
23161
23299
  const [selectedRatio, setSelectedRatio] = useState29(() => {
23162
- const s = getPendingSettings(element.id);
23163
- return s?.ratio ?? defaultRatio;
23300
+ const stored = getStoredSettings(element);
23301
+ const pending = getPendingSettings(element.id);
23302
+ return stored?.ratio ?? pending?.ratio ?? defaultRatio;
23164
23303
  });
23165
23304
  const [selectedResolution, setSelectedResolution] = useState29(
23166
23305
  () => {
23167
- const s = getPendingSettings(element.id);
23168
- return s?.resolution ?? defaultResolution;
23306
+ const stored = getStoredSettings(element);
23307
+ const pending = getPendingSettings(element.id);
23308
+ return stored?.resolution ?? pending?.resolution ?? defaultResolution;
23169
23309
  }
23170
23310
  );
23311
+ const persistFrameSettings = useCallback15(
23312
+ (settings) => {
23313
+ app.scene.mutateElement(element, {
23314
+ customData: {
23315
+ ...element.customData ?? {},
23316
+ imageGenerator: settings
23317
+ }
23318
+ });
23319
+ },
23320
+ [app.scene, element]
23321
+ );
23322
+ const fitFrameIntoView = useCallback15(
23323
+ (frame, animate = false) => {
23324
+ app.scrollToContent(frame, {
23325
+ fitToViewport: true,
23326
+ viewportZoomFactor: 0.8,
23327
+ animate,
23328
+ canvasOffsets: app.getEditorUIOffsets()
23329
+ });
23330
+ },
23331
+ [app]
23332
+ );
23171
23333
  const isInitialModelMount = useRef26(true);
23172
23334
  useEffect30(() => {
23173
23335
  if (isInitialModelMount.current) {
@@ -23196,8 +23358,13 @@ var ImageGeneratorPanel = ({
23196
23358
  width: dims.width,
23197
23359
  height: dims.height
23198
23360
  });
23361
+ persistFrameSettings({
23362
+ model: selectedModel,
23363
+ ratio: newRatio,
23364
+ resolution: newRes
23365
+ });
23199
23366
  app.syncActionResult({ captureUpdate: CaptureUpdateAction36.IMMEDIATELY });
23200
- }, [selectedModel]);
23367
+ }, [selectedModel, app, element, persistFrameSettings]);
23201
23368
  const [isGenerating, setIsGenerating] = useState29(
23202
23369
  () => hasPendingGeneration(element.id)
23203
23370
  );
@@ -23217,6 +23384,32 @@ var ImageGeneratorPanel = ({
23217
23384
  elementRef.current = element;
23218
23385
  appRef.current = app;
23219
23386
  });
23387
+ useEffect30(() => {
23388
+ if (!element.customData?.imageGeneratorAutoFitPending) {
23389
+ return;
23390
+ }
23391
+ let frameId = 0;
23392
+ let cancelled = false;
23393
+ const run = () => {
23394
+ if (cancelled) {
23395
+ return;
23396
+ }
23397
+ fitFrameIntoView(element);
23398
+ app.scene.mutateElement(element, {
23399
+ customData: {
23400
+ ...element.customData ?? {},
23401
+ imageGeneratorAutoFitPending: false
23402
+ }
23403
+ });
23404
+ };
23405
+ frameId = window.requestAnimationFrame(() => {
23406
+ frameId = window.requestAnimationFrame(run);
23407
+ });
23408
+ return () => {
23409
+ cancelled = true;
23410
+ window.cancelAnimationFrame(frameId);
23411
+ };
23412
+ }, [app.scene, element, fitFrameIntoView]);
23220
23413
  const applyRatio = useCallback15(
23221
23414
  (ratioLabel) => {
23222
23415
  setSelectedRatio(ratioLabel);
@@ -23224,7 +23417,15 @@ var ImageGeneratorPanel = ({
23224
23417
  const dims = getDimensions(selectedModel, ratioLabel, selectedResolution);
23225
23418
  appRef.current.scene.mutateElement(elementRef.current, {
23226
23419
  width: dims.width,
23227
- height: dims.height
23420
+ height: dims.height,
23421
+ customData: {
23422
+ ...elementRef.current.customData ?? {},
23423
+ imageGenerator: {
23424
+ model: selectedModel,
23425
+ ratio: ratioLabel,
23426
+ resolution: selectedResolution
23427
+ }
23428
+ }
23228
23429
  });
23229
23430
  appRef.current.syncActionResult({
23230
23431
  captureUpdate: CaptureUpdateAction36.IMMEDIATELY
@@ -23239,7 +23440,15 @@ var ImageGeneratorPanel = ({
23239
23440
  const dims = getDimensions(selectedModel, selectedRatio, res);
23240
23441
  appRef.current.scene.mutateElement(elementRef.current, {
23241
23442
  width: dims.width,
23242
- height: dims.height
23443
+ height: dims.height,
23444
+ customData: {
23445
+ ...elementRef.current.customData ?? {},
23446
+ imageGenerator: {
23447
+ model: selectedModel,
23448
+ ratio: selectedRatio,
23449
+ resolution: res
23450
+ }
23451
+ }
23243
23452
  });
23244
23453
  appRef.current.syncActionResult({
23245
23454
  captureUpdate: CaptureUpdateAction36.IMMEDIATELY
@@ -23301,6 +23510,20 @@ var ImageGeneratorPanel = ({
23301
23510
  );
23302
23511
  return;
23303
23512
  }
23513
+ if (onBeforeImageGen) {
23514
+ try {
23515
+ const { allowed, error } = await onBeforeImageGen();
23516
+ if (!allowed) {
23517
+ setGenerateError(
23518
+ error || "Insufficient credits for image generation"
23519
+ );
23520
+ return;
23521
+ }
23522
+ } catch {
23523
+ setGenerateError("Credit check failed");
23524
+ return;
23525
+ }
23526
+ }
23304
23527
  const controller = new AbortController();
23305
23528
  abortControllerRef.current = controller;
23306
23529
  startPendingGeneration(
@@ -23330,6 +23553,7 @@ var ImageGeneratorPanel = ({
23330
23553
  controller.signal
23331
23554
  );
23332
23555
  await app.insertGeneratedImageIntoFrame(imageDataUrl, element);
23556
+ onAfterImageGen?.();
23333
23557
  } catch (err) {
23334
23558
  if (err instanceof Error) {
23335
23559
  if (err.name === "AbortError") {
@@ -23359,12 +23583,14 @@ var ImageGeneratorPanel = ({
23359
23583
  selectedResolution,
23360
23584
  referenceImage,
23361
23585
  app,
23362
- element
23586
+ element,
23587
+ onBeforeImageGen,
23588
+ onAfterImageGen
23363
23589
  ]);
23364
23590
  if (appState.contextMenu || appState.newElement || appState.resizingElement || appState.isRotating || appState.openMenu || appState.viewModeEnabled || appState.selectedElementsAreBeingDragged) {
23365
23591
  return null;
23366
23592
  }
23367
- const [x1, , , y2] = getElementAbsoluteCoords8(
23593
+ const [x1, , , y2] = getElementAbsoluteCoords9(
23368
23594
  element,
23369
23595
  app.scene.getNonDeletedElementsMap()
23370
23596
  );
@@ -23440,6 +23666,11 @@ var ImageGeneratorPanel = ({
23440
23666
  {
23441
23667
  className: "igp-dropdown-item",
23442
23668
  onClick: () => {
23669
+ persistFrameSettings({
23670
+ model: m,
23671
+ ratio: selectedRatio,
23672
+ resolution: selectedResolution
23673
+ });
23443
23674
  setSelectedModel(m);
23444
23675
  setModelOpen(false);
23445
23676
  },
@@ -23568,7 +23799,7 @@ var ImageGeneratorPanel = ({
23568
23799
  // components/ImageQuickEditPanel.tsx
23569
23800
  import { useCallback as useCallback16, useEffect as useEffect31, useRef as useRef27, useState as useState30 } from "react";
23570
23801
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords6 } from "@orangecatai/common";
23571
- import { getElementAbsoluteCoords as getElementAbsoluteCoords9 } from "@orangecatai/element";
23802
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords10 } from "@orangecatai/element";
23572
23803
  import { ArrowUp as ArrowUp2 } from "lucide-react";
23573
23804
  import { Fragment as Fragment12, jsx as jsx88, jsxs as jsxs48 } from "react/jsx-runtime";
23574
23805
  var ChevronDownIcon3 = () => /* @__PURE__ */ jsx88("svg", { width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx88(
@@ -23671,7 +23902,9 @@ var RatioShapeIcon2 = ({ label }) => {
23671
23902
  };
23672
23903
  var ImageQuickEditPanel = ({
23673
23904
  element,
23674
- app
23905
+ app,
23906
+ onBeforeImageGen,
23907
+ onAfterImageGen
23675
23908
  }) => {
23676
23909
  const appState = app.state;
23677
23910
  const resolvedApiKey = resolveGeminiApiKey(app.props.geminiApiKey);
@@ -23770,6 +24003,20 @@ var ImageQuickEditPanel = ({
23770
24003
  );
23771
24004
  return;
23772
24005
  }
24006
+ if (onBeforeImageGen) {
24007
+ try {
24008
+ const { allowed, error } = await onBeforeImageGen();
24009
+ if (!allowed) {
24010
+ setGenerateError(
24011
+ error || "Insufficient credits for image generation"
24012
+ );
24013
+ return;
24014
+ }
24015
+ } catch {
24016
+ setGenerateError("Credit check failed");
24017
+ return;
24018
+ }
24019
+ }
23773
24020
  const controller = new AbortController();
23774
24021
  abortControllerRef.current = controller;
23775
24022
  startPendingGeneration(
@@ -23810,6 +24057,7 @@ var ImageQuickEditPanel = ({
23810
24057
  dims.width,
23811
24058
  dims.height
23812
24059
  );
24060
+ onAfterImageGen?.();
23813
24061
  } catch (err) {
23814
24062
  if (err instanceof Error) {
23815
24063
  if (err.name === "AbortError") {
@@ -23838,13 +24086,15 @@ var ImageQuickEditPanel = ({
23838
24086
  selectedRatio,
23839
24087
  selectedResolution,
23840
24088
  app,
23841
- element
24089
+ element,
24090
+ onBeforeImageGen,
24091
+ onAfterImageGen
23842
24092
  ]);
23843
24093
  if (appState.contextMenu || appState.newElement || appState.resizingElement || appState.isRotating || appState.openMenu || appState.viewModeEnabled || appState.selectedElementsAreBeingDragged) {
23844
24094
  return null;
23845
24095
  }
23846
24096
  const elementsMap = app.scene.getNonDeletedElementsMap();
23847
- const [x1, y1, , y2] = getElementAbsoluteCoords9(element, elementsMap);
24097
+ const [x1, y1, , y2] = getElementAbsoluteCoords10(element, elementsMap);
23848
24098
  const { x: triggerVpX, y: triggerVpY } = sceneCoordsToViewportCoords6(
23849
24099
  { sceneX: x1 + element.width / 2, sceneY: y1 },
23850
24100
  appState
@@ -26767,24 +27017,40 @@ var Footer = ({
26767
27017
  className: clsx47("layer-ui__wrapper__footer-left zen-mode-transition", {
26768
27018
  "layer-ui__wrapper__footer-left--transition-left": appState.zenModeEnabled
26769
27019
  }),
26770
- children: /* @__PURE__ */ jsx104(Stack_default.Col, { gap: 2, children: /* @__PURE__ */ jsxs60(Section, { heading: "canvasActions", children: [
27020
+ children: /* @__PURE__ */ jsx104(Stack_default.Col, { gap: 2, children: /* @__PURE__ */ jsx104(Section, { heading: "canvasActions", children: /* @__PURE__ */ jsxs60(Stack_default.Col, { gap: 1, className: "footer-canvas-actions", children: [
26771
27021
  /* @__PURE__ */ jsx104(
26772
- ZoomActions,
27022
+ ScrollBackToContentAction,
26773
27023
  {
26774
- renderAction: actionManager.renderAction,
26775
- zoom: appState.zoom
27024
+ renderAction: actionManager.renderAction
26776
27025
  }
26777
27026
  ),
26778
- !appState.viewModeEnabled && /* @__PURE__ */ jsx104(
26779
- UndoRedoActions,
27027
+ /* @__PURE__ */ jsxs60(
27028
+ Stack_default.Row,
26780
27029
  {
26781
- renderAction: actionManager.renderAction,
26782
- className: clsx47("zen-mode-transition", {
26783
- "layer-ui__wrapper__footer-left--transition-bottom": appState.zenModeEnabled
26784
- })
27030
+ align: "center",
27031
+ gap: 1,
27032
+ className: "footer-canvas-actions-row",
27033
+ children: [
27034
+ /* @__PURE__ */ jsx104(
27035
+ ZoomActions,
27036
+ {
27037
+ renderAction: actionManager.renderAction,
27038
+ zoom: appState.zoom
27039
+ }
27040
+ ),
27041
+ !appState.viewModeEnabled && /* @__PURE__ */ jsx104(
27042
+ UndoRedoActions,
27043
+ {
27044
+ renderAction: actionManager.renderAction,
27045
+ className: clsx47("zen-mode-transition", {
27046
+ "layer-ui__wrapper__footer-left--transition-bottom": appState.zenModeEnabled
27047
+ })
27048
+ }
27049
+ )
27050
+ ]
26785
27051
  }
26786
27052
  )
26787
- ] }) })
27053
+ ] }) }) })
26788
27054
  }
26789
27055
  ),
26790
27056
  /* @__PURE__ */ jsx104(FooterCenterTunnel.Out, {}),
@@ -35050,7 +35316,7 @@ import {
35050
35316
  } from "@orangecatai/element";
35051
35317
  import {
35052
35318
  getCommonBounds as getCommonBounds9,
35053
- getElementAbsoluteCoords as getElementAbsoluteCoords10
35319
+ getElementAbsoluteCoords as getElementAbsoluteCoords11
35054
35320
  } from "@orangecatai/element";
35055
35321
  import {
35056
35322
  getGlobalFixedPointForBindableElement as getGlobalFixedPointForBindableElement2,
@@ -35837,7 +36103,7 @@ var renderSelectionBorder = (context, appState, elementProperties) => {
35837
36103
  context.restore();
35838
36104
  };
35839
36105
  var renderFrameHighlight = (context, appState, frame, elementsMap) => {
35840
- const [x1, y1, x2, y2] = getElementAbsoluteCoords10(frame, elementsMap);
36106
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords11(frame, elementsMap);
35841
36107
  const width = x2 - x1;
35842
36108
  const height = y2 - y1;
35843
36109
  context.strokeStyle = "rgb(0,118,255)";
@@ -36098,7 +36364,7 @@ var renderTransformHandles = (context, renderConfig, appState, transformHandles,
36098
36364
  });
36099
36365
  };
36100
36366
  var renderCropHandles = (context, renderConfig, appState, croppingElement, elementsMap) => {
36101
- const [x1, y1, , , cx, cy] = getElementAbsoluteCoords10(
36367
+ const [x1, y1, , , cx, cy] = getElementAbsoluteCoords11(
36102
36368
  croppingElement,
36103
36369
  elementsMap
36104
36370
  );
@@ -36379,7 +36645,7 @@ var _renderInteractiveScene = ({
36379
36645
  }
36380
36646
  }
36381
36647
  if (selectionColors.length) {
36382
- const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords10(
36648
+ const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords11(
36383
36649
  element,
36384
36650
  elementsMap,
36385
36651
  true
@@ -36506,7 +36772,7 @@ var _renderInteractiveScene = ({
36506
36772
  appState.searchMatches?.matches.forEach(({ id, focus, matchedLines }) => {
36507
36773
  const element = elementsMap.get(id);
36508
36774
  if (element) {
36509
- const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords10(
36775
+ const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords11(
36510
36776
  element,
36511
36777
  elementsMap,
36512
36778
  true
@@ -36860,7 +37126,7 @@ import {
36860
37126
  var _renderNewElementScene = ({
36861
37127
  canvas,
36862
37128
  rc,
36863
- newElement: newElement5,
37129
+ newElement: newElement6,
36864
37130
  elementsMap,
36865
37131
  allElementsMap,
36866
37132
  scale,
@@ -36880,19 +37146,19 @@ var _renderNewElementScene = ({
36880
37146
  });
36881
37147
  context.save();
36882
37148
  context.scale(appState.zoom.value, appState.zoom.value);
36883
- if (newElement5 && newElement5.type !== "selection") {
36884
- if (isInvisiblySmallElement2(newElement5)) {
37149
+ if (newElement6 && newElement6.type !== "selection") {
37150
+ if (isInvisiblySmallElement2(newElement6)) {
36885
37151
  return;
36886
37152
  }
36887
- const frameId = newElement5.frameId || appState.frameToHighlight?.id;
37153
+ const frameId = newElement6.frameId || appState.frameToHighlight?.id;
36888
37154
  if (frameId && appState.frameRendering.enabled && appState.frameRendering.clip) {
36889
- const frame = getTargetFrame(newElement5, elementsMap, appState);
36890
- if (frame && shouldApplyFrameClip(newElement5, frame, appState, elementsMap)) {
37155
+ const frame = getTargetFrame(newElement6, elementsMap, appState);
37156
+ if (frame && shouldApplyFrameClip(newElement6, frame, appState, elementsMap)) {
36891
37157
  frameClip(frame, context, renderConfig, appState);
36892
37158
  }
36893
37159
  }
36894
37160
  renderElement(
36895
- newElement5,
37161
+ newElement6,
36896
37162
  elementsMap,
36897
37163
  allElementsMap,
36898
37164
  rc,
@@ -37275,11 +37541,11 @@ var App = class _App extends React52.Component {
37275
37541
  );
37276
37542
  if (frameNameDiv) {
37277
37543
  const box = frameNameDiv.getBoundingClientRect();
37278
- const boxSceneTopLeft = viewportCoordsToSceneCoords3(
37544
+ const boxSceneTopLeft = viewportCoordsToSceneCoords4(
37279
37545
  { clientX: box.x, clientY: box.y },
37280
37546
  this.state
37281
37547
  );
37282
- const boxSceneBottomRight = viewportCoordsToSceneCoords3(
37548
+ const boxSceneBottomRight = viewportCoordsToSceneCoords4(
37283
37549
  { clientX: box.right, clientY: box.bottom },
37284
37550
  this.state
37285
37551
  );
@@ -37569,7 +37835,10 @@ var App = class _App extends React52.Component {
37569
37835
  locked: false,
37570
37836
  backgroundColor: "#cce8f5",
37571
37837
  fillStyle: "solid",
37572
- customData: { type: "image-generator" }
37838
+ customData: {
37839
+ type: "image-generator",
37840
+ imageGeneratorAutoFitPending: true
37841
+ }
37573
37842
  });
37574
37843
  this.scene.insertElement(frame);
37575
37844
  this.setActiveTool({ type: "selection" });
@@ -37614,7 +37883,7 @@ var App = class _App extends React52.Component {
37614
37883
  const CLEARANCE = 25;
37615
37884
  const elementsMap = this.scene.getNonDeletedElementsMap();
37616
37885
  const allElements = this.scene.getNonDeletedElements().filter((el) => el.id !== sourceElement.id);
37617
- const [sx1, sy1, sx2, sy2] = getElementAbsoluteCoords11(
37886
+ const [sx1, sy1, sx2, sy2] = getElementAbsoluteCoords12(
37618
37887
  sourceElement,
37619
37888
  elementsMap
37620
37889
  );
@@ -37632,7 +37901,7 @@ var App = class _App extends React52.Component {
37632
37901
  const right = cRight + CLEARANCE;
37633
37902
  const bottom = cBottom + CLEARANCE;
37634
37903
  return allElements.some((el) => {
37635
- const [ex1, ey1, ex2, ey2] = getElementAbsoluteCoords11(el, elementsMap);
37904
+ const [ex1, ey1, ex2, ey2] = getElementAbsoluteCoords12(el, elementsMap);
37636
37905
  return ex1 < right && ex2 > left && ey1 < bottom && ey2 > top;
37637
37906
  });
37638
37907
  };
@@ -38118,7 +38387,7 @@ var App = class _App extends React52.Component {
38118
38387
  const elementsCenterY = distance2(minY, maxY) / 2;
38119
38388
  const clientX = typeof opts.position === "object" ? opts.position.clientX : opts.position === "cursor" ? this.lastViewportPosition.x : this.state.width / 2 + this.state.offsetLeft;
38120
38389
  const clientY = typeof opts.position === "object" ? opts.position.clientY : opts.position === "cursor" ? this.lastViewportPosition.y : this.state.height / 2 + this.state.offsetTop;
38121
- const { x, y } = viewportCoordsToSceneCoords3(
38390
+ const { x, y } = viewportCoordsToSceneCoords4(
38122
38391
  { clientX, clientY },
38123
38392
  this.state
38124
38393
  );
@@ -38157,13 +38426,13 @@ var App = class _App extends React52.Component {
38157
38426
  );
38158
38427
  }
38159
38428
  this.scene.replaceAllElements(nextElements);
38160
- duplicatedElements.forEach((newElement5) => {
38161
- if (isTextElement19(newElement5) && isBoundToContainer9(newElement5)) {
38429
+ duplicatedElements.forEach((newElement6) => {
38430
+ if (isTextElement19(newElement6) && isBoundToContainer9(newElement6)) {
38162
38431
  const container = getContainerElement5(
38163
- newElement5,
38432
+ newElement6,
38164
38433
  this.scene.getElementsMapIncludingDeleted()
38165
38434
  );
38166
- redrawTextBoundingBox8(newElement5, container, this.scene);
38435
+ redrawTextBoundingBox8(newElement6, container, this.scene);
38167
38436
  }
38168
38437
  });
38169
38438
  if (isSafari2) {
@@ -38970,7 +39239,7 @@ var App = class _App extends React52.Component {
38970
39239
  bindMode: "orbit"
38971
39240
  });
38972
39241
  if (this.lastPointerMoveEvent && getFeatureFlag4("COMPLEX_BINDINGS")) {
38973
- const scenePointer = viewportCoordsToSceneCoords3(
39242
+ const scenePointer = viewportCoordsToSceneCoords4(
38974
39243
  {
38975
39244
  clientX: this.lastPointerMoveEvent.clientX,
38976
39245
  clientY: this.lastPointerMoveEvent.clientY
@@ -39371,7 +39640,7 @@ var App = class _App extends React52.Component {
39371
39640
  return;
39372
39641
  }
39373
39642
  const selectedElements = this.scene.getSelectedElements(this.state);
39374
- let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
39643
+ let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
39375
39644
  event,
39376
39645
  this.state
39377
39646
  );
@@ -39536,7 +39805,7 @@ var App = class _App extends React52.Component {
39536
39805
  if (!this.hitLinkElement || draggedDistance > DRAGGING_THRESHOLD3) {
39537
39806
  return;
39538
39807
  }
39539
- const lastPointerDownCoords = viewportCoordsToSceneCoords3(
39808
+ const lastPointerDownCoords = viewportCoordsToSceneCoords4(
39540
39809
  this.lastPointerDownEvent,
39541
39810
  this.state
39542
39811
  );
@@ -39548,7 +39817,7 @@ var App = class _App extends React52.Component {
39548
39817
  pointFrom29(lastPointerDownCoords.x, lastPointerDownCoords.y),
39549
39818
  this.editorInterface.formFactor === "phone"
39550
39819
  );
39551
- const lastPointerUpCoords = viewportCoordsToSceneCoords3(
39820
+ const lastPointerUpCoords = viewportCoordsToSceneCoords4(
39552
39821
  this.lastPointerUpEvent,
39553
39822
  this.state
39554
39823
  );
@@ -39596,7 +39865,7 @@ var App = class _App extends React52.Component {
39596
39865
  __publicField(this, "handleCanvasPointerMove", (event) => {
39597
39866
  this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
39598
39867
  this.lastPointerMoveEvent = event.nativeEvent;
39599
- const scenePointer = viewportCoordsToSceneCoords3(event, this.state);
39868
+ const scenePointer = viewportCoordsToSceneCoords4(event, this.state);
39600
39869
  const { x: scenePointerX, y: scenePointerY } = scenePointer;
39601
39870
  this.lastPointerMoveCoords = {
39602
39871
  x: scenePointerX,
@@ -39705,8 +39974,8 @@ var App = class _App extends React52.Component {
39705
39974
  }
39706
39975
  }
39707
39976
  if (isBindingElementType(this.state.activeTool.type)) {
39708
- const { newElement: newElement5 } = this.state;
39709
- if (!newElement5 && isBindingEnabled2(this.state)) {
39977
+ const { newElement: newElement6 } = this.state;
39978
+ if (!newElement6 && isBindingEnabled2(this.state)) {
39710
39979
  const globalPoint = pointFrom29(
39711
39980
  scenePointerX,
39712
39981
  scenePointerY
@@ -40072,7 +40341,7 @@ var App = class _App extends React52.Component {
40072
40341
  invalidateContextMenu = true;
40073
40342
  });
40074
40343
  __publicField(this, "handleCanvasPointerDown", (event) => {
40075
- const scenePointer = viewportCoordsToSceneCoords3(event, this.state);
40344
+ const scenePointer = viewportCoordsToSceneCoords4(event, this.state);
40076
40345
  const { x: scenePointerX, y: scenePointerY } = scenePointer;
40077
40346
  this.lastPointerMoveCoords = {
40078
40347
  x: scenePointerX,
@@ -40364,7 +40633,7 @@ var App = class _App extends React52.Component {
40364
40633
  }
40365
40634
  this.removePointer(event);
40366
40635
  this.lastPointerUpEvent = event;
40367
- const scenePointer = viewportCoordsToSceneCoords3(
40636
+ const scenePointer = viewportCoordsToSceneCoords4(
40368
40637
  { clientX: event.clientX, clientY: event.clientY },
40369
40638
  this.state
40370
40639
  );
@@ -41444,7 +41713,7 @@ var App = class _App extends React52.Component {
41444
41713
  try {
41445
41714
  const clientX = this.state.width / 2 + this.state.offsetLeft;
41446
41715
  const clientY = this.state.height / 2 + this.state.offsetTop;
41447
- const { x, y } = viewportCoordsToSceneCoords3(
41716
+ const { x, y } = viewportCoordsToSceneCoords4(
41448
41717
  { clientX, clientY },
41449
41718
  this.state
41450
41719
  );
@@ -41619,7 +41888,7 @@ var App = class _App extends React52.Component {
41619
41888
  });
41620
41889
  });
41621
41890
  __publicField(this, "handleAppOnDrop", async (event) => {
41622
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
41891
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
41623
41892
  event,
41624
41893
  this.state
41625
41894
  );
@@ -41786,7 +42055,7 @@ var App = class _App extends React52.Component {
41786
42055
  event.button !== POINTER_BUTTON2.SECONDARY) && this.state.activeTool.type !== this.state.preferredSelectionTool.type) {
41787
42056
  return;
41788
42057
  }
41789
- const { x, y } = viewportCoordsToSceneCoords3(event, this.state);
42058
+ const { x, y } = viewportCoordsToSceneCoords4(event, this.state);
41790
42059
  const element = this.getElementAtPosition(x, y, {
41791
42060
  preferSelected: true,
41792
42061
  includeLockedElements: true
@@ -41850,8 +42119,8 @@ var App = class _App extends React52.Component {
41850
42119
  });
41851
42120
  return;
41852
42121
  }
41853
- const newElement5 = this.state.newElement;
41854
- if (!newElement5) {
42122
+ const newElement6 = this.state.newElement;
42123
+ if (!newElement6) {
41855
42124
  return;
41856
42125
  }
41857
42126
  let [gridX, gridY] = getGridPoint2(
@@ -41859,11 +42128,11 @@ var App = class _App extends React52.Component {
41859
42128
  pointerCoords.y,
41860
42129
  event[KEYS55.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
41861
42130
  );
41862
- const image = isInitializedImageElement3(newElement5) && this.imageCache.get(newElement5.fileId)?.image;
42131
+ const image = isInitializedImageElement3(newElement6) && this.imageCache.get(newElement6.fileId)?.image;
41863
42132
  const aspectRatio = image && !(image instanceof Promise) ? image.width / image.height : null;
41864
- this.maybeCacheReferenceSnapPoints(event, [newElement5]);
42133
+ this.maybeCacheReferenceSnapPoints(event, [newElement6]);
41865
42134
  const { snapOffset, snapLines } = snapNewElement(
41866
- newElement5,
42135
+ newElement6,
41867
42136
  this,
41868
42137
  event,
41869
42138
  {
@@ -41881,9 +42150,9 @@ var App = class _App extends React52.Component {
41881
42150
  this.setState({
41882
42151
  snapLines
41883
42152
  });
41884
- if (!isBindingElement4(newElement5)) {
42153
+ if (!isBindingElement4(newElement6)) {
41885
42154
  dragNewElement({
41886
- newElement: newElement5,
42155
+ newElement: newElement6,
41887
42156
  elementType: this.state.activeTool.type,
41888
42157
  originX: pointerDownState.originInGrid.x,
41889
42158
  originY: pointerDownState.originInGrid.y,
@@ -41891,7 +42160,7 @@ var App = class _App extends React52.Component {
41891
42160
  y: gridY,
41892
42161
  width: distance2(pointerDownState.originInGrid.x, gridX),
41893
42162
  height: distance2(pointerDownState.originInGrid.y, gridY),
41894
- shouldMaintainAspectRatio: isImageElement9(newElement5) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event),
42163
+ shouldMaintainAspectRatio: isImageElement9(newElement6) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event),
41895
42164
  shouldResizeFromCenter: shouldResizeFromCenter(event),
41896
42165
  zoom: this.state.zoom.value,
41897
42166
  scene: this.scene,
@@ -41901,13 +42170,13 @@ var App = class _App extends React52.Component {
41901
42170
  });
41902
42171
  }
41903
42172
  this.setState({
41904
- newElement: newElement5
42173
+ newElement: newElement6
41905
42174
  });
41906
42175
  if (this.state.activeTool.type === TOOL_TYPE3.frame || this.state.activeTool.type === TOOL_TYPE3.magicframe) {
41907
42176
  this.setState({
41908
42177
  elementsToHighlight: getElementsInResizingFrame4(
41909
42178
  this.scene.getNonDeletedElements(),
41910
- newElement5,
42179
+ newElement6,
41911
42180
  this.state,
41912
42181
  this.scene.getNonDeletedElementsMap()
41913
42182
  )
@@ -42206,7 +42475,7 @@ var App = class _App extends React52.Component {
42206
42475
  if (!x || !y) {
42207
42476
  return;
42208
42477
  }
42209
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
42478
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
42210
42479
  { clientX: x, clientY: y },
42211
42480
  this.state
42212
42481
  );
@@ -42631,7 +42900,7 @@ var App = class _App extends React52.Component {
42631
42900
  return false;
42632
42901
  }
42633
42902
  const viewportClickStart_scenePoint = pointFrom29(
42634
- viewportCoordsToSceneCoords3(
42903
+ viewportCoordsToSceneCoords4(
42635
42904
  {
42636
42905
  clientX: this.lastPointerDownEvent.clientX,
42637
42906
  clientY: this.lastPointerDownEvent.clientY
@@ -42640,7 +42909,7 @@ var App = class _App extends React52.Component {
42640
42909
  )
42641
42910
  );
42642
42911
  const viewportClickEnd_scenePoint = pointFrom29(
42643
- viewportCoordsToSceneCoords3(
42912
+ viewportCoordsToSceneCoords4(
42644
42913
  {
42645
42914
  clientX: this.lastPointerUpEvent.clientX,
42646
42915
  clientY: this.lastPointerUpEvent.clientY
@@ -43161,14 +43430,18 @@ var App = class _App extends React52.Component {
43161
43430
  ImageGeneratorPanel,
43162
43431
  {
43163
43432
  element: firstSelectedElement,
43164
- app: this
43433
+ app: this,
43434
+ onBeforeImageGen: this.props.onBeforeImageGen,
43435
+ onAfterImageGen: this.props.onAfterImageGen
43165
43436
  }
43166
43437
  ),
43167
43438
  selectedElements.length === 1 && isImageElement9(firstSelectedElement) && !this.state.viewModeEnabled && /* @__PURE__ */ jsx168(
43168
43439
  ImageQuickEditPanel,
43169
43440
  {
43170
43441
  element: firstSelectedElement,
43171
- app: this
43442
+ app: this,
43443
+ onBeforeImageGen: this.props.onBeforeImageGen,
43444
+ onAfterImageGen: this.props.onAfterImageGen
43172
43445
  }
43173
43446
  ),
43174
43447
  this.state.toast !== null && /* @__PURE__ */ jsx168(
@@ -43702,7 +43975,7 @@ var App = class _App extends React52.Component {
43702
43975
  }
43703
43976
  // TODO: Cover with tests
43704
43977
  async insertClipboardContent(data, dataTransferFiles, isPlainPaste) {
43705
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
43978
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
43706
43979
  {
43707
43980
  clientX: this.lastViewportPosition.x,
43708
43981
  clientY: this.lastViewportPosition.y
@@ -43849,7 +44122,7 @@ var App = class _App extends React52.Component {
43849
44122
  }
43850
44123
  }
43851
44124
  addTextFromPaste(text, isPlainPaste = false) {
43852
- const { x, y } = viewportCoordsToSceneCoords3(
44125
+ const { x, y } = viewportCoordsToSceneCoords4(
43853
44126
  {
43854
44127
  clientX: this.lastViewportPosition.x,
43855
44128
  clientY: this.lastViewportPosition.y
@@ -44153,7 +44426,7 @@ var App = class _App extends React52.Component {
44153
44426
  if (elements[index].isDeleted) {
44154
44427
  continue;
44155
44428
  }
44156
- const [x1, y1, x2, y2] = getElementAbsoluteCoords11(
44429
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords12(
44157
44430
  elements[index],
44158
44431
  this.scene.getNonDeletedElementsMap()
44159
44432
  );
@@ -44286,7 +44559,7 @@ var App = class _App extends React52.Component {
44286
44559
  }
44287
44560
  }
44288
44561
  initialPointerDownState(event) {
44289
- const origin = viewportCoordsToSceneCoords3(event, this.state);
44562
+ const origin = viewportCoordsToSceneCoords4(event, this.state);
44290
44563
  const selectedElements = this.scene.getSelectedElements(this.state);
44291
44564
  const [minX, minY, maxX, maxY] = getCommonBounds11(selectedElements);
44292
44565
  const isElbowArrowOnly = selectedElements.findIndex(isElbowArrow10) === 0;
@@ -44450,7 +44723,7 @@ var App = class _App extends React52.Component {
44450
44723
  if (this.state.openDialog?.name === "elementLinkSelector") {
44451
44724
  return;
44452
44725
  }
44453
- const pointerCoords = viewportCoordsToSceneCoords3(event, this.state);
44726
+ const pointerCoords = viewportCoordsToSceneCoords4(event, this.state);
44454
44727
  if (this.state.activeLockedId) {
44455
44728
  this.setState({
44456
44729
  activeLockedId: null
@@ -44699,7 +44972,7 @@ var App = class _App extends React52.Component {
44699
44972
  );
44700
44973
  instantDragOffset[0] *= image.naturalWidth / uncroppedSize.width;
44701
44974
  instantDragOffset[1] *= image.naturalHeight / uncroppedSize.height;
44702
- const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords11(
44975
+ const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords12(
44703
44976
  croppingElement,
44704
44977
  elementsMap
44705
44978
  );
@@ -44867,7 +45140,7 @@ var App = class _App extends React52.Component {
44867
45140
  },
44868
45141
  []
44869
45142
  );
44870
- pointerDownState.drag.origin = viewportCoordsToSceneCoords3(
45143
+ pointerDownState.drag.origin = viewportCoordsToSceneCoords4(
44871
45144
  event,
44872
45145
  this.state
44873
45146
  );
@@ -44926,20 +45199,20 @@ var App = class _App extends React52.Component {
44926
45199
  );
44927
45200
  }
44928
45201
  } else {
44929
- const newElement5 = this.state.newElement;
44930
- if (!newElement5) {
45202
+ const newElement6 = this.state.newElement;
45203
+ if (!newElement6) {
44931
45204
  return;
44932
45205
  }
44933
- if (newElement5.type === "freedraw") {
44934
- const points = newElement5.points;
44935
- const dx = pointerCoords.x - newElement5.x;
44936
- const dy = pointerCoords.y - newElement5.y;
45206
+ if (newElement6.type === "freedraw") {
45207
+ const points = newElement6.points;
45208
+ const dx = pointerCoords.x - newElement6.x;
45209
+ const dy = pointerCoords.y - newElement6.y;
44937
45210
  const lastPoint = points.length > 0 && points[points.length - 1];
44938
45211
  const discardPoint = lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
44939
45212
  if (!discardPoint) {
44940
- const pressures = newElement5.simulatePressure ? newElement5.pressures : [...newElement5.pressures, event.pressure];
45213
+ const pressures = newElement6.simulatePressure ? newElement6.pressures : [...newElement6.pressures, event.pressure];
44941
45214
  this.scene.mutateElement(
44942
- newElement5,
45215
+ newElement6,
44943
45216
  {
44944
45217
  points: [...points, pointFrom29(dx, dy)],
44945
45218
  pressures
@@ -44950,12 +45223,12 @@ var App = class _App extends React52.Component {
44950
45223
  }
44951
45224
  );
44952
45225
  this.setState({
44953
- newElement: newElement5
45226
+ newElement: newElement6
44954
45227
  });
44955
45228
  }
44956
- } else if (isLinearElement12(newElement5) && !newElement5.isDeleted) {
45229
+ } else if (isLinearElement12(newElement6) && !newElement6.isDeleted) {
44957
45230
  pointerDownState.drag.hasOccurred = true;
44958
- const points = newElement5.points;
45231
+ const points = newElement6.points;
44959
45232
  invariant16(
44960
45233
  points.length > 1,
44961
45234
  "Do not create linear elements with less than 2 points"
@@ -44963,7 +45236,7 @@ var App = class _App extends React52.Component {
44963
45236
  let linearElementEditor = this.state.selectedLinearElement;
44964
45237
  if (!linearElementEditor) {
44965
45238
  linearElementEditor = new LinearElementEditor11(
44966
- newElement5,
45239
+ newElement6,
44967
45240
  this.scene.getNonDeletedElementsMap()
44968
45241
  );
44969
45242
  linearElementEditor = {
@@ -44976,7 +45249,7 @@ var App = class _App extends React52.Component {
44976
45249
  };
44977
45250
  }
44978
45251
  this.setState({
44979
- newElement: newElement5,
45252
+ newElement: newElement6,
44980
45253
  ...LinearElementEditor11.handlePointDragging(
44981
45254
  event,
44982
45255
  this,
@@ -45100,7 +45373,7 @@ var App = class _App extends React52.Component {
45100
45373
  pointerDownState.eventListeners.onMove.flush();
45101
45374
  }
45102
45375
  const {
45103
- newElement: newElement5,
45376
+ newElement: newElement6,
45104
45377
  resizingElement,
45105
45378
  croppingElementId,
45106
45379
  multiElement,
@@ -45127,7 +45400,7 @@ var App = class _App extends React52.Component {
45127
45400
  SnapCache.setVisibleGaps(null);
45128
45401
  this.savePointer(childEvent.clientX, childEvent.clientY, "up");
45129
45402
  const hitElements = pointerDownState.hit.allHitElements;
45130
- const sceneCoords = viewportCoordsToSceneCoords3(
45403
+ const sceneCoords = viewportCoordsToSceneCoords4(
45131
45404
  { clientX: childEvent.clientX, clientY: childEvent.clientY },
45132
45405
  this.state
45133
45406
  );
@@ -45264,31 +45537,31 @@ var App = class _App extends React52.Component {
45264
45537
  pointerDownState,
45265
45538
  childEvent
45266
45539
  );
45267
- if (newElement5?.type === "freedraw") {
45268
- const pointerCoords = viewportCoordsToSceneCoords3(
45540
+ if (newElement6?.type === "freedraw") {
45541
+ const pointerCoords = viewportCoordsToSceneCoords4(
45269
45542
  childEvent,
45270
45543
  this.state
45271
45544
  );
45272
- const points = newElement5.points;
45273
- let dx = pointerCoords.x - newElement5.x;
45274
- let dy = pointerCoords.y - newElement5.y;
45545
+ const points = newElement6.points;
45546
+ let dx = pointerCoords.x - newElement6.x;
45547
+ let dy = pointerCoords.y - newElement6.y;
45275
45548
  if (dx === points[0][0] && dy === points[0][1]) {
45276
45549
  dy += 1e-4;
45277
45550
  dx += 1e-4;
45278
45551
  }
45279
- const pressures = newElement5.simulatePressure ? [] : [...newElement5.pressures, childEvent.pressure];
45280
- this.scene.mutateElement(newElement5, {
45552
+ const pressures = newElement6.simulatePressure ? [] : [...newElement6.pressures, childEvent.pressure];
45553
+ this.scene.mutateElement(newElement6, {
45281
45554
  points: [...points, pointFrom29(dx, dy)],
45282
45555
  pressures
45283
45556
  });
45284
45557
  this.actionManager.executeAction(actionFinalize);
45285
45558
  return;
45286
45559
  }
45287
- if (isLinearElement12(newElement5)) {
45288
- if (newElement5.points.length > 1 && newElement5.points[1][0] !== 0 && newElement5.points[1][1] !== 0) {
45560
+ if (isLinearElement12(newElement6)) {
45561
+ if (newElement6.points.length > 1 && newElement6.points[1][0] !== 0 && newElement6.points[1][1] !== 0) {
45289
45562
  this.store.scheduleCapture();
45290
45563
  }
45291
- const pointerCoords = viewportCoordsToSceneCoords3(
45564
+ const pointerCoords = viewportCoordsToSceneCoords4(
45292
45565
  childEvent,
45293
45566
  this.state
45294
45567
  );
@@ -45296,16 +45569,16 @@ var App = class _App extends React52.Component {
45296
45569
  pointFrom29(pointerCoords.x, pointerCoords.y),
45297
45570
  pointFrom29(pointerDownState.origin.x, pointerDownState.origin.y)
45298
45571
  ) * this.state.zoom.value;
45299
- if ((!pointerDownState.drag.hasOccurred || dragDistance < MINIMUM_ARROW_SIZE) && newElement5 && !multiElement) {
45572
+ if ((!pointerDownState.drag.hasOccurred || dragDistance < MINIMUM_ARROW_SIZE) && newElement6 && !multiElement) {
45300
45573
  if (this.editorInterface.isTouchScreen) {
45301
45574
  const FIXED_DELTA_X = Math.min(
45302
45575
  this.state.width * 0.7 / this.state.zoom.value,
45303
45576
  100
45304
45577
  );
45305
45578
  this.scene.mutateElement(
45306
- newElement5,
45579
+ newElement6,
45307
45580
  {
45308
- x: newElement5.x - FIXED_DELTA_X / 2,
45581
+ x: newElement6.x - FIXED_DELTA_X / 2,
45309
45582
  points: [
45310
45583
  pointFrom29(0, 0),
45311
45584
  pointFrom29(FIXED_DELTA_X, 0)
@@ -45315,22 +45588,22 @@ var App = class _App extends React52.Component {
45315
45588
  );
45316
45589
  this.actionManager.executeAction(actionFinalize);
45317
45590
  } else {
45318
- const dx = pointerCoords.x - newElement5.x;
45319
- const dy = pointerCoords.y - newElement5.y;
45591
+ const dx = pointerCoords.x - newElement6.x;
45592
+ const dy = pointerCoords.y - newElement6.y;
45320
45593
  this.scene.mutateElement(
45321
- newElement5,
45594
+ newElement6,
45322
45595
  {
45323
- points: [newElement5.points[0], pointFrom29(dx, dy)]
45596
+ points: [newElement6.points[0], pointFrom29(dx, dy)]
45324
45597
  },
45325
45598
  { informMutation: false, isDragging: false }
45326
45599
  );
45327
45600
  this.setState({
45328
- multiElement: newElement5,
45329
- newElement: newElement5
45601
+ multiElement: newElement6,
45602
+ newElement: newElement6
45330
45603
  });
45331
45604
  }
45332
45605
  } else if (pointerDownState.drag.hasOccurred && !multiElement) {
45333
- if (isLinearElement12(newElement5)) {
45606
+ if (isLinearElement12(newElement6)) {
45334
45607
  this.actionManager.executeAction(actionFinalize, "ui", {
45335
45608
  event: childEvent,
45336
45609
  sceneCoords
@@ -45347,12 +45620,12 @@ var App = class _App extends React52.Component {
45347
45620
  selectedElementIds: makeNextSelectedElementIds2(
45348
45621
  {
45349
45622
  ...prevState.selectedElementIds,
45350
- [newElement5.id]: true
45623
+ [newElement6.id]: true
45351
45624
  },
45352
45625
  prevState
45353
45626
  ),
45354
45627
  selectedLinearElement: new LinearElementEditor11(
45355
- newElement5,
45628
+ newElement6,
45356
45629
  this.scene.getNonDeletedElementsMap()
45357
45630
  )
45358
45631
  }));
@@ -45365,27 +45638,27 @@ var App = class _App extends React52.Component {
45365
45638
  }
45366
45639
  return;
45367
45640
  }
45368
- if (isTextElement19(newElement5)) {
45641
+ if (isTextElement19(newElement6)) {
45369
45642
  const minWidth = getMinTextElementWidth(
45370
45643
  getFontString9({
45371
- fontSize: newElement5.fontSize,
45372
- fontFamily: newElement5.fontFamily
45644
+ fontSize: newElement6.fontSize,
45645
+ fontFamily: newElement6.fontFamily
45373
45646
  }),
45374
- newElement5.lineHeight
45647
+ newElement6.lineHeight
45375
45648
  );
45376
- if (newElement5.width < minWidth) {
45377
- this.scene.mutateElement(newElement5, {
45649
+ if (newElement6.width < minWidth) {
45650
+ this.scene.mutateElement(newElement6, {
45378
45651
  autoResize: true
45379
45652
  });
45380
45653
  }
45381
45654
  this.resetCursor();
45382
- this.handleTextWysiwyg(newElement5, {
45655
+ this.handleTextWysiwyg(newElement6, {
45383
45656
  isExistingElement: true
45384
45657
  });
45385
45658
  }
45386
- if (activeTool.type !== "selection" && newElement5 && isInvisiblySmallElement3(newElement5)) {
45659
+ if (activeTool.type !== "selection" && newElement6 && isInvisiblySmallElement3(newElement6)) {
45387
45660
  this.updateScene({
45388
- elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== newElement5.id),
45661
+ elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== newElement6.id),
45389
45662
  appState: {
45390
45663
  newElement: null
45391
45664
  },
@@ -45393,25 +45666,25 @@ var App = class _App extends React52.Component {
45393
45666
  });
45394
45667
  return;
45395
45668
  }
45396
- if (isFrameLikeElement16(newElement5)) {
45669
+ if (isFrameLikeElement16(newElement6)) {
45397
45670
  const elementsInsideFrame = getElementsInNewFrame(
45398
45671
  this.scene.getElementsIncludingDeleted(),
45399
- newElement5,
45672
+ newElement6,
45400
45673
  this.scene.getNonDeletedElementsMap()
45401
45674
  );
45402
45675
  this.scene.replaceAllElements(
45403
45676
  addElementsToFrame2(
45404
45677
  this.scene.getElementsMapIncludingDeleted(),
45405
45678
  elementsInsideFrame,
45406
- newElement5,
45679
+ newElement6,
45407
45680
  this.state
45408
45681
  )
45409
45682
  );
45410
45683
  }
45411
- if (newElement5) {
45684
+ if (newElement6) {
45412
45685
  this.scene.mutateElement(
45413
- newElement5,
45414
- getNormalizedDimensions(newElement5),
45686
+ newElement6,
45687
+ getNormalizedDimensions(newElement6),
45415
45688
  {
45416
45689
  informMutation: false,
45417
45690
  isDragging: false
@@ -45420,7 +45693,7 @@ var App = class _App extends React52.Component {
45420
45693
  this.scene.triggerUpdate();
45421
45694
  }
45422
45695
  if (pointerDownState.drag.hasOccurred) {
45423
- const sceneCoords2 = viewportCoordsToSceneCoords3(childEvent, this.state);
45696
+ const sceneCoords2 = viewportCoordsToSceneCoords4(childEvent, this.state);
45424
45697
  if (this.state.selectedLinearElement && this.state.selectedLinearElement.isDragging) {
45425
45698
  const linearElement = this.scene.getElement(
45426
45699
  this.state.selectedLinearElement.elementId
@@ -45573,7 +45846,7 @@ var App = class _App extends React52.Component {
45573
45846
  pointFrom29(pointerEnd.clientX, pointerEnd.clientY)
45574
45847
  );
45575
45848
  if (draggedDistance === 0) {
45576
- const scenePointer = viewportCoordsToSceneCoords3(
45849
+ const scenePointer = viewportCoordsToSceneCoords4(
45577
45850
  {
45578
45851
  clientX: pointerEnd.clientX,
45579
45852
  clientY: pointerEnd.clientY
@@ -45741,16 +46014,16 @@ var App = class _App extends React52.Component {
45741
46014
  setCursor(this.interactiveCanvas, CURSOR_TYPE4.AUTO);
45742
46015
  return;
45743
46016
  }
45744
- if (!activeTool.locked && activeTool.type !== "freedraw" && newElement5) {
46017
+ if (!activeTool.locked && activeTool.type !== "freedraw" && newElement6) {
45745
46018
  this.setState((prevState) => ({
45746
46019
  selectedElementIds: makeNextSelectedElementIds2(
45747
46020
  {
45748
46021
  ...prevState.selectedElementIds,
45749
- [newElement5.id]: true
46022
+ [newElement6.id]: true
45750
46023
  },
45751
46024
  prevState
45752
46025
  ),
45753
- showHyperlinkPopup: isEmbeddableElement4(newElement5) && !newElement5.link ? "editor" : prevState.showHyperlinkPopup
46026
+ showHyperlinkPopup: isEmbeddableElement4(newElement6) && !newElement6.link ? "editor" : prevState.showHyperlinkPopup
45754
46027
  }));
45755
46028
  }
45756
46029
  if (activeTool.type !== "selection" || isSomeElementSelected(this.scene.getNonDeletedElements(), this.state) || !isShallowEqual9(
@@ -46300,7 +46573,7 @@ import {
46300
46573
  UserIdleState as UserIdleState2,
46301
46574
  normalizeLink as normalizeLink4,
46302
46575
  sceneCoordsToViewportCoords as sceneCoordsToViewportCoords2,
46303
- viewportCoordsToSceneCoords as viewportCoordsToSceneCoords4,
46576
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords5,
46304
46577
  getFormFactor as getFormFactor2
46305
46578
  } from "@orangecatai/common";
46306
46579
  import {
@@ -46518,7 +46791,2465 @@ var DiagramToCodePlugin = (props) => {
46518
46791
  // index.tsx
46519
46792
  import { isElementLink as isElementLink3 } from "@orangecatai/element";
46520
46793
  import { setCustomTextMetricsProvider } from "@orangecatai/element";
46794
+
46795
+ // components/AIChatPanel.tsx
46796
+ import React57, {
46797
+ useCallback as useCallback24,
46798
+ useEffect as useEffect55,
46799
+ useImperativeHandle as useImperativeHandle4,
46800
+ useRef as useRef49,
46801
+ useState as useState49
46802
+ } from "react";
46803
+ import {
46804
+ ArrowUp as ArrowUp3,
46805
+ AtSign,
46806
+ Check,
46807
+ ChevronDown as ChevronDown2,
46808
+ ChevronRight,
46809
+ Copy,
46810
+ Globe,
46811
+ MessageSquare,
46812
+ Mic,
46813
+ MicOff,
46814
+ Paperclip,
46815
+ Plus,
46816
+ Search,
46817
+ Wrench,
46818
+ X as X2
46819
+ } from "lucide-react";
46820
+
46821
+ // utils/openRouterApiKey.ts
46822
+ function resolveOpenRouterApiKey(propKey) {
46823
+ const normalizedPropKey = propKey?.trim();
46824
+ return (normalizedPropKey ? normalizedPropKey : void 0) ?? (typeof import.meta !== "undefined" && define_import_meta_env_default?.VITE_APP_OPENROUTER_API_KEY ? define_import_meta_env_default.VITE_APP_OPENROUTER_API_KEY : "") ?? "";
46825
+ }
46826
+
46827
+ // components/ai-chat/canvasTools.ts
46828
+ import { nanoid as nanoid2 } from "nanoid";
46829
+ import {
46830
+ newFrameElement as newFrameElement3,
46831
+ newElement as newElement5,
46832
+ newTextElement as newTextElement4,
46833
+ newImageElement as newImageElement2,
46834
+ getFrameChildren as getFrameChildren7,
46835
+ isFrameLikeElement as isFrameLikeElement17
46836
+ } from "@orangecatai/element";
46837
+ import { FRAME_STYLE as FRAME_STYLE5 } from "@orangecatai/common";
46838
+ var CANVAS_TOOLS = [
46839
+ {
46840
+ type: "function",
46841
+ function: {
46842
+ name: "create_frame",
46843
+ description: "Create a new frame on the canvas. Frames act as artboards that clip their children. Default size is 512x512.",
46844
+ parameters: {
46845
+ type: "object",
46846
+ properties: {
46847
+ width: {
46848
+ type: "number",
46849
+ description: "Frame width in pixels (default 512)"
46850
+ },
46851
+ height: {
46852
+ type: "number",
46853
+ description: "Frame height in pixels (default 512)"
46854
+ },
46855
+ name: {
46856
+ type: "string",
46857
+ description: "Optional display name for the frame"
46858
+ }
46859
+ },
46860
+ required: [],
46861
+ additionalProperties: false
46862
+ }
46863
+ }
46864
+ },
46865
+ {
46866
+ type: "function",
46867
+ function: {
46868
+ name: "add_rectangle",
46869
+ description: "Add a filled rectangle inside a frame. Commonly used for solid background fills behind images and text.",
46870
+ parameters: {
46871
+ type: "object",
46872
+ properties: {
46873
+ frameId: {
46874
+ type: "string",
46875
+ description: "ID of the parent frame"
46876
+ },
46877
+ x: {
46878
+ type: "number",
46879
+ description: "X position relative to canvas (use the frame's x for full coverage)"
46880
+ },
46881
+ y: {
46882
+ type: "number",
46883
+ description: "Y position relative to canvas (use the frame's y for full coverage)"
46884
+ },
46885
+ width: {
46886
+ type: "number",
46887
+ description: "Rectangle width in pixels"
46888
+ },
46889
+ height: {
46890
+ type: "number",
46891
+ description: "Rectangle height in pixels"
46892
+ },
46893
+ backgroundColor: {
46894
+ type: "string",
46895
+ description: 'Fill color as hex string e.g. "#1a1a2e". Use "transparent" for no fill.'
46896
+ },
46897
+ fillStyle: {
46898
+ type: "string",
46899
+ enum: ["solid", "hachure", "cross-hatch"],
46900
+ description: 'Fill style (default "solid")'
46901
+ },
46902
+ strokeColor: {
46903
+ type: "string",
46904
+ description: 'Stroke/border color as hex (default "transparent")'
46905
+ },
46906
+ strokeWidth: {
46907
+ type: "number",
46908
+ description: "Stroke width in pixels (default 0)"
46909
+ },
46910
+ opacity: {
46911
+ type: "number",
46912
+ description: "Opacity 0-100 (default 100)"
46913
+ }
46914
+ },
46915
+ required: ["frameId", "x", "y", "width", "height", "backgroundColor"],
46916
+ additionalProperties: false
46917
+ }
46918
+ }
46919
+ },
46920
+ {
46921
+ type: "function",
46922
+ function: {
46923
+ name: "add_text",
46924
+ description: "Add a text element inside a frame. Use for headlines, subheads, body copy, CTAs etc.",
46925
+ parameters: {
46926
+ type: "object",
46927
+ properties: {
46928
+ frameId: {
46929
+ type: "string",
46930
+ description: "ID of the parent frame"
46931
+ },
46932
+ x: {
46933
+ type: "number",
46934
+ description: "X position (canvas coordinates)"
46935
+ },
46936
+ y: {
46937
+ type: "number",
46938
+ description: "Y position (canvas coordinates)"
46939
+ },
46940
+ text: {
46941
+ type: "string",
46942
+ description: "The text content"
46943
+ },
46944
+ fontSize: {
46945
+ type: "number",
46946
+ description: "Font size in pixels (default 20)"
46947
+ },
46948
+ fontFamily: {
46949
+ type: "number",
46950
+ enum: [1, 2, 3, 4, 5],
46951
+ description: "Font family: 1=Excalifont(hand), 2=Nunito(sans), 3=Comic Shanns(code), 4=Liberation Sans(clean sans), 5=cascadia(code). Default 2."
46952
+ },
46953
+ strokeColor: {
46954
+ type: "string",
46955
+ description: 'Text color as hex string (default "#000000")'
46956
+ },
46957
+ textAlign: {
46958
+ type: "string",
46959
+ enum: ["left", "center", "right"],
46960
+ description: "Horizontal alignment (default left)"
46961
+ },
46962
+ width: {
46963
+ type: "number",
46964
+ description: "Optional fixed width for text wrapping"
46965
+ }
46966
+ },
46967
+ required: ["frameId", "x", "y", "text"],
46968
+ additionalProperties: false
46969
+ }
46970
+ }
46971
+ },
46972
+ {
46973
+ type: "function",
46974
+ function: {
46975
+ name: "generate_image",
46976
+ description: "Generate an image using AI (Gemini) from a text prompt and place it inside a frame. Use for hero images, product shots, backgrounds, illustrations etc.",
46977
+ parameters: {
46978
+ type: "object",
46979
+ properties: {
46980
+ frameId: {
46981
+ type: "string",
46982
+ description: "ID of the parent frame to insert the image into"
46983
+ },
46984
+ prompt: {
46985
+ type: "string",
46986
+ description: "Detailed image generation prompt describing what to create"
46987
+ },
46988
+ x: {
46989
+ type: "number",
46990
+ description: "X position for the image (canvas coordinates)"
46991
+ },
46992
+ y: {
46993
+ type: "number",
46994
+ description: "Y position for the image (canvas coordinates)"
46995
+ },
46996
+ width: {
46997
+ type: "number",
46998
+ description: "Image width in pixels"
46999
+ },
47000
+ height: {
47001
+ type: "number",
47002
+ description: "Image height in pixels"
47003
+ }
47004
+ },
47005
+ required: ["frameId", "prompt", "x", "y", "width", "height"],
47006
+ additionalProperties: false
47007
+ }
47008
+ }
47009
+ },
47010
+ {
47011
+ type: "function",
47012
+ function: {
47013
+ name: "update_element",
47014
+ description: "Update properties of an existing element (move, resize, recolor, change text, etc.)",
47015
+ parameters: {
47016
+ type: "object",
47017
+ properties: {
47018
+ elementId: {
47019
+ type: "string",
47020
+ description: "ID of the element to update"
47021
+ },
47022
+ x: { type: "number", description: "New X position" },
47023
+ y: { type: "number", description: "New Y position" },
47024
+ width: { type: "number", description: "New width" },
47025
+ height: { type: "number", description: "New height" },
47026
+ backgroundColor: {
47027
+ type: "string",
47028
+ description: "New background/fill color"
47029
+ },
47030
+ strokeColor: {
47031
+ type: "string",
47032
+ description: "New stroke/text color"
47033
+ },
47034
+ opacity: { type: "number", description: "New opacity 0-100" },
47035
+ fontSize: {
47036
+ type: "number",
47037
+ description: "New font size (text elements only)"
47038
+ },
47039
+ text: {
47040
+ type: "string",
47041
+ description: "New text content (text elements only)"
47042
+ },
47043
+ fillStyle: {
47044
+ type: "string",
47045
+ enum: ["solid", "hachure", "cross-hatch"],
47046
+ description: "New fill style"
47047
+ }
47048
+ },
47049
+ required: ["elementId"],
47050
+ additionalProperties: false
47051
+ }
47052
+ }
47053
+ },
47054
+ {
47055
+ type: "function",
47056
+ function: {
47057
+ name: "delete_element",
47058
+ description: "Delete an element from the canvas",
47059
+ parameters: {
47060
+ type: "object",
47061
+ properties: {
47062
+ elementId: {
47063
+ type: "string",
47064
+ description: "ID of the element to delete"
47065
+ }
47066
+ },
47067
+ required: ["elementId"],
47068
+ additionalProperties: false
47069
+ }
47070
+ }
47071
+ },
47072
+ {
47073
+ type: "function",
47074
+ function: {
47075
+ name: "get_frame_elements",
47076
+ description: "Get all child elements inside a frame. Returns their IDs, types, positions, sizes, colors, and text content. Use this to understand the current layout before making changes.",
47077
+ parameters: {
47078
+ type: "object",
47079
+ properties: {
47080
+ frameId: {
47081
+ type: "string",
47082
+ description: "ID of the frame to inspect"
47083
+ }
47084
+ },
47085
+ required: ["frameId"],
47086
+ additionalProperties: false
47087
+ }
47088
+ }
47089
+ }
47090
+ ];
47091
+ function serializeElement(el) {
47092
+ const base = {
47093
+ id: el.id,
47094
+ type: el.type,
47095
+ x: Math.round(el.x),
47096
+ y: Math.round(el.y),
47097
+ width: Math.round(el.width),
47098
+ height: Math.round(el.height)
47099
+ };
47100
+ if (el.backgroundColor && el.backgroundColor !== "transparent") {
47101
+ base.backgroundColor = el.backgroundColor;
47102
+ }
47103
+ if (el.strokeColor && el.strokeColor !== "#1e1e1e") {
47104
+ base.strokeColor = el.strokeColor;
47105
+ }
47106
+ if (el.opacity !== 100) {
47107
+ base.opacity = el.opacity;
47108
+ }
47109
+ if (el.fillStyle && el.fillStyle !== "solid") {
47110
+ base.fillStyle = el.fillStyle;
47111
+ }
47112
+ if (el.type === "text") {
47113
+ const textEl = el;
47114
+ base.text = textEl.text;
47115
+ base.fontSize = textEl.fontSize;
47116
+ base.fontFamily = textEl.fontFamily;
47117
+ base.textAlign = textEl.textAlign;
47118
+ }
47119
+ if (el.type === "image") {
47120
+ base.type = "image";
47121
+ }
47122
+ if (el.type === "frame") {
47123
+ const frameEl = el;
47124
+ if (frameEl.name) {
47125
+ base.name = frameEl.name;
47126
+ }
47127
+ }
47128
+ return base;
47129
+ }
47130
+ function serializeElements(elements) {
47131
+ return elements.map(serializeElement);
47132
+ }
47133
+ function listFrames(api) {
47134
+ const elements = api.getSceneElements();
47135
+ const frames = [];
47136
+ for (const el of elements) {
47137
+ if (isFrameLikeElement17(el)) {
47138
+ const children = getFrameChildren7(elements, el.id);
47139
+ const frameLike = el;
47140
+ frames.push({
47141
+ id: el.id,
47142
+ name: frameLike.name || `Frame ${Math.round(el.width)}\xD7${Math.round(el.height)}`,
47143
+ width: Math.round(el.width),
47144
+ height: Math.round(el.height),
47145
+ childCount: children.length
47146
+ });
47147
+ }
47148
+ }
47149
+ return frames;
47150
+ }
47151
+ async function captureFrameScreenshot(api, frameId) {
47152
+ const elements = api.getSceneElements();
47153
+ const frame = elements.find((el) => el.id === frameId);
47154
+ if (!frame || !isFrameLikeElement17(frame)) {
47155
+ return null;
47156
+ }
47157
+ const appState = api.getAppState();
47158
+ const files = api.getFiles();
47159
+ const children = getFrameChildren7(elements, frameId);
47160
+ try {
47161
+ const canvas = await exportToCanvas([...children, frame], appState, files, {
47162
+ exportBackground: true,
47163
+ viewBackgroundColor: appState.viewBackgroundColor,
47164
+ exportingFrame: frame,
47165
+ exportPadding: 0
47166
+ });
47167
+ return canvas.toDataURL("image/jpeg", 0.7);
47168
+ } catch {
47169
+ return null;
47170
+ }
47171
+ }
47172
+ function getFrameContext(api, frameId) {
47173
+ const elements = api.getSceneElements();
47174
+ const frame = elements.find((el) => el.id === frameId);
47175
+ if (!frame || !isFrameLikeElement17(frame)) {
47176
+ return null;
47177
+ }
47178
+ const children = getFrameChildren7(elements, frameId);
47179
+ const frameLike = frame;
47180
+ const frameInfo = {
47181
+ id: frame.id,
47182
+ name: frameLike.name || `Frame ${Math.round(frame.width)}\xD7${Math.round(frame.height)}`,
47183
+ width: Math.round(frame.width),
47184
+ height: Math.round(frame.height),
47185
+ childCount: children.length
47186
+ };
47187
+ const data = {
47188
+ frame: serializeElement(frame),
47189
+ children: serializeElements(children)
47190
+ };
47191
+ return {
47192
+ serialized: JSON.stringify(data, null, 2),
47193
+ frameInfo
47194
+ };
47195
+ }
47196
+ async function executeCanvasTool(toolName, rawArgs, ctx) {
47197
+ let args;
47198
+ try {
47199
+ args = JSON.parse(rawArgs);
47200
+ } catch {
47201
+ return {
47202
+ success: false,
47203
+ error: "Invalid JSON arguments",
47204
+ statusMessage: "Failed to parse tool arguments"
47205
+ };
47206
+ }
47207
+ switch (toolName) {
47208
+ case "create_frame":
47209
+ return execCreateFrame(args, ctx);
47210
+ case "add_rectangle":
47211
+ return execAddRectangle(args, ctx);
47212
+ case "add_text":
47213
+ return execAddText(args, ctx);
47214
+ case "generate_image":
47215
+ return execGenerateImage(args, ctx);
47216
+ case "update_element":
47217
+ return execUpdateElement(args, ctx);
47218
+ case "delete_element":
47219
+ return execDeleteElement(args, ctx);
47220
+ case "get_frame_elements":
47221
+ return execGetFrameElements(args, ctx);
47222
+ default:
47223
+ return {
47224
+ success: false,
47225
+ error: `Unknown tool: ${toolName}`,
47226
+ statusMessage: `Unknown tool: ${toolName}`
47227
+ };
47228
+ }
47229
+ }
47230
+ function getViewportCenter(api) {
47231
+ const appState = api.getAppState();
47232
+ const x = -appState.scrollX + appState.width / 2 / appState.zoom.value;
47233
+ const y = -appState.scrollY + appState.height / 2 / appState.zoom.value;
47234
+ return { x, y };
47235
+ }
47236
+ var COVER_TOLERANCE = 10;
47237
+ function coversFrame(elX, elY, elW, elH, frameEl) {
47238
+ return Math.abs(elX - frameEl.x) < COVER_TOLERANCE && Math.abs(elY - frameEl.y) < COVER_TOLERANCE && Math.abs(elW - frameEl.width) < COVER_TOLERANCE && Math.abs(elH - frameEl.height) < COVER_TOLERANCE;
47239
+ }
47240
+ function getElementTier(type, x, y, w, h, frame) {
47241
+ if (type === "text") {
47242
+ return 3 /* TEXT */;
47243
+ }
47244
+ if (type === "image") {
47245
+ return 1 /* IMAGE */;
47246
+ }
47247
+ if (coversFrame(x, y, w, h, frame)) {
47248
+ return 0 /* BG_FILL */;
47249
+ }
47250
+ return 2 /* SHAPE */;
47251
+ }
47252
+ function findInsertIndexByTier(elements, frameIndex, tier) {
47253
+ const frame = elements[frameIndex];
47254
+ const frameId = frame.id;
47255
+ let insertAt = frameIndex + 1;
47256
+ for (let i = frameIndex + 1; i < elements.length; i++) {
47257
+ const el = elements[i];
47258
+ if (el.frameId !== frameId) {
47259
+ continue;
47260
+ }
47261
+ const elTier = getElementTier(
47262
+ el.type,
47263
+ el.x,
47264
+ el.y,
47265
+ el.width,
47266
+ el.height,
47267
+ frame
47268
+ );
47269
+ if (elTier > tier) {
47270
+ return i;
47271
+ }
47272
+ insertAt = i + 1;
47273
+ }
47274
+ return insertAt;
47275
+ }
47276
+ function execCreateFrame(args, ctx) {
47277
+ const width = args.width || 512;
47278
+ const height = args.height || 512;
47279
+ const name = args.name || null;
47280
+ const center = getViewportCenter(ctx.excalidrawAPI);
47281
+ const frame = newFrameElement3({
47282
+ ...FRAME_STYLE5,
47283
+ x: center.x - width / 2,
47284
+ y: center.y - height / 2,
47285
+ width,
47286
+ height,
47287
+ opacity: 100,
47288
+ locked: false,
47289
+ name: name ?? void 0
47290
+ });
47291
+ const elements = ctx.excalidrawAPI.getSceneElements();
47292
+ ctx.excalidrawAPI.updateScene({
47293
+ elements: [...elements, frame]
47294
+ });
47295
+ ctx.excalidrawAPI.scrollToContent(frame, { fitToViewport: false });
47296
+ return {
47297
+ success: true,
47298
+ data: {
47299
+ frameId: frame.id,
47300
+ x: Math.round(frame.x),
47301
+ y: Math.round(frame.y),
47302
+ width,
47303
+ height
47304
+ },
47305
+ statusMessage: `Created frame "${name || "Ad Frame"}" (${width}\xD7${height})`
47306
+ };
47307
+ }
47308
+ function execAddRectangle(args, ctx) {
47309
+ const frameId = args.frameId;
47310
+ if (!frameId) {
47311
+ return {
47312
+ success: false,
47313
+ error: "frameId is required",
47314
+ statusMessage: "Failed: missing frameId"
47315
+ };
47316
+ }
47317
+ const rect = newElement5({
47318
+ type: "rectangle",
47319
+ x: args.x,
47320
+ y: args.y,
47321
+ width: args.width,
47322
+ height: args.height,
47323
+ backgroundColor: args.backgroundColor || "transparent",
47324
+ fillStyle: args.fillStyle || "solid",
47325
+ strokeColor: args.strokeColor || "transparent",
47326
+ strokeWidth: args.strokeWidth ?? 0,
47327
+ opacity: args.opacity ?? 100
47328
+ });
47329
+ const elements = ctx.excalidrawAPI.getSceneElements();
47330
+ const frameIndex = elements.findIndex((el) => el.id === frameId);
47331
+ if (frameIndex === -1) {
47332
+ return {
47333
+ success: false,
47334
+ error: `Frame ${frameId} not found`,
47335
+ statusMessage: "Failed: frame not found"
47336
+ };
47337
+ }
47338
+ const frame = elements[frameIndex];
47339
+ const tier = getElementTier(
47340
+ "rectangle",
47341
+ args.x,
47342
+ args.y,
47343
+ args.width,
47344
+ args.height,
47345
+ frame
47346
+ );
47347
+ const updatedRect = { ...rect, frameId };
47348
+ const newElements = [...elements];
47349
+ const insertAt = findInsertIndexByTier(newElements, frameIndex, tier);
47350
+ newElements.splice(insertAt, 0, updatedRect);
47351
+ ctx.excalidrawAPI.updateScene({ elements: newElements });
47352
+ const isBg = tier === 0 /* BG_FILL */;
47353
+ return {
47354
+ success: true,
47355
+ data: {
47356
+ elementId: rect.id,
47357
+ type: "rectangle",
47358
+ x: Math.round(rect.x),
47359
+ y: Math.round(rect.y),
47360
+ width: Math.round(rect.width),
47361
+ height: Math.round(rect.height),
47362
+ backgroundColor: rect.backgroundColor,
47363
+ isBackground: isBg
47364
+ },
47365
+ statusMessage: `Added ${isBg ? "background " : ""}rectangle (${Math.round(
47366
+ rect.width
47367
+ )}\xD7${Math.round(rect.height)}) with fill ${rect.backgroundColor}`
47368
+ };
47369
+ }
47370
+ function execAddText(args, ctx) {
47371
+ const frameId = args.frameId;
47372
+ if (!frameId) {
47373
+ return {
47374
+ success: false,
47375
+ error: "frameId is required",
47376
+ statusMessage: "Failed: missing frameId"
47377
+ };
47378
+ }
47379
+ const textEl = newTextElement4({
47380
+ x: args.x,
47381
+ y: args.y,
47382
+ text: args.text,
47383
+ fontSize: args.fontSize || 20,
47384
+ fontFamily: args.fontFamily || 2,
47385
+ strokeColor: args.strokeColor || "#000000",
47386
+ textAlign: args.textAlign || "left",
47387
+ width: args.width
47388
+ });
47389
+ const elements = ctx.excalidrawAPI.getSceneElements();
47390
+ const frameIndex = elements.findIndex((el) => el.id === frameId);
47391
+ if (frameIndex === -1) {
47392
+ return {
47393
+ success: false,
47394
+ error: `Frame ${frameId} not found`,
47395
+ statusMessage: "Failed: frame not found"
47396
+ };
47397
+ }
47398
+ const updatedText = { ...textEl, frameId };
47399
+ const newElements = [...elements];
47400
+ const insertAt = findInsertIndexByTier(
47401
+ newElements,
47402
+ frameIndex,
47403
+ 3 /* TEXT */
47404
+ );
47405
+ newElements.splice(insertAt, 0, updatedText);
47406
+ ctx.excalidrawAPI.updateScene({ elements: newElements });
47407
+ return {
47408
+ success: true,
47409
+ data: {
47410
+ elementId: textEl.id,
47411
+ type: "text",
47412
+ x: Math.round(textEl.x),
47413
+ y: Math.round(textEl.y),
47414
+ text: args.text,
47415
+ fontSize: args.fontSize || 20
47416
+ },
47417
+ statusMessage: `Added text "${args.text.slice(0, 40)}${args.text.length > 40 ? "\u2026" : ""}"`
47418
+ };
47419
+ }
47420
+ async function execGenerateImage(args, ctx) {
47421
+ const frameId = args.frameId;
47422
+ const prompt = args.prompt;
47423
+ const x = args.x;
47424
+ const y = args.y;
47425
+ const width = args.width;
47426
+ const height = args.height;
47427
+ if (!frameId || !prompt) {
47428
+ return {
47429
+ success: false,
47430
+ error: "frameId and prompt are required",
47431
+ statusMessage: "Failed: missing required parameters"
47432
+ };
47433
+ }
47434
+ if (!ctx.geminiApiKey) {
47435
+ return {
47436
+ success: false,
47437
+ error: "No Gemini API key configured. Set geminiApiKey prop or VITE_APP_GEMINI_API_KEY.",
47438
+ statusMessage: "Failed: no Gemini API key"
47439
+ };
47440
+ }
47441
+ const elements = ctx.excalidrawAPI.getSceneElements();
47442
+ const frameIndex = elements.findIndex((el) => el.id === frameId);
47443
+ if (frameIndex === -1) {
47444
+ return {
47445
+ success: false,
47446
+ error: `Frame ${frameId} not found`,
47447
+ statusMessage: "Failed: frame not found"
47448
+ };
47449
+ }
47450
+ const aspectRatio = width >= height ? width / height > 1.3 ? "16:9" : "1:1" : "9:16";
47451
+ try {
47452
+ const imageDataUrl = await callGeminiAPI(
47453
+ prompt,
47454
+ "gemini-2.5-flash-image",
47455
+ aspectRatio,
47456
+ "1K",
47457
+ false,
47458
+ false,
47459
+ ctx.geminiApiKey,
47460
+ void 0,
47461
+ ctx.signal
47462
+ );
47463
+ if (ctx.signal?.aborted) {
47464
+ return {
47465
+ success: false,
47466
+ error: "Cancelled",
47467
+ statusMessage: "Image generation cancelled"
47468
+ };
47469
+ }
47470
+ const fileId = nanoid2();
47471
+ const imageEl = newImageElement2({
47472
+ type: "image",
47473
+ x,
47474
+ y,
47475
+ width,
47476
+ height,
47477
+ fileId,
47478
+ status: "pending",
47479
+ scale: [1, 1]
47480
+ });
47481
+ const currentElements = ctx.excalidrawAPI.getSceneElements();
47482
+ const currentFrameIndex = currentElements.findIndex(
47483
+ (el) => el.id === frameId
47484
+ );
47485
+ if (currentFrameIndex === -1) {
47486
+ return {
47487
+ success: false,
47488
+ error: `Frame ${frameId} was removed during image generation`,
47489
+ statusMessage: "Failed: frame no longer exists"
47490
+ };
47491
+ }
47492
+ const updatedImage = { ...imageEl, frameId };
47493
+ const newElements = [...currentElements];
47494
+ const insertAt = findInsertIndexByTier(
47495
+ newElements,
47496
+ currentFrameIndex,
47497
+ 1 /* IMAGE */
47498
+ );
47499
+ newElements.splice(insertAt, 0, updatedImage);
47500
+ ctx.excalidrawAPI.updateScene({ elements: newElements });
47501
+ ctx.excalidrawAPI.addFiles([
47502
+ {
47503
+ id: fileId,
47504
+ dataURL: imageDataUrl,
47505
+ mimeType: getMimeTypeFromDataURL(imageDataUrl),
47506
+ created: Date.now(),
47507
+ lastRetrieved: Date.now()
47508
+ }
47509
+ ]);
47510
+ return {
47511
+ success: true,
47512
+ data: {
47513
+ elementId: imageEl.id,
47514
+ type: "image",
47515
+ x: Math.round(x),
47516
+ y: Math.round(y),
47517
+ width: Math.round(width),
47518
+ height: Math.round(height)
47519
+ },
47520
+ statusMessage: `Generated image from prompt "${prompt.slice(0, 50)}${prompt.length > 50 ? "\u2026" : ""}"`
47521
+ };
47522
+ } catch (err) {
47523
+ const message = err instanceof Error ? err.message : "Image generation failed";
47524
+ return {
47525
+ success: false,
47526
+ error: message,
47527
+ statusMessage: `Image generation failed: ${message}`
47528
+ };
47529
+ }
47530
+ }
47531
+ function execUpdateElement(args, ctx) {
47532
+ const elementId = args.elementId;
47533
+ if (!elementId) {
47534
+ return {
47535
+ success: false,
47536
+ error: "elementId is required",
47537
+ statusMessage: "Failed: missing elementId"
47538
+ };
47539
+ }
47540
+ const elements = ctx.excalidrawAPI.getSceneElements();
47541
+ const element = elements.find((el) => el.id === elementId);
47542
+ if (!element) {
47543
+ return {
47544
+ success: false,
47545
+ error: `Element ${elementId} not found`,
47546
+ statusMessage: "Failed: element not found"
47547
+ };
47548
+ }
47549
+ const updates = {};
47550
+ const allowedKeys = [
47551
+ "x",
47552
+ "y",
47553
+ "width",
47554
+ "height",
47555
+ "backgroundColor",
47556
+ "strokeColor",
47557
+ "opacity",
47558
+ "fontSize",
47559
+ "text",
47560
+ "fillStyle"
47561
+ ];
47562
+ for (const key of allowedKeys) {
47563
+ if (args[key] !== void 0) {
47564
+ updates[key] = args[key];
47565
+ }
47566
+ }
47567
+ if (Object.keys(updates).length === 0) {
47568
+ return {
47569
+ success: false,
47570
+ error: "No valid properties to update",
47571
+ statusMessage: "Failed: nothing to update"
47572
+ };
47573
+ }
47574
+ if (updates.text !== void 0 && element.type === "text" && (updates.width === void 0 || updates.height === void 0)) {
47575
+ return {
47576
+ success: false,
47577
+ error: "Updating text content requires providing new width and height to avoid clipping.",
47578
+ statusMessage: "Failed: text update requires width and height"
47579
+ };
47580
+ }
47581
+ ctx.excalidrawAPI.mutateElement(element, updates);
47582
+ return {
47583
+ success: true,
47584
+ data: { elementId, updated: Object.keys(updates) },
47585
+ statusMessage: `Updated element (${Object.keys(updates).join(", ")})`
47586
+ };
47587
+ }
47588
+ function execDeleteElement(args, ctx) {
47589
+ const elementId = args.elementId;
47590
+ if (!elementId) {
47591
+ return {
47592
+ success: false,
47593
+ error: "elementId is required",
47594
+ statusMessage: "Failed: missing elementId"
47595
+ };
47596
+ }
47597
+ const elements = ctx.excalidrawAPI.getSceneElements();
47598
+ const element = elements.find((el) => el.id === elementId);
47599
+ if (!element) {
47600
+ return {
47601
+ success: false,
47602
+ error: `Element ${elementId} not found`,
47603
+ statusMessage: "Failed: element not found"
47604
+ };
47605
+ }
47606
+ ctx.excalidrawAPI.mutateElement(element, { isDeleted: true });
47607
+ return {
47608
+ success: true,
47609
+ data: { elementId },
47610
+ statusMessage: `Deleted element ${elementId}`
47611
+ };
47612
+ }
47613
+ function execGetFrameElements(args, ctx) {
47614
+ const frameId = args.frameId;
47615
+ if (!frameId) {
47616
+ return {
47617
+ success: false,
47618
+ error: "frameId is required",
47619
+ statusMessage: "Failed: missing frameId"
47620
+ };
47621
+ }
47622
+ const elements = ctx.excalidrawAPI.getSceneElements();
47623
+ const frame = elements.find((el) => el.id === frameId);
47624
+ if (!frame) {
47625
+ return {
47626
+ success: false,
47627
+ error: `Frame ${frameId} not found`,
47628
+ statusMessage: "Failed: frame not found"
47629
+ };
47630
+ }
47631
+ const children = getFrameChildren7(elements, frameId);
47632
+ const serialized = serializeElements(children);
47633
+ return {
47634
+ success: true,
47635
+ data: {
47636
+ frameId,
47637
+ frame: serializeElement(frame),
47638
+ children: serialized,
47639
+ childCount: serialized.length
47640
+ },
47641
+ statusMessage: `Found ${serialized.length} elements in frame`
47642
+ };
47643
+ }
47644
+
47645
+ // components/ai-chat/agentLoop.ts
47646
+ var SYSTEM_PROMPT = `You are an AI ad designer assistant embedded in a canvas editor (Excalidraw-based). You create advertisements, graphics, and visual compositions by calling tools that manipulate canvas elements.
47647
+
47648
+ ## Your Capabilities
47649
+ You have tools to: create frames, add rectangles (for backgrounds/shapes), add text, generate AI images, update elements, delete elements, and inspect frame contents.
47650
+
47651
+ ## Design Process
47652
+ When asked to create an ad or visual from scratch:
47653
+ 1. **Create a frame** first (default 512\xD7512 unless the user specifies a size). This is the artboard.
47654
+ 2. **Add a background** \u2014 use add_rectangle at the frame's exact position and size with a solid fill color. This is the base layer.
47655
+ 3. **Generate a hero image** \u2014 use generate_image to create the main visual. Position it within the frame. It can be full-bleed (same size/position as frame) or partial.
47656
+ 4. **Add headline text** \u2014 large, bold text. Position it where it's readable against the background/image. Use contrasting colors (white text on dark images, dark text on light backgrounds).
47657
+ 5. **Add subhead text** \u2014 smaller supporting text below the headline.
47658
+
47659
+ ## Layout & Z-ordering Rules
47660
+ - Elements are rendered in insertion order. Add background rectangle FIRST, then images, then text ON TOP.
47661
+ - All element positions use absolute canvas coordinates. The frame's x,y is the top-left corner.
47662
+ - To place an element at a frame's top-left: use the frame's x,y values.
47663
+ - To center text horizontally in a frame: set text x = frame.x + (frame.width - estimated_text_width) / 2
47664
+ - Leave padding from edges (at least 20px from frame borders for text).
47665
+ - Headline font size: 28-48px depending on frame size. Subhead: 16-24px.
47666
+
47667
+ ## Color & Typography
47668
+ - Use font family 2 (Nunito/clean sans-serif) for professional ads.
47669
+ - For dark backgrounds: use white (#ffffff) or light text.
47670
+ - For light backgrounds: use dark (#1a1a2e or #000000) text.
47671
+ - Popular ad background colors: deep navy (#1a1a2e), warm red (#c0392b), forest green (#27ae60), elegant black (#111111), soft cream (#fdf6e3).
47672
+
47673
+ ## When modifying existing elements (@ context provided)
47674
+ - The user may reference existing elements via @ mentions. Their current state will be described in the message.
47675
+ - Use get_frame_elements to see the full current state of a frame before making changes.
47676
+ - Use update_element to modify properties. Use delete_element to remove things.
47677
+ - Preserve existing elements unless explicitly asked to change them.
47678
+
47679
+ ## Image Generation Tips
47680
+ - Write detailed, descriptive prompts for generate_image. Include style, mood, composition, colors.
47681
+ - Example: "A steaming bowl of creamy pasta on a rustic wooden table, warm golden lighting, top-down view, food photography style, appetizing, high quality"
47682
+ - The image will be generated by Gemini AI and placed at the coordinates you specify.
47683
+
47684
+ ## Important
47685
+ - Always respond with a brief summary of what you created/modified after completing the tools.
47686
+ - If the user's request is unclear, use the available tools to create a reasonable default and explain what you did.
47687
+ - Coordinates: frame x,y is the top-left corner. Width extends right, height extends down.`;
47688
+ var MAX_ITERATIONS = 15;
47689
+ async function callOpenRouter(messages, apiKey, signal, webSearchEnabled) {
47690
+ const model = webSearchEnabled ? "openai/gpt-4.1-mini:online" : "openai/gpt-4.1-mini";
47691
+ const body = {
47692
+ model,
47693
+ messages: messages.map((m) => {
47694
+ if (m.role === "assistant" && m.tool_calls) {
47695
+ return {
47696
+ role: m.role,
47697
+ content: m.content,
47698
+ tool_calls: m.tool_calls
47699
+ };
47700
+ }
47701
+ if (m.role === "tool") {
47702
+ return {
47703
+ role: m.role,
47704
+ tool_call_id: m.tool_call_id,
47705
+ content: m.content
47706
+ };
47707
+ }
47708
+ return { role: m.role, content: m.content };
47709
+ }),
47710
+ tools: CANVAS_TOOLS,
47711
+ tool_choice: "auto"
47712
+ };
47713
+ if (webSearchEnabled) {
47714
+ body.plugins = [{ id: "web", max_results: 5 }];
47715
+ }
47716
+ const response = await fetch(
47717
+ "https://openrouter.ai/api/v1/chat/completions",
47718
+ {
47719
+ method: "POST",
47720
+ signal,
47721
+ headers: {
47722
+ Authorization: `Bearer ${apiKey}`,
47723
+ "Content-Type": "application/json"
47724
+ },
47725
+ body: JSON.stringify(body)
47726
+ }
47727
+ );
47728
+ if (!response.ok) {
47729
+ let message = `OpenRouter error ${response.status}`;
47730
+ try {
47731
+ const err = await response.json();
47732
+ if (err?.error?.message) {
47733
+ message = err.error.message;
47734
+ }
47735
+ } catch {
47736
+ }
47737
+ throw new Error(message);
47738
+ }
47739
+ const data = await response.json();
47740
+ const choice = data?.choices?.[0]?.message;
47741
+ if (!choice) {
47742
+ throw new Error("No response returned by the API.");
47743
+ }
47744
+ return {
47745
+ content: choice.content ?? null,
47746
+ tool_calls: choice.tool_calls ?? void 0
47747
+ };
47748
+ }
47749
+ async function runAgentLoop(opts) {
47750
+ const {
47751
+ userMessages,
47752
+ elementContext,
47753
+ frameScreenshot,
47754
+ fileAttachments,
47755
+ webSearchEnabled,
47756
+ apiKey,
47757
+ toolCtx,
47758
+ onUpdate,
47759
+ signal,
47760
+ onBeforeImageGen,
47761
+ onAfterImageGen
47762
+ } = opts;
47763
+ const messages = [{ role: "system", content: SYSTEM_PROMPT }];
47764
+ if (elementContext) {
47765
+ messages.push({
47766
+ role: "system",
47767
+ content: `The user has referenced a frame (via @ mention). Here are the frame details and its child elements. Use these IDs and properties when modifying or working with these elements:
47768
+
47769
+ ${elementContext}`
47770
+ });
47771
+ }
47772
+ for (let i = 0; i < userMessages.length; i++) {
47773
+ const msg = userMessages[i];
47774
+ const isLast = i === userMessages.length - 1;
47775
+ const hasMultimodal = isLast && msg.role === "user" && (frameScreenshot || fileAttachments && fileAttachments.some((a) => a.type === "image"));
47776
+ const hasTextFiles = isLast && msg.role === "user" && fileAttachments && fileAttachments.some((a) => a.type === "text");
47777
+ if (hasMultimodal || hasTextFiles) {
47778
+ const contentParts = [];
47779
+ if (frameScreenshot) {
47780
+ contentParts.push({
47781
+ type: "image_url",
47782
+ image_url: { url: frameScreenshot }
47783
+ });
47784
+ }
47785
+ if (fileAttachments) {
47786
+ for (const att of fileAttachments) {
47787
+ if (att.type === "image" && att.dataUrl) {
47788
+ contentParts.push({
47789
+ type: "image_url",
47790
+ image_url: { url: att.dataUrl }
47791
+ });
47792
+ }
47793
+ }
47794
+ }
47795
+ let text = msg.content;
47796
+ if (frameScreenshot) {
47797
+ text = `[Screenshot of the referenced frame is attached above]
47798
+
47799
+ ${text}`;
47800
+ }
47801
+ if (fileAttachments) {
47802
+ const textFiles = fileAttachments.filter((a) => a.type === "text");
47803
+ if (textFiles.length > 0) {
47804
+ text += `
47805
+
47806
+ ${textFiles.map((f) => `[File: ${f.name}]
47807
+ ${f.textContent}`).join("\n\n")}`;
47808
+ }
47809
+ }
47810
+ contentParts.push({ type: "text", text });
47811
+ messages.push({ role: "user", content: contentParts });
47812
+ } else {
47813
+ messages.push({ role: msg.role, content: msg.content });
47814
+ }
47815
+ }
47816
+ const toolActions = [];
47817
+ let iterations = 0;
47818
+ while (iterations < MAX_ITERATIONS) {
47819
+ iterations++;
47820
+ onUpdate({
47821
+ type: "status",
47822
+ message: iterations === 1 ? "Thinking\u2026" : "Processing tool results\u2026"
47823
+ });
47824
+ const response = await callOpenRouter(
47825
+ messages,
47826
+ apiKey,
47827
+ signal,
47828
+ webSearchEnabled
47829
+ );
47830
+ if (!response.tool_calls || response.tool_calls.length === 0) {
47831
+ const reply = response.content || "Done!";
47832
+ onUpdate({ type: "final", message: reply });
47833
+ return { reply, toolActions };
47834
+ }
47835
+ messages.push({
47836
+ role: "assistant",
47837
+ content: response.content,
47838
+ tool_calls: response.tool_calls
47839
+ });
47840
+ for (const toolCall of response.tool_calls) {
47841
+ const { name, arguments: rawArgs } = toolCall.function;
47842
+ let parsedArgs = {};
47843
+ try {
47844
+ parsedArgs = JSON.parse(rawArgs);
47845
+ } catch {
47846
+ }
47847
+ onUpdate({
47848
+ type: "tool_start",
47849
+ message: formatToolStartMessage(name, parsedArgs)
47850
+ });
47851
+ if (name === "generate_image" && onBeforeImageGen) {
47852
+ const { allowed, error } = await onBeforeImageGen();
47853
+ if (!allowed) {
47854
+ const gatedResult = {
47855
+ success: false,
47856
+ error: error || "Insufficient credits for image generation",
47857
+ statusMessage: error || "Insufficient credits for image generation"
47858
+ };
47859
+ const gatedAction = {
47860
+ toolName: name,
47861
+ args: parsedArgs,
47862
+ result: gatedResult
47863
+ };
47864
+ toolActions.push(gatedAction);
47865
+ onUpdate({
47866
+ type: "tool_done",
47867
+ message: gatedResult.statusMessage,
47868
+ toolAction: gatedAction
47869
+ });
47870
+ messages.push({
47871
+ role: "tool",
47872
+ tool_call_id: toolCall.id,
47873
+ content: JSON.stringify({
47874
+ success: false,
47875
+ error: gatedResult.error
47876
+ })
47877
+ });
47878
+ continue;
47879
+ }
47880
+ }
47881
+ const result = await executeCanvasTool(name, rawArgs, toolCtx);
47882
+ if (name === "generate_image" && result.success && onAfterImageGen) {
47883
+ onAfterImageGen();
47884
+ }
47885
+ const action = {
47886
+ toolName: name,
47887
+ args: parsedArgs,
47888
+ result
47889
+ };
47890
+ toolActions.push(action);
47891
+ onUpdate({
47892
+ type: "tool_done",
47893
+ message: result.statusMessage,
47894
+ toolAction: action
47895
+ });
47896
+ messages.push({
47897
+ role: "tool",
47898
+ tool_call_id: toolCall.id,
47899
+ content: JSON.stringify(
47900
+ result.success ? { success: true, ...result.data } : { success: false, error: result.error }
47901
+ )
47902
+ });
47903
+ }
47904
+ }
47905
+ const fallbackReply = "I've completed the available operations. Let me know if you'd like any adjustments!";
47906
+ onUpdate({ type: "final", message: fallbackReply });
47907
+ return { reply: fallbackReply, toolActions };
47908
+ }
47909
+ function formatToolStartMessage(name, args) {
47910
+ switch (name) {
47911
+ case "create_frame":
47912
+ return `Creating frame (${args.width || 512}\xD7${args.height || 512})\u2026`;
47913
+ case "add_rectangle":
47914
+ return `Adding background rectangle\u2026`;
47915
+ case "add_text":
47916
+ return `Adding text "${String(args.text || "").slice(0, 30)}"\u2026`;
47917
+ case "generate_image":
47918
+ return `Generating image \u2014 this may take a moment\u2026`;
47919
+ case "update_element":
47920
+ return `Updating element\u2026`;
47921
+ case "delete_element":
47922
+ return `Deleting element\u2026`;
47923
+ case "get_frame_elements":
47924
+ return `Inspecting frame contents\u2026`;
47925
+ default:
47926
+ return `Running ${name}\u2026`;
47927
+ }
47928
+ }
47929
+
47930
+ // components/ai-chat/audioUtils.ts
47931
+ async function blobToWavBase64(blob) {
47932
+ const arrayBuffer = await blob.arrayBuffer();
47933
+ const audioCtx = new AudioContext();
47934
+ const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
47935
+ await audioCtx.close();
47936
+ const numChannels = audioBuffer.numberOfChannels;
47937
+ const sampleRate = audioBuffer.sampleRate;
47938
+ const numSamples = audioBuffer.length;
47939
+ const bytesPerSample = 2;
47940
+ const dataSize = numSamples * numChannels * bytesPerSample;
47941
+ const bufferSize = 44 + dataSize;
47942
+ const wavBuffer = new ArrayBuffer(bufferSize);
47943
+ const view = new DataView(wavBuffer);
47944
+ const writeStr = (offset2, str) => {
47945
+ for (let i = 0; i < str.length; i++) {
47946
+ view.setUint8(offset2 + i, str.charCodeAt(i));
47947
+ }
47948
+ };
47949
+ writeStr(0, "RIFF");
47950
+ view.setUint32(4, 36 + dataSize, true);
47951
+ writeStr(8, "WAVE");
47952
+ writeStr(12, "fmt ");
47953
+ view.setUint32(16, 16, true);
47954
+ view.setUint16(20, 1, true);
47955
+ view.setUint16(22, numChannels, true);
47956
+ view.setUint32(24, sampleRate, true);
47957
+ view.setUint32(28, sampleRate * numChannels * bytesPerSample, true);
47958
+ view.setUint16(32, numChannels * bytesPerSample, true);
47959
+ view.setUint16(34, 16, true);
47960
+ writeStr(36, "data");
47961
+ view.setUint32(40, dataSize, true);
47962
+ let offset = 44;
47963
+ for (let i = 0; i < numSamples; i++) {
47964
+ for (let ch = 0; ch < numChannels; ch++) {
47965
+ const sample = audioBuffer.getChannelData(ch)[i];
47966
+ const clamped = Math.max(-1, Math.min(1, sample));
47967
+ const int16 = clamped < 0 ? clamped * 32768 : clamped * 32767;
47968
+ view.setInt16(offset, int16, true);
47969
+ offset += 2;
47970
+ }
47971
+ }
47972
+ const uint8 = new Uint8Array(wavBuffer);
47973
+ const CHUNK = 8192;
47974
+ let binary = "";
47975
+ for (let i = 0; i < uint8.length; i += CHUNK) {
47976
+ binary += String.fromCharCode(...uint8.subarray(i, i + CHUNK));
47977
+ }
47978
+ return btoa(binary);
47979
+ }
47980
+ async function transcribeAudio(base64Audio, format, apiKey) {
47981
+ const response = await fetch(
47982
+ "https://openrouter.ai/api/v1/chat/completions",
47983
+ {
47984
+ method: "POST",
47985
+ headers: {
47986
+ Authorization: `Bearer ${apiKey}`,
47987
+ "Content-Type": "application/json"
47988
+ },
47989
+ body: JSON.stringify({
47990
+ model: "mistralai/voxtral-small-24b-2507",
47991
+ messages: [
47992
+ {
47993
+ role: "user",
47994
+ content: [
47995
+ {
47996
+ type: "text",
47997
+ text: "Transcribe this audio. Return only the transcription text, nothing else."
47998
+ },
47999
+ {
48000
+ type: "input_audio",
48001
+ input_audio: { data: base64Audio, format }
48002
+ }
48003
+ ]
48004
+ }
48005
+ ]
48006
+ })
48007
+ }
48008
+ );
48009
+ if (!response.ok) {
48010
+ let message = `Transcription error ${response.status}`;
48011
+ try {
48012
+ const err = await response.json();
48013
+ if (err?.error?.message) {
48014
+ message = err.error.message;
48015
+ }
48016
+ } catch {
48017
+ }
48018
+ throw new Error(message);
48019
+ }
48020
+ const data = await response.json();
48021
+ return data?.choices?.[0]?.message?.content || "";
48022
+ }
48023
+
48024
+ // components/ui/chat-container.tsx
48025
+ import { StickToBottom } from "use-stick-to-bottom";
46521
48026
  import { jsx as jsx177 } from "react/jsx-runtime";
48027
+ function ChatContainerRoot({
48028
+ children,
48029
+ className,
48030
+ ...props
48031
+ }) {
48032
+ return /* @__PURE__ */ jsx177(
48033
+ StickToBottom,
48034
+ {
48035
+ className: cn("flex overflow-y-auto", className),
48036
+ resize: "smooth",
48037
+ initial: "instant",
48038
+ role: "log",
48039
+ ...props,
48040
+ children
48041
+ }
48042
+ );
48043
+ }
48044
+ function ChatContainerContent({
48045
+ children,
48046
+ className,
48047
+ ...props
48048
+ }) {
48049
+ return /* @__PURE__ */ jsx177(
48050
+ StickToBottom.Content,
48051
+ {
48052
+ className: cn("flex w-full flex-col", className),
48053
+ ...props,
48054
+ children
48055
+ }
48056
+ );
48057
+ }
48058
+
48059
+ // components/ui/markdown.tsx
48060
+ import { marked } from "marked";
48061
+ import { memo as memo6, useId, useMemo as useMemo13 } from "react";
48062
+ import ReactMarkdown from "react-markdown";
48063
+ import remarkBreaks from "remark-breaks";
48064
+ import remarkGfm from "remark-gfm";
48065
+
48066
+ // components/ui/code-block.tsx
48067
+ import { useEffect as useEffect54, useState as useState48 } from "react";
48068
+ import { codeToHtml } from "shiki";
48069
+ import { jsx as jsx178 } from "react/jsx-runtime";
48070
+ function CodeBlock({ children, className, ...props }) {
48071
+ return /* @__PURE__ */ jsx178(
48072
+ "div",
48073
+ {
48074
+ className: cn(
48075
+ "not-prose flex w-full flex-col overflow-clip border",
48076
+ "border-zinc-200 bg-white text-zinc-950 rounded-xl dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
48077
+ className
48078
+ ),
48079
+ ...props,
48080
+ children
48081
+ }
48082
+ );
48083
+ }
48084
+ function CodeBlockCode({
48085
+ code,
48086
+ language = "tsx",
48087
+ theme = "github-light",
48088
+ className,
48089
+ ...props
48090
+ }) {
48091
+ const [highlightedHtml, setHighlightedHtml] = useState48(null);
48092
+ useEffect54(() => {
48093
+ async function highlight() {
48094
+ if (!code) {
48095
+ setHighlightedHtml("<pre><code></code></pre>");
48096
+ return;
48097
+ }
48098
+ const html = await codeToHtml(code, { lang: language, theme });
48099
+ setHighlightedHtml(html);
48100
+ }
48101
+ highlight();
48102
+ }, [code, language, theme]);
48103
+ const classNames = cn(
48104
+ "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4",
48105
+ className
48106
+ );
48107
+ return highlightedHtml ? /* @__PURE__ */ jsx178(
48108
+ "div",
48109
+ {
48110
+ className: classNames,
48111
+ dangerouslySetInnerHTML: { __html: highlightedHtml },
48112
+ ...props
48113
+ }
48114
+ ) : /* @__PURE__ */ jsx178("div", { className: classNames, ...props, children: /* @__PURE__ */ jsx178("pre", { children: /* @__PURE__ */ jsx178("code", { children: code }) }) });
48115
+ }
48116
+
48117
+ // components/ui/markdown.tsx
48118
+ import { Fragment as Fragment33, jsx as jsx179 } from "react/jsx-runtime";
48119
+ function parseMarkdownIntoBlocks(markdown) {
48120
+ const tokens = marked.lexer(markdown);
48121
+ return tokens.map((token) => token.raw);
48122
+ }
48123
+ function extractLanguage(className) {
48124
+ if (!className) {
48125
+ return "plaintext";
48126
+ }
48127
+ const match = className.match(/language-(\w+)/);
48128
+ return match ? match[1] : "plaintext";
48129
+ }
48130
+ var INITIAL_COMPONENTS = {
48131
+ code: function CodeComponent({ className, children, ...props }) {
48132
+ const isInline = !props.node?.position?.start.line || props.node?.position?.start.line === props.node?.position?.end.line;
48133
+ if (isInline) {
48134
+ return /* @__PURE__ */ jsx179(
48135
+ "span",
48136
+ {
48137
+ className: cn(
48138
+ "bg-zinc-50 rounded-sm px-1 font-mono text-sm dark:bg-zinc-900",
48139
+ className
48140
+ ),
48141
+ ...props,
48142
+ children
48143
+ }
48144
+ );
48145
+ }
48146
+ const language = extractLanguage(className);
48147
+ return /* @__PURE__ */ jsx179(CodeBlock, { className, children: /* @__PURE__ */ jsx179(CodeBlockCode, { code: children, language }) });
48148
+ },
48149
+ pre: function PreComponent({ children }) {
48150
+ return /* @__PURE__ */ jsx179(Fragment33, { children });
48151
+ }
48152
+ };
48153
+ var MemoizedMarkdownBlock = memo6(
48154
+ ({
48155
+ content,
48156
+ components = INITIAL_COMPONENTS
48157
+ }) => {
48158
+ return /* @__PURE__ */ jsx179(
48159
+ ReactMarkdown,
48160
+ {
48161
+ remarkPlugins: [remarkGfm, remarkBreaks],
48162
+ components,
48163
+ children: content
48164
+ }
48165
+ );
48166
+ },
48167
+ (prevProps, nextProps) => {
48168
+ return prevProps.content === nextProps.content;
48169
+ }
48170
+ );
48171
+ MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
48172
+ function MarkdownComponent({
48173
+ children,
48174
+ id,
48175
+ className,
48176
+ components = INITIAL_COMPONENTS
48177
+ }) {
48178
+ const generatedId = useId();
48179
+ const blockId = id ?? generatedId;
48180
+ const blocks = useMemo13(() => parseMarkdownIntoBlocks(children), [children]);
48181
+ return /* @__PURE__ */ jsx179("div", { className, children: blocks.map((block, index) => /* @__PURE__ */ jsx179(
48182
+ MemoizedMarkdownBlock,
48183
+ {
48184
+ content: block,
48185
+ components
48186
+ },
48187
+ `${blockId}-block-${index}`
48188
+ )) });
48189
+ }
48190
+ var Markdown = memo6(MarkdownComponent);
48191
+ Markdown.displayName = "Markdown";
48192
+
48193
+ // components/ui/avatar.tsx
48194
+ import * as React55 from "react";
48195
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
48196
+ import { jsx as jsx180 } from "react/jsx-runtime";
48197
+ var Avatar2 = React55.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx180(
48198
+ AvatarPrimitive.Root,
48199
+ {
48200
+ ref,
48201
+ className: cn(
48202
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
48203
+ className
48204
+ ),
48205
+ ...props
48206
+ }
48207
+ ));
48208
+ Avatar2.displayName = AvatarPrimitive.Root.displayName;
48209
+ var AvatarImage = React55.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx180(
48210
+ AvatarPrimitive.Image,
48211
+ {
48212
+ ref,
48213
+ className: cn("aspect-square h-full w-full", className),
48214
+ ...props
48215
+ }
48216
+ ));
48217
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
48218
+ var AvatarFallback = React55.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx180(
48219
+ AvatarPrimitive.Fallback,
48220
+ {
48221
+ ref,
48222
+ className: cn(
48223
+ "flex h-full w-full items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800",
48224
+ className
48225
+ ),
48226
+ ...props
48227
+ }
48228
+ ));
48229
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
48230
+
48231
+ // components/ui/message.tsx
48232
+ import { jsx as jsx181, jsxs as jsxs99 } from "react/jsx-runtime";
48233
+ var Message = ({ children, className, ...props }) => /* @__PURE__ */ jsx181("div", { className: cn("flex gap-3", className), ...props, children });
48234
+ var MessageContent = ({
48235
+ children,
48236
+ markdown = false,
48237
+ className,
48238
+ ...props
48239
+ }) => {
48240
+ const classNames = cn(
48241
+ "rounded-lg p-2 text-zinc-950 bg-zinc-100 prose break-words whitespace-normal dark:text-zinc-50 dark:bg-zinc-800",
48242
+ className
48243
+ );
48244
+ return markdown ? /* @__PURE__ */ jsx181(Markdown, { className: classNames, ...props, children }) : /* @__PURE__ */ jsx181("div", { className: classNames, ...props, children });
48245
+ };
48246
+ var MessageActions = ({
48247
+ children,
48248
+ className,
48249
+ ...props
48250
+ }) => /* @__PURE__ */ jsx181(
48251
+ "div",
48252
+ {
48253
+ className: cn(
48254
+ "text-zinc-500 flex items-center gap-2 dark:text-zinc-400",
48255
+ className
48256
+ ),
48257
+ ...props,
48258
+ children
48259
+ }
48260
+ );
48261
+ var MessageAction = ({
48262
+ tooltip,
48263
+ children,
48264
+ className,
48265
+ side = "top",
48266
+ ...props
48267
+ }) => {
48268
+ return /* @__PURE__ */ jsx181(TooltipProvider, { children: /* @__PURE__ */ jsxs99(Tooltip2, { ...props, children: [
48269
+ /* @__PURE__ */ jsx181(TooltipTrigger, { asChild: true, children }),
48270
+ /* @__PURE__ */ jsx181(TooltipContent, { side, className, children: tooltip })
48271
+ ] }) });
48272
+ };
48273
+
48274
+ // components/ui/scroll-button.tsx
48275
+ import { ChevronDown } from "lucide-react";
48276
+ import { useStickToBottomContext } from "use-stick-to-bottom";
48277
+
48278
+ // components/ui/button.tsx
48279
+ import * as React56 from "react";
48280
+ import { Slot } from "@radix-ui/react-slot";
48281
+ import { cva } from "class-variance-authority";
48282
+ import { jsx as jsx182 } from "react/jsx-runtime";
48283
+ var buttonVariants = cva(
48284
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-zinc-300",
48285
+ {
48286
+ variants: {
48287
+ variant: {
48288
+ default: "bg-zinc-900 text-zinc-50 shadow hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90",
48289
+ destructive: "bg-red-500 text-zinc-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90",
48290
+ outline: "border border-zinc-200 bg-white shadow-sm hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
48291
+ secondary: "bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80",
48292
+ ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
48293
+ link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50"
48294
+ },
48295
+ size: {
48296
+ default: "h-9 px-4 py-2",
48297
+ sm: "h-8 rounded-md px-3 text-xs",
48298
+ lg: "h-10 rounded-md px-8",
48299
+ icon: "h-9 w-9"
48300
+ }
48301
+ },
48302
+ defaultVariants: {
48303
+ variant: "default",
48304
+ size: "default"
48305
+ }
48306
+ }
48307
+ );
48308
+ var Button2 = React56.forwardRef(
48309
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
48310
+ const Comp = asChild ? Slot : "button";
48311
+ return /* @__PURE__ */ jsx182(
48312
+ Comp,
48313
+ {
48314
+ className: cn(buttonVariants({ variant, size, className })),
48315
+ ref,
48316
+ ...props
48317
+ }
48318
+ );
48319
+ }
48320
+ );
48321
+ Button2.displayName = "Button";
48322
+
48323
+ // components/ui/scroll-button.tsx
48324
+ import { jsx as jsx183 } from "react/jsx-runtime";
48325
+ function ScrollButton({
48326
+ className,
48327
+ variant = "outline",
48328
+ size = "sm",
48329
+ ...props
48330
+ }) {
48331
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
48332
+ return /* @__PURE__ */ jsx183(
48333
+ Button2,
48334
+ {
48335
+ variant,
48336
+ size,
48337
+ className: cn(
48338
+ "h-10 w-10 rounded-full transition-all duration-150 ease-out",
48339
+ !isAtBottom ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-95 opacity-0",
48340
+ className
48341
+ ),
48342
+ onClick: () => scrollToBottom(),
48343
+ ...props,
48344
+ children: /* @__PURE__ */ jsx183(ChevronDown, { className: "h-5 w-5" })
48345
+ }
48346
+ );
48347
+ }
48348
+
48349
+ // components/AIChatPanel.tsx
48350
+ import { jsx as jsx184, jsxs as jsxs100 } from "react/jsx-runtime";
48351
+ function genId() {
48352
+ return Math.random().toString(36).slice(2, 10);
48353
+ }
48354
+ var MAX_ATTACHED_FILES = 3;
48355
+ var MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
48356
+ var SUGGESTIONS = [
48357
+ "Create a pasta ad",
48358
+ "Design a sale banner",
48359
+ "Make a product showcase",
48360
+ "Help me design"
48361
+ ];
48362
+ var AIChatPanel = React57.forwardRef(
48363
+ ({
48364
+ isOpen,
48365
+ onClose,
48366
+ apiKey,
48367
+ excalidrawAPI,
48368
+ geminiApiKey,
48369
+ initialMessages,
48370
+ initialSessionId,
48371
+ initialSessions,
48372
+ onMessagesChange,
48373
+ onSessionCreate,
48374
+ onSessionSwitch,
48375
+ onBeforeSend,
48376
+ onAfterSend,
48377
+ onBeforeImageGen,
48378
+ onAfterImageGen
48379
+ }, ref) => {
48380
+ const [prompt, setPrompt] = useState49("");
48381
+ const [isLoading, setIsLoading] = useState49(false);
48382
+ const [statusText, setStatusText] = useState49("");
48383
+ const [messages, setMessages] = useState49(
48384
+ initialMessages ?? []
48385
+ );
48386
+ const [sessions, setSessions] = useState49(
48387
+ initialSessions ?? []
48388
+ );
48389
+ const [currentSessionId, setCurrentSessionId] = useState49(
48390
+ initialSessionId ?? genId
48391
+ );
48392
+ const [historyOpen, setHistoryOpen] = useState49(false);
48393
+ const [historySearch, setHistorySearch] = useState49("");
48394
+ const [error, setError] = useState49(null);
48395
+ const [frameRef, setFrameRef] = useState49(null);
48396
+ const [mentionMenuOpen, setMentionMenuOpen] = useState49(false);
48397
+ const [availableFrames, setAvailableFrames] = useState49([]);
48398
+ const [attachedFiles, setAttachedFiles] = useState49([]);
48399
+ const [webSearchEnabled, setWebSearchEnabled] = useState49(false);
48400
+ const [isRecording, setIsRecording] = useState49(false);
48401
+ const [isTranscribing, setIsTranscribing] = useState49(false);
48402
+ const [copiedMsgId, setCopiedMsgId] = useState49(null);
48403
+ const historyRef = useRef49(null);
48404
+ const abortControllerRef = useRef49(null);
48405
+ const mentionMenuRef = useRef49(null);
48406
+ const fileInputRef = useRef49(null);
48407
+ const mediaRecorderRef = useRef49(null);
48408
+ const audioChunksRef = useRef49([]);
48409
+ const prevPromptRef = useRef49("");
48410
+ const voiceUsedRef = useRef49(false);
48411
+ useEffect55(() => {
48412
+ const handler = (e) => {
48413
+ if (historyRef.current && !historyRef.current.contains(e.target)) {
48414
+ setHistoryOpen(false);
48415
+ }
48416
+ if (mentionMenuRef.current && !mentionMenuRef.current.contains(e.target)) {
48417
+ setMentionMenuOpen(false);
48418
+ }
48419
+ };
48420
+ document.addEventListener("mousedown", handler);
48421
+ return () => document.removeEventListener("mousedown", handler);
48422
+ }, []);
48423
+ useEffect55(() => {
48424
+ if (!isOpen) {
48425
+ abortControllerRef.current?.abort();
48426
+ }
48427
+ }, [isOpen]);
48428
+ const currentTitle = messages.length > 0 ? messages[0].content.slice(0, 30) + (messages[0].content.length > 30 ? "\u2026" : "") : "New chat";
48429
+ const saveCurrentSession = useCallback24(() => {
48430
+ if (messages.length === 0) {
48431
+ return;
48432
+ }
48433
+ const title = messages[0].content.slice(0, 40) + (messages[0].content.length > 40 ? "\u2026" : "");
48434
+ setSessions((prev) => {
48435
+ const existing = prev.findIndex((s) => s.id === currentSessionId);
48436
+ const updated = { id: currentSessionId, title, messages };
48437
+ if (existing >= 0) {
48438
+ const next = [...prev];
48439
+ next[existing] = updated;
48440
+ return next;
48441
+ }
48442
+ return [updated, ...prev];
48443
+ });
48444
+ }, [messages, currentSessionId]);
48445
+ const handleNewChat = useCallback24(() => {
48446
+ saveCurrentSession();
48447
+ const newSessionId = genId();
48448
+ setMessages([]);
48449
+ setCurrentSessionId(newSessionId);
48450
+ setHistoryOpen(false);
48451
+ setError(null);
48452
+ setPrompt("");
48453
+ setFrameRef(null);
48454
+ setAttachedFiles([]);
48455
+ setStatusText("");
48456
+ if (onSessionCreate) {
48457
+ onSessionCreate({ id: newSessionId, title: "New chat" });
48458
+ }
48459
+ }, [saveCurrentSession, onSessionCreate]);
48460
+ const handleSwitchSession = useCallback24(
48461
+ (session) => {
48462
+ saveCurrentSession();
48463
+ setMessages(session.messages);
48464
+ setCurrentSessionId(session.id);
48465
+ setHistoryOpen(false);
48466
+ setError(null);
48467
+ if (onSessionSwitch) {
48468
+ onSessionSwitch(session.id);
48469
+ }
48470
+ },
48471
+ [saveCurrentSession, onSessionSwitch]
48472
+ );
48473
+ const handlePromptChange = useCallback24(
48474
+ (value) => {
48475
+ const prevValue = prevPromptRef.current;
48476
+ setPrompt(value);
48477
+ prevPromptRef.current = value;
48478
+ if (excalidrawAPI && value.length > prevValue.length && value.endsWith("@")) {
48479
+ setAvailableFrames(listFrames(excalidrawAPI));
48480
+ setMentionMenuOpen(true);
48481
+ }
48482
+ },
48483
+ [excalidrawAPI]
48484
+ );
48485
+ const handlePickFrame = useCallback24(
48486
+ async (frame) => {
48487
+ if (!excalidrawAPI) {
48488
+ return;
48489
+ }
48490
+ setMentionMenuOpen(false);
48491
+ setPrompt((prev) => {
48492
+ const atIndex2 = prev.lastIndexOf("@");
48493
+ return atIndex2 >= 0 ? prev.slice(0, atIndex2) : prev;
48494
+ });
48495
+ const atIndex = prevPromptRef.current.lastIndexOf("@");
48496
+ prevPromptRef.current = atIndex >= 0 ? prevPromptRef.current.slice(0, atIndex) : prevPromptRef.current;
48497
+ const ctx = getFrameContext(excalidrawAPI, frame.id);
48498
+ if (!ctx) {
48499
+ return;
48500
+ }
48501
+ const screenshot = await captureFrameScreenshot(excalidrawAPI, frame.id);
48502
+ setFrameRef({
48503
+ frameId: frame.id,
48504
+ label: ctx.frameInfo.name,
48505
+ serialized: ctx.serialized,
48506
+ screenshot: screenshot ?? void 0
48507
+ });
48508
+ },
48509
+ [excalidrawAPI]
48510
+ );
48511
+ const handleRemoveRef = useCallback24(() => {
48512
+ setFrameRef(null);
48513
+ }, []);
48514
+ const handleFileUpload = useCallback24(
48515
+ (e) => {
48516
+ const files = e.target.files;
48517
+ if (!files) {
48518
+ return;
48519
+ }
48520
+ const remaining = MAX_ATTACHED_FILES - attachedFiles.length;
48521
+ if (remaining <= 0) {
48522
+ return;
48523
+ }
48524
+ const toAdd = Array.from(files).slice(0, remaining);
48525
+ for (const file2 of toAdd) {
48526
+ if (file2.size > MAX_FILE_SIZE_BYTES) {
48527
+ setError(`"${file2.name}" is too large (max 10 MB).`);
48528
+ continue;
48529
+ }
48530
+ const id = genId();
48531
+ const isImage = file2.type.startsWith("image/");
48532
+ const reader = new FileReader();
48533
+ if (isImage) {
48534
+ reader.onload = () => {
48535
+ setAttachedFiles(
48536
+ (prev) => prev.length < MAX_ATTACHED_FILES ? [
48537
+ ...prev,
48538
+ {
48539
+ id,
48540
+ file: file2,
48541
+ name: file2.name,
48542
+ type: "image",
48543
+ dataUrl: reader.result
48544
+ }
48545
+ ] : prev
48546
+ );
48547
+ };
48548
+ reader.readAsDataURL(file2);
48549
+ } else {
48550
+ reader.onload = () => {
48551
+ setAttachedFiles(
48552
+ (prev) => prev.length < MAX_ATTACHED_FILES ? [
48553
+ ...prev,
48554
+ {
48555
+ id,
48556
+ file: file2,
48557
+ name: file2.name,
48558
+ type: "text",
48559
+ textContent: reader.result
48560
+ }
48561
+ ] : prev
48562
+ );
48563
+ };
48564
+ reader.readAsText(file2);
48565
+ }
48566
+ }
48567
+ if (fileInputRef.current) {
48568
+ fileInputRef.current.value = "";
48569
+ }
48570
+ },
48571
+ [attachedFiles.length]
48572
+ );
48573
+ const handleRemoveFile = useCallback24((fileId) => {
48574
+ setAttachedFiles((prev) => prev.filter((f) => f.id !== fileId));
48575
+ }, []);
48576
+ const toggleWebSearch = useCallback24(() => {
48577
+ setWebSearchEnabled((prev) => !prev);
48578
+ }, []);
48579
+ const handleVoiceInput = useCallback24(async () => {
48580
+ if (isRecording) {
48581
+ mediaRecorderRef.current?.stop();
48582
+ setIsRecording(false);
48583
+ voiceUsedRef.current = true;
48584
+ return;
48585
+ }
48586
+ let stream = null;
48587
+ try {
48588
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
48589
+ const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
48590
+ const mediaRecorder = new MediaRecorder(stream, { mimeType });
48591
+ mediaRecorderRef.current = mediaRecorder;
48592
+ audioChunksRef.current = [];
48593
+ mediaRecorder.ondataavailable = (e) => {
48594
+ if (e.data.size > 0) {
48595
+ audioChunksRef.current.push(e.data);
48596
+ }
48597
+ };
48598
+ mediaRecorder.onstop = async () => {
48599
+ stream?.getTracks().forEach((t2) => t2.stop());
48600
+ const audioBlob = new Blob(audioChunksRef.current, {
48601
+ type: mediaRecorder.mimeType
48602
+ });
48603
+ const normalizedKey = resolveOpenRouterApiKey(apiKey);
48604
+ if (!normalizedKey) {
48605
+ setError("No API key for voice transcription.");
48606
+ return;
48607
+ }
48608
+ setIsTranscribing(true);
48609
+ setStatusText("Transcribing\u2026");
48610
+ try {
48611
+ const wavBase64 = await blobToWavBase64(audioBlob);
48612
+ const transcription = await transcribeAudio(
48613
+ wavBase64,
48614
+ "wav",
48615
+ normalizedKey
48616
+ );
48617
+ const updated = (prevPromptRef.current ? `${prevPromptRef.current} ` : "") + transcription;
48618
+ prevPromptRef.current = updated;
48619
+ setPrompt(updated);
48620
+ } catch (err) {
48621
+ setError(err instanceof Error ? err.message : "Transcription failed");
48622
+ } finally {
48623
+ setIsTranscribing(false);
48624
+ setStatusText("");
48625
+ }
48626
+ };
48627
+ mediaRecorder.start();
48628
+ setIsRecording(true);
48629
+ } catch {
48630
+ stream?.getTracks().forEach((t2) => t2.stop());
48631
+ setError("Could not access microphone. Check browser permissions.");
48632
+ }
48633
+ }, [isRecording, apiKey]);
48634
+ const handleCopy = useCallback24((msgId, content) => {
48635
+ navigator.clipboard.writeText(content).then(
48636
+ () => {
48637
+ setCopiedMsgId(msgId);
48638
+ setTimeout(() => setCopiedMsgId(null), 2e3);
48639
+ },
48640
+ () => {
48641
+ setError("Copy failed \u2014 please grant clipboard permission.");
48642
+ }
48643
+ );
48644
+ }, []);
48645
+ const handleSend = useCallback24(async (textOverride) => {
48646
+ const text = (textOverride ?? prompt).trim();
48647
+ const normalizedKey = resolveOpenRouterApiKey(apiKey);
48648
+ if (!text || isLoading) {
48649
+ return;
48650
+ }
48651
+ if (!normalizedKey) {
48652
+ setError(
48653
+ "No OpenRouter API key. Set VITE_APP_OPENROUTER_API_KEY in .env or pass the key via the `apiKey` prop."
48654
+ );
48655
+ return;
48656
+ }
48657
+ const hadVoice = voiceUsedRef.current;
48658
+ voiceUsedRef.current = false;
48659
+ if (onBeforeSend) {
48660
+ const { allowed, error: creditError } = await onBeforeSend({
48661
+ hasVoice: hadVoice
48662
+ });
48663
+ if (!allowed) {
48664
+ setError(creditError || "Insufficient credits");
48665
+ return;
48666
+ }
48667
+ }
48668
+ const capturedFiles = [...attachedFiles];
48669
+ const capturedWebSearch = webSearchEnabled;
48670
+ const capturedFrameRef = frameRef;
48671
+ const userMsg = {
48672
+ id: genId(),
48673
+ role: "user",
48674
+ content: text,
48675
+ timestamp: /* @__PURE__ */ new Date(),
48676
+ attachments: capturedFiles.length > 0 ? capturedFiles.map((f) => ({
48677
+ name: f.name,
48678
+ type: f.type,
48679
+ dataUrl: f.dataUrl
48680
+ })) : void 0
48681
+ };
48682
+ const nextMessages = [...messages, userMsg];
48683
+ setMessages(nextMessages);
48684
+ setPrompt("");
48685
+ prevPromptRef.current = "";
48686
+ setAttachedFiles([]);
48687
+ setFrameRef(null);
48688
+ const controller = new AbortController();
48689
+ abortControllerRef.current = controller;
48690
+ setIsLoading(true);
48691
+ setError(null);
48692
+ setStatusText("Thinking\u2026");
48693
+ if (!excalidrawAPI) {
48694
+ try {
48695
+ const reply = await callPlainChatAPI(
48696
+ nextMessages,
48697
+ normalizedKey,
48698
+ controller.signal,
48699
+ {
48700
+ webSearchEnabled: capturedWebSearch,
48701
+ attachments: capturedFiles
48702
+ }
48703
+ );
48704
+ const assistantMsg = {
48705
+ id: genId(),
48706
+ role: "assistant",
48707
+ content: reply,
48708
+ timestamp: /* @__PURE__ */ new Date()
48709
+ };
48710
+ const updatedMessages = [...nextMessages, assistantMsg];
48711
+ setMessages(updatedMessages);
48712
+ if (onAfterSend) {
48713
+ onAfterSend({ hasVoice: hadVoice });
48714
+ }
48715
+ if (onMessagesChange) {
48716
+ onMessagesChange(updatedMessages, currentSessionId);
48717
+ }
48718
+ } catch (err) {
48719
+ if (err instanceof Error && err.name !== "AbortError") {
48720
+ setError(err.message);
48721
+ }
48722
+ } finally {
48723
+ abortControllerRef.current = null;
48724
+ setIsLoading(false);
48725
+ setStatusText("");
48726
+ }
48727
+ return;
48728
+ }
48729
+ try {
48730
+ const result = await runAgentLoop({
48731
+ userMessages: nextMessages.map((m) => ({
48732
+ role: m.role,
48733
+ content: m.content
48734
+ })),
48735
+ elementContext: capturedFrameRef?.serialized,
48736
+ frameScreenshot: capturedFrameRef?.screenshot,
48737
+ fileAttachments: capturedFiles.length > 0 ? capturedFiles.map((f) => ({
48738
+ name: f.name,
48739
+ type: f.type,
48740
+ dataUrl: f.dataUrl,
48741
+ textContent: f.textContent
48742
+ })) : void 0,
48743
+ webSearchEnabled: capturedWebSearch,
48744
+ apiKey: normalizedKey,
48745
+ toolCtx: {
48746
+ excalidrawAPI,
48747
+ geminiApiKey: geminiApiKey || "",
48748
+ signal: controller.signal
48749
+ },
48750
+ onUpdate: (update) => {
48751
+ if (update.type !== "final") {
48752
+ setStatusText(update.message);
48753
+ }
48754
+ },
48755
+ signal: controller.signal,
48756
+ onBeforeImageGen,
48757
+ onAfterImageGen
48758
+ });
48759
+ const assistantMsgAgent = {
48760
+ id: genId(),
48761
+ role: "assistant",
48762
+ content: result.reply,
48763
+ timestamp: /* @__PURE__ */ new Date(),
48764
+ toolActions: result.toolActions.length > 0 ? result.toolActions : void 0
48765
+ };
48766
+ const updatedMessagesAgent = [...nextMessages, assistantMsgAgent];
48767
+ setMessages(updatedMessagesAgent);
48768
+ if (onAfterSend) {
48769
+ onAfterSend({ hasVoice: hadVoice });
48770
+ }
48771
+ if (onMessagesChange) {
48772
+ onMessagesChange(updatedMessagesAgent, currentSessionId);
48773
+ }
48774
+ } catch (err) {
48775
+ if (err instanceof Error && err.name !== "AbortError") {
48776
+ setError(err.message);
48777
+ }
48778
+ } finally {
48779
+ abortControllerRef.current = null;
48780
+ setIsLoading(false);
48781
+ setStatusText("");
48782
+ }
48783
+ }, [
48784
+ prompt,
48785
+ isLoading,
48786
+ messages,
48787
+ apiKey,
48788
+ excalidrawAPI,
48789
+ geminiApiKey,
48790
+ frameRef,
48791
+ attachedFiles,
48792
+ webSearchEnabled,
48793
+ currentSessionId,
48794
+ onBeforeSend,
48795
+ onAfterSend,
48796
+ onMessagesChange,
48797
+ onBeforeImageGen,
48798
+ onAfterImageGen
48799
+ ]);
48800
+ useImperativeHandle4(
48801
+ ref,
48802
+ () => ({
48803
+ setMessages,
48804
+ send: (text) => handleSend(text)
48805
+ }),
48806
+ [setMessages, handleSend]
48807
+ );
48808
+ const handleStop = useCallback24(() => {
48809
+ abortControllerRef.current?.abort();
48810
+ }, []);
48811
+ const handleChip = useCallback24((chip) => {
48812
+ setPrompt(chip);
48813
+ }, []);
48814
+ const filteredSessions = sessions.filter(
48815
+ (s) => historySearch ? s.title.toLowerCase().includes(historySearch.toLowerCase()) : true
48816
+ );
48817
+ if (!isOpen) {
48818
+ return null;
48819
+ }
48820
+ return /* @__PURE__ */ jsx184(
48821
+ "div",
48822
+ {
48823
+ className: "acp",
48824
+ onPointerDown: (e) => e.stopPropagation(),
48825
+ onClick: (e) => e.stopPropagation(),
48826
+ children: /* @__PURE__ */ jsxs100("div", { className: "acp-panel", children: [
48827
+ /* @__PURE__ */ jsxs100("div", { className: "acp-header", ref: historyRef, children: [
48828
+ /* @__PURE__ */ jsxs100(
48829
+ "button",
48830
+ {
48831
+ className: "acp-title-btn",
48832
+ onClick: () => setHistoryOpen((v) => !v),
48833
+ title: "Chat history",
48834
+ children: [
48835
+ /* @__PURE__ */ jsx184("span", { children: currentTitle }),
48836
+ /* @__PURE__ */ jsx184(ChevronDown2, { size: 13 })
48837
+ ]
48838
+ }
48839
+ ),
48840
+ /* @__PURE__ */ jsxs100("div", { className: "acp-header-right", children: [
48841
+ /* @__PURE__ */ jsx184(
48842
+ "button",
48843
+ {
48844
+ className: "acp-icon-btn",
48845
+ onClick: handleNewChat,
48846
+ title: "New chat",
48847
+ children: /* @__PURE__ */ jsx184(Plus, { size: 15 })
48848
+ }
48849
+ ),
48850
+ /* @__PURE__ */ jsx184("button", { className: "acp-icon-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsx184(X2, { size: 15 }) })
48851
+ ] }),
48852
+ historyOpen && /* @__PURE__ */ jsxs100("div", { className: "acp-history-dropdown", children: [
48853
+ /* @__PURE__ */ jsxs100("div", { className: "acp-history-search", children: [
48854
+ /* @__PURE__ */ jsx184(Search, { size: 13 }),
48855
+ /* @__PURE__ */ jsx184(
48856
+ "input",
48857
+ {
48858
+ type: "search",
48859
+ placeholder: "Search chats\u2026",
48860
+ value: historySearch,
48861
+ onChange: (e) => setHistorySearch(e.target.value),
48862
+ autoFocus: true
48863
+ }
48864
+ )
48865
+ ] }),
48866
+ /* @__PURE__ */ jsxs100("div", { className: "acp-history-list", children: [
48867
+ /* @__PURE__ */ jsxs100(
48868
+ "button",
48869
+ {
48870
+ className: "acp-history-item acp-history-item--new",
48871
+ onClick: handleNewChat,
48872
+ children: [
48873
+ /* @__PURE__ */ jsx184(Plus, { size: 13 }),
48874
+ /* @__PURE__ */ jsx184("span", { children: "New chat" })
48875
+ ]
48876
+ }
48877
+ ),
48878
+ filteredSessions.map((session) => /* @__PURE__ */ jsxs100(
48879
+ "button",
48880
+ {
48881
+ className: "acp-history-item",
48882
+ onClick: () => handleSwitchSession(session),
48883
+ children: [
48884
+ /* @__PURE__ */ jsx184(MessageSquare, { size: 13 }),
48885
+ /* @__PURE__ */ jsx184("span", { children: session.title })
48886
+ ]
48887
+ },
48888
+ session.id
48889
+ )),
48890
+ filteredSessions.length === 0 && historySearch && /* @__PURE__ */ jsx184("div", { className: "acp-history-empty", children: "No matching chats" })
48891
+ ] })
48892
+ ] })
48893
+ ] }),
48894
+ /* @__PURE__ */ jsx184("div", { className: "acp-messages-wrap", children: messages.length === 0 && !isLoading ? /* @__PURE__ */ jsxs100("div", { className: "acp-empty", children: [
48895
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-icon", children: /* @__PURE__ */ jsx184(MessageSquare, { size: 22 }) }),
48896
+ /* @__PURE__ */ jsxs100("div", { children: [
48897
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-title", children: "AI Ad Designer" }),
48898
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-sub", children: "Describe an ad and I'll create it on the canvas. Type @ to reference frames." })
48899
+ ] }),
48900
+ /* @__PURE__ */ jsx184("div", { className: "acp-chips", children: SUGGESTIONS.map((chip) => /* @__PURE__ */ jsx184(
48901
+ "button",
48902
+ {
48903
+ className: "acp-chip",
48904
+ onClick: () => handleChip(chip),
48905
+ children: chip
48906
+ },
48907
+ chip
48908
+ )) })
48909
+ ] }) : /* @__PURE__ */ jsxs100(ChatContainerRoot, { className: "acp-chat-root", children: [
48910
+ /* @__PURE__ */ jsxs100(ChatContainerContent, { className: "acp-chat-content", children: [
48911
+ messages.map((msg, index) => {
48912
+ const isAssistant = msg.role === "assistant";
48913
+ const isLast = index === messages.length - 1;
48914
+ return /* @__PURE__ */ jsx184(
48915
+ Message,
48916
+ {
48917
+ className: `acp-msg ${isAssistant ? "acp-msg--assistant" : "acp-msg--user"}`,
48918
+ children: isAssistant ? /* @__PURE__ */ jsxs100("div", { className: "acp-msg-inner", children: [
48919
+ msg.toolActions && msg.toolActions.length > 0 && /* @__PURE__ */ jsx184(ToolActionsDisplay, { actions: msg.toolActions }),
48920
+ /* @__PURE__ */ jsx184(
48921
+ MessageContent,
48922
+ {
48923
+ markdown: true,
48924
+ className: "acp-content-assistant",
48925
+ children: msg.content
48926
+ }
48927
+ ),
48928
+ /* @__PURE__ */ jsx184(
48929
+ MessageActions,
48930
+ {
48931
+ className: `acp-msg-actions${isLast ? " acp-msg-actions--visible" : ""}`,
48932
+ children: /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Copy", delayDuration: 100, children: /* @__PURE__ */ jsx184(
48933
+ "button",
48934
+ {
48935
+ className: "acp-action-btn",
48936
+ onClick: () => handleCopy(msg.id, msg.content),
48937
+ children: copiedMsgId === msg.id ? /* @__PURE__ */ jsx184(Check, { size: 14 }) : /* @__PURE__ */ jsx184(Copy, { size: 14 })
48938
+ }
48939
+ ) })
48940
+ }
48941
+ )
48942
+ ] }) : /* @__PURE__ */ jsxs100("div", { className: "acp-msg-inner", children: [
48943
+ msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ jsx184("div", { className: "acp-msg-attachments", children: msg.attachments.map(
48944
+ (att, i) => att.type === "image" && att.dataUrl ? /* @__PURE__ */ jsx184(
48945
+ "img",
48946
+ {
48947
+ src: att.dataUrl,
48948
+ alt: att.name,
48949
+ className: "acp-msg-att-thumb",
48950
+ title: att.name
48951
+ },
48952
+ i
48953
+ ) : /* @__PURE__ */ jsxs100("span", { className: "acp-msg-att-pill", children: [
48954
+ /* @__PURE__ */ jsx184(Paperclip, { size: 11 }),
48955
+ att.name
48956
+ ] }, i)
48957
+ ) }),
48958
+ /* @__PURE__ */ jsx184(MessageContent, { className: "acp-content-user", children: msg.content }),
48959
+ /* @__PURE__ */ jsx184(MessageActions, { className: "acp-msg-actions", children: /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Copy", delayDuration: 100, children: /* @__PURE__ */ jsx184(
48960
+ "button",
48961
+ {
48962
+ className: "acp-action-btn",
48963
+ onClick: () => handleCopy(msg.id, msg.content),
48964
+ children: copiedMsgId === msg.id ? /* @__PURE__ */ jsx184(Check, { size: 14 }) : /* @__PURE__ */ jsx184(Copy, { size: 14 })
48965
+ }
48966
+ ) }) })
48967
+ ] })
48968
+ },
48969
+ msg.id
48970
+ );
48971
+ }),
48972
+ isLoading && /* @__PURE__ */ jsx184(Message, { className: "acp-msg acp-msg--assistant", children: /* @__PURE__ */ jsx184("div", { className: "acp-msg-inner", children: /* @__PURE__ */ jsxs100("div", { className: "acp-status-indicator", children: [
48973
+ /* @__PURE__ */ jsxs100("div", { className: "acp-loading-dots", children: [
48974
+ /* @__PURE__ */ jsx184("span", {}),
48975
+ /* @__PURE__ */ jsx184("span", {}),
48976
+ /* @__PURE__ */ jsx184("span", {})
48977
+ ] }),
48978
+ statusText && /* @__PURE__ */ jsx184("span", { className: "acp-status-text", children: statusText })
48979
+ ] }) }) })
48980
+ ] }),
48981
+ /* @__PURE__ */ jsx184("div", { className: "acp-scroll-anchor", children: /* @__PURE__ */ jsx184(ScrollButton, { className: "acp-scroll-btn" }) })
48982
+ ] }) }),
48983
+ error && /* @__PURE__ */ jsx184("div", { className: "acp-error", children: error }),
48984
+ frameRef && /* @__PURE__ */ jsx184("div", { className: "acp-ref-area", children: /* @__PURE__ */ jsxs100("div", { className: "acp-ref-pill", children: [
48985
+ /* @__PURE__ */ jsx184(AtSign, { size: 12 }),
48986
+ /* @__PURE__ */ jsx184("span", { children: frameRef.label }),
48987
+ /* @__PURE__ */ jsx184(
48988
+ "button",
48989
+ {
48990
+ className: "acp-ref-remove",
48991
+ onClick: handleRemoveRef,
48992
+ title: "Remove reference",
48993
+ children: /* @__PURE__ */ jsx184(X2, { size: 10 })
48994
+ }
48995
+ )
48996
+ ] }) }),
48997
+ attachedFiles.length > 0 && /* @__PURE__ */ jsx184("div", { className: "acp-attachments-area", children: attachedFiles.map((f) => /* @__PURE__ */ jsxs100("div", { className: "acp-attachment-pill", children: [
48998
+ f.type === "image" && f.dataUrl ? /* @__PURE__ */ jsx184(
48999
+ "img",
49000
+ {
49001
+ src: f.dataUrl,
49002
+ alt: f.name,
49003
+ className: "acp-attachment-thumb"
49004
+ }
49005
+ ) : /* @__PURE__ */ jsx184(Paperclip, { size: 12 }),
49006
+ /* @__PURE__ */ jsx184("span", { children: f.name }),
49007
+ /* @__PURE__ */ jsx184(
49008
+ "button",
49009
+ {
49010
+ className: "acp-attachment-remove",
49011
+ onClick: () => handleRemoveFile(f.id),
49012
+ title: "Remove file",
49013
+ children: /* @__PURE__ */ jsx184(X2, { size: 10 })
49014
+ }
49015
+ )
49016
+ ] }, f.id)) }),
49017
+ /* @__PURE__ */ jsx184("div", { className: "acp-input-area", children: /* @__PURE__ */ jsxs100("div", { className: "acp-input-wrapper", ref: mentionMenuRef, children: [
49018
+ mentionMenuOpen && /* @__PURE__ */ jsx184("div", { className: "acp-mention-menu", children: availableFrames.length > 0 ? availableFrames.map((f) => /* @__PURE__ */ jsxs100(
49019
+ "button",
49020
+ {
49021
+ className: "acp-mention-item",
49022
+ onClick: () => handlePickFrame(f),
49023
+ children: [
49024
+ /* @__PURE__ */ jsx184(AtSign, { size: 14 }),
49025
+ /* @__PURE__ */ jsxs100("span", { children: [
49026
+ f.name,
49027
+ " ",
49028
+ /* @__PURE__ */ jsxs100("span", { className: "acp-mention-dim", children: [
49029
+ "(",
49030
+ f.width,
49031
+ "x",
49032
+ f.height,
49033
+ ", ",
49034
+ f.childCount,
49035
+ " items)"
49036
+ ] })
49037
+ ] })
49038
+ ]
49039
+ },
49040
+ f.id
49041
+ )) : /* @__PURE__ */ jsx184("div", { className: "acp-mention-empty", children: "No frames on the canvas. Create a frame first, then type @ to reference it." }) }),
49042
+ /* @__PURE__ */ jsx184(
49043
+ "input",
49044
+ {
49045
+ ref: fileInputRef,
49046
+ type: "file",
49047
+ multiple: true,
49048
+ accept: "image/*,.txt,.json,.csv,.md,.html,.css,.js,.ts,.tsx,.jsx,.py,.xml",
49049
+ onChange: handleFileUpload,
49050
+ style: { display: "none" }
49051
+ }
49052
+ ),
49053
+ /* @__PURE__ */ jsxs100(
49054
+ PromptInput,
49055
+ {
49056
+ isLoading,
49057
+ value: prompt,
49058
+ onValueChange: handlePromptChange,
49059
+ onSubmit: () => handleSend(),
49060
+ className: "acp-prompt-input",
49061
+ children: [
49062
+ /* @__PURE__ */ jsx184(
49063
+ PromptInputTextarea,
49064
+ {
49065
+ placeholder: excalidrawAPI ? "Describe an ad to create\u2026 (@ for frames)" : "Ask anything",
49066
+ className: "acp-textarea"
49067
+ }
49068
+ ),
49069
+ /* @__PURE__ */ jsxs100(PromptInputActions, { className: "acp-actions-bar", children: [
49070
+ /* @__PURE__ */ jsxs100("div", { className: "acp-actions-left", children: [
49071
+ /* @__PURE__ */ jsx184(PromptInputAction, { tooltip: "Attach files (max 3)", children: /* @__PURE__ */ jsx184(
49072
+ "button",
49073
+ {
49074
+ className: "acp-pill-btn acp-pill-btn--icon",
49075
+ onClick: () => fileInputRef.current?.click(),
49076
+ disabled: attachedFiles.length >= MAX_ATTACHED_FILES,
49077
+ children: /* @__PURE__ */ jsx184(Paperclip, { size: 16 })
49078
+ }
49079
+ ) }),
49080
+ /* @__PURE__ */ jsx184(
49081
+ PromptInputAction,
49082
+ {
49083
+ tooltip: webSearchEnabled ? "Web search enabled" : "Enable web search",
49084
+ children: /* @__PURE__ */ jsxs100(
49085
+ "button",
49086
+ {
49087
+ className: `acp-pill-btn${webSearchEnabled ? " acp-pill-btn--active" : ""}`,
49088
+ onClick: toggleWebSearch,
49089
+ children: [
49090
+ /* @__PURE__ */ jsx184(Globe, { size: 15 }),
49091
+ /* @__PURE__ */ jsx184("span", { children: "Search" })
49092
+ ]
49093
+ }
49094
+ )
49095
+ }
49096
+ )
49097
+ ] }),
49098
+ /* @__PURE__ */ jsxs100("div", { className: "acp-actions-right", children: [
49099
+ /* @__PURE__ */ jsx184(
49100
+ PromptInputAction,
49101
+ {
49102
+ tooltip: isTranscribing ? "Transcribing\u2026" : isRecording ? "Stop recording" : "Voice input",
49103
+ children: /* @__PURE__ */ jsx184(
49104
+ "button",
49105
+ {
49106
+ className: `acp-pill-btn acp-pill-btn--icon${isRecording ? " acp-pill-btn--recording" : ""}${isTranscribing ? " acp-pill-btn--transcribing" : ""}`,
49107
+ onClick: handleVoiceInput,
49108
+ disabled: isTranscribing,
49109
+ children: isTranscribing ? /* @__PURE__ */ jsx184(
49110
+ "svg",
49111
+ {
49112
+ width: "16",
49113
+ height: "16",
49114
+ viewBox: "0 0 24 24",
49115
+ fill: "none",
49116
+ stroke: "currentColor",
49117
+ strokeWidth: "2",
49118
+ strokeLinecap: "round",
49119
+ strokeLinejoin: "round",
49120
+ className: "acp-mic-spinner",
49121
+ children: /* @__PURE__ */ jsx184("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
49122
+ }
49123
+ ) : isRecording ? /* @__PURE__ */ jsx184(MicOff, { size: 16 }) : /* @__PURE__ */ jsx184(Mic, { size: 16 })
49124
+ }
49125
+ )
49126
+ }
49127
+ ),
49128
+ /* @__PURE__ */ jsx184(
49129
+ "button",
49130
+ {
49131
+ className: "acp-send-btn",
49132
+ onClick: isLoading ? handleStop : () => handleSend(),
49133
+ disabled: !isLoading && !prompt.trim(),
49134
+ title: isLoading ? "Stop" : "Send",
49135
+ children: isLoading ? /* @__PURE__ */ jsx184("span", { className: "acp-stop-square" }) : /* @__PURE__ */ jsx184(ArrowUp3, { size: 16 })
49136
+ }
49137
+ )
49138
+ ] })
49139
+ ] })
49140
+ ]
49141
+ }
49142
+ )
49143
+ ] }) })
49144
+ ] })
49145
+ }
49146
+ );
49147
+ }
49148
+ );
49149
+ AIChatPanel.displayName = "AIChatPanel";
49150
+ function ToolActionsDisplay({ actions: actions2 }) {
49151
+ const [expanded, setExpanded] = useState49(false);
49152
+ return /* @__PURE__ */ jsxs100("div", { className: "acp-tool-actions", children: [
49153
+ /* @__PURE__ */ jsxs100(
49154
+ "button",
49155
+ {
49156
+ className: "acp-tool-actions-toggle",
49157
+ onClick: () => setExpanded((v) => !v),
49158
+ children: [
49159
+ /* @__PURE__ */ jsx184(Wrench, { size: 13 }),
49160
+ /* @__PURE__ */ jsxs100("span", { children: [
49161
+ actions2.length,
49162
+ " action",
49163
+ actions2.length !== 1 ? "s" : "",
49164
+ " performed"
49165
+ ] }),
49166
+ /* @__PURE__ */ jsx184(
49167
+ ChevronRight,
49168
+ {
49169
+ size: 12,
49170
+ className: `acp-tool-chevron ${expanded ? "acp-tool-chevron--open" : ""}`
49171
+ }
49172
+ )
49173
+ ]
49174
+ }
49175
+ ),
49176
+ expanded && /* @__PURE__ */ jsx184("div", { className: "acp-tool-actions-list", children: actions2.map((action, i) => /* @__PURE__ */ jsxs100(
49177
+ "div",
49178
+ {
49179
+ className: `acp-tool-action-item ${action.result.success ? "acp-tool-action-item--ok" : "acp-tool-action-item--err"}`,
49180
+ children: [
49181
+ /* @__PURE__ */ jsx184("span", { className: "acp-tool-action-dot" }),
49182
+ /* @__PURE__ */ jsx184("span", { children: action.result.statusMessage })
49183
+ ]
49184
+ },
49185
+ i
49186
+ )) })
49187
+ ] });
49188
+ }
49189
+ async function callPlainChatAPI(messages, apiKey, signal, opts) {
49190
+ const model = opts?.webSearchEnabled ? "openai/gpt-4.1-mini:online" : "openai/gpt-4.1-mini";
49191
+ const apiMessages = messages.map((m, i) => {
49192
+ const isLast = i === messages.length - 1;
49193
+ if (isLast && m.role === "user" && opts?.attachments?.length) {
49194
+ const content2 = [];
49195
+ for (const att of opts.attachments) {
49196
+ if (att.type === "image" && att.dataUrl) {
49197
+ content2.push({
49198
+ type: "image_url",
49199
+ image_url: { url: att.dataUrl }
49200
+ });
49201
+ }
49202
+ }
49203
+ let text = m.content;
49204
+ const textFiles = opts.attachments.filter((a) => a.type === "text");
49205
+ if (textFiles.length > 0) {
49206
+ text += `
49207
+
49208
+ ${textFiles.map((f) => `[File: ${f.name}]
49209
+ ${f.textContent}`).join("\n\n")}`;
49210
+ }
49211
+ content2.push({ type: "text", text });
49212
+ return { role: m.role, content: content2 };
49213
+ }
49214
+ return { role: m.role, content: m.content };
49215
+ });
49216
+ const body = { model, messages: apiMessages };
49217
+ if (opts?.webSearchEnabled) {
49218
+ body.plugins = [{ id: "web", max_results: 5 }];
49219
+ }
49220
+ const response = await fetch(
49221
+ "https://openrouter.ai/api/v1/chat/completions",
49222
+ {
49223
+ method: "POST",
49224
+ signal,
49225
+ headers: {
49226
+ Authorization: `Bearer ${apiKey}`,
49227
+ "Content-Type": "application/json"
49228
+ },
49229
+ body: JSON.stringify(body)
49230
+ }
49231
+ );
49232
+ if (!response.ok) {
49233
+ let message = `OpenRouter error ${response.status}`;
49234
+ try {
49235
+ const err = await response.json();
49236
+ if (err?.error?.message) {
49237
+ message = err.error.message;
49238
+ }
49239
+ } catch {
49240
+ }
49241
+ throw new Error(message);
49242
+ }
49243
+ const data = await response.json();
49244
+ const content = data?.choices?.[0]?.message?.content;
49245
+ if (!content) {
49246
+ throw new Error("No response returned by the API.");
49247
+ }
49248
+ return content;
49249
+ }
49250
+
49251
+ // index.tsx
49252
+ import { jsx as jsx185 } from "react/jsx-runtime";
46522
49253
  polyfill_default();
46523
49254
  var ExcalidrawBase = (props) => {
46524
49255
  const {
@@ -46555,7 +49286,10 @@ var ExcalidrawBase = (props) => {
46555
49286
  renderEmbeddable,
46556
49287
  aiEnabled,
46557
49288
  showDeprecatedFonts,
46558
- renderScrollbars
49289
+ geminiApiKey,
49290
+ renderScrollbars,
49291
+ onBeforeImageGen,
49292
+ onAfterImageGen
46559
49293
  } = props;
46560
49294
  const canvasActions = props.UIOptions?.canvasActions;
46561
49295
  const UIOptions = {
@@ -46574,7 +49308,7 @@ var ExcalidrawBase = (props) => {
46574
49308
  if (UIOptions.canvasActions.toggleTheme === null && typeof theme === "undefined") {
46575
49309
  UIOptions.canvasActions.toggleTheme = true;
46576
49310
  }
46577
- useEffect54(() => {
49311
+ useEffect56(() => {
46578
49312
  const importPolyfill = async () => {
46579
49313
  await import("canvas-roundrect-polyfill");
46580
49314
  };
@@ -46591,7 +49325,7 @@ var ExcalidrawBase = (props) => {
46591
49325
  document.removeEventListener("touchmove", handleTouchMove);
46592
49326
  };
46593
49327
  }, []);
46594
- return /* @__PURE__ */ jsx177(EditorJotaiProvider, { store: editorJotaiStore, children: /* @__PURE__ */ jsx177(InitializeApp, { langCode, theme, children: /* @__PURE__ */ jsx177(
49328
+ return /* @__PURE__ */ jsx185(EditorJotaiProvider, { store: editorJotaiStore, children: /* @__PURE__ */ jsx185(InitializeApp, { langCode, theme, children: /* @__PURE__ */ jsx185(
46595
49329
  App_default,
46596
49330
  {
46597
49331
  onChange,
@@ -46627,7 +49361,10 @@ var ExcalidrawBase = (props) => {
46627
49361
  renderEmbeddable,
46628
49362
  aiEnabled: aiEnabled !== false,
46629
49363
  showDeprecatedFonts,
49364
+ geminiApiKey,
46630
49365
  renderScrollbars,
49366
+ onBeforeImageGen,
49367
+ onAfterImageGen,
46631
49368
  children
46632
49369
  }
46633
49370
  ) }) });
@@ -46670,9 +49407,10 @@ var areEqual5 = (prevProps, nextProps) => {
46670
49407
  });
46671
49408
  return isUIOptionsSame && isShallowEqual10(prev, next);
46672
49409
  };
46673
- var Excalidraw = React54.memo(ExcalidrawBase, areEqual5);
49410
+ var Excalidraw = React58.memo(ExcalidrawBase, areEqual5);
46674
49411
  Excalidraw.displayName = "Excalidraw";
46675
49412
  export {
49413
+ AIChatPanel,
46676
49414
  Button,
46677
49415
  CaptureUpdateAction39 as CaptureUpdateAction,
46678
49416
  CommandPalette,
@@ -46740,7 +49478,7 @@ export {
46740
49478
  useHandleLibrary,
46741
49479
  useI18n,
46742
49480
  useStylesPanelMode,
46743
- viewportCoordsToSceneCoords4 as viewportCoordsToSceneCoords,
49481
+ viewportCoordsToSceneCoords5 as viewportCoordsToSceneCoords,
46744
49482
  zoomToFitBounds
46745
49483
  };
46746
49484
  //# sourceMappingURL=index.js.map