@orangecatai/adgen-canvas 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dev/index.js CHANGED
@@ -66,10 +66,10 @@ import {
66
66
  serializeAsJSON,
67
67
  serializeLibraryAsJSON,
68
68
  strokeRectWithRotation_simple
69
- } from "./chunk-UV7ECD7A.js";
69
+ } from "./chunk-YOQUQ6O5.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-X2WWSILD.js";
72
+ } from "./chunk-EP27W34X.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
@@ -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,
@@ -9626,7 +9626,7 @@ var exportCanvas = async (type, elements, appState, files, {
9626
9626
  let blob = canvasToBlob(tempCanvas);
9627
9627
  if (appState.exportEmbedScene) {
9628
9628
  blob = blob.then(
9629
- (blob2) => import("./data/image-H4O52A73.js").then(
9629
+ (blob2) => import("./data/image-IZS5VEYX.js").then(
9630
9630
  ({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
9631
9631
  blob: blob2,
9632
9632
  metadata: serializeAsJSON(elements, appState, files, "local")
@@ -10038,7 +10038,7 @@ var actionPasteStyles = register({
10038
10038
  if (!elementStylesToCopyFrom) {
10039
10039
  return element;
10040
10040
  }
10041
- let newElement5 = newElementWith5(element, {
10041
+ let newElement6 = newElementWith5(element, {
10042
10042
  backgroundColor: elementStylesToCopyFrom?.backgroundColor,
10043
10043
  strokeWidth: elementStylesToCopyFrom?.strokeWidth,
10044
10044
  strokeColor: elementStylesToCopyFrom?.strokeColor,
@@ -10051,36 +10051,36 @@ var actionPasteStyles = register({
10051
10051
  element
10052
10052
  ) ? elementStylesToCopyFrom.roundness : getDefaultRoundnessTypeForElement(element) : null
10053
10053
  });
10054
- if (isTextElement3(newElement5)) {
10054
+ if (isTextElement3(newElement6)) {
10055
10055
  const fontSize = elementStylesToCopyFrom.fontSize || DEFAULT_FONT_SIZE3;
10056
10056
  const fontFamily = elementStylesToCopyFrom.fontFamily || DEFAULT_FONT_FAMILY3;
10057
- newElement5 = newElementWith5(newElement5, {
10057
+ newElement6 = newElementWith5(newElement6, {
10058
10058
  fontSize,
10059
10059
  fontFamily,
10060
10060
  textAlign: elementStylesToCopyFrom.textAlign || DEFAULT_TEXT_ALIGN,
10061
10061
  lineHeight: elementStylesToCopyFrom.lineHeight || getLineHeight2(fontFamily)
10062
10062
  });
10063
10063
  let container = null;
10064
- if (newElement5.containerId) {
10064
+ if (newElement6.containerId) {
10065
10065
  container = selectedElements.find(
10066
- (element2) => isTextElement3(newElement5) && element2.id === newElement5.containerId
10066
+ (element2) => isTextElement3(newElement6) && element2.id === newElement6.containerId
10067
10067
  ) || null;
10068
10068
  }
10069
- redrawTextBoundingBox2(newElement5, container, app.scene);
10069
+ redrawTextBoundingBox2(newElement6, container, app.scene);
10070
10070
  }
10071
- if (newElement5.type === "arrow" && isArrowElement2(elementStylesToCopyFrom)) {
10072
- newElement5 = newElementWith5(newElement5, {
10071
+ if (newElement6.type === "arrow" && isArrowElement2(elementStylesToCopyFrom)) {
10072
+ newElement6 = newElementWith5(newElement6, {
10073
10073
  startArrowhead: elementStylesToCopyFrom.startArrowhead,
10074
10074
  endArrowhead: elementStylesToCopyFrom.endArrowhead
10075
10075
  });
10076
10076
  }
10077
10077
  if (isFrameLikeElement4(element)) {
10078
- newElement5 = newElementWith5(newElement5, {
10078
+ newElement6 = newElementWith5(newElement6, {
10079
10079
  roundness: null,
10080
10080
  backgroundColor: "transparent"
10081
10081
  });
10082
10082
  }
10083
- return newElement5;
10083
+ return newElement6;
10084
10084
  }
10085
10085
  return element;
10086
10086
  }),
@@ -21838,8 +21838,8 @@ var snapResizingElements = (selectedElements, selectedOriginalElements, app, eve
21838
21838
  snapLines: pointSnapLines
21839
21839
  };
21840
21840
  };
21841
- var snapNewElement = (newElement5, app, event, origin, dragOffset, elementsMap) => {
21842
- if (!isSnappingEnabled({ event, selectedElements: [newElement5], app })) {
21841
+ var snapNewElement = (newElement6, app, event, origin, dragOffset, elementsMap) => {
21842
+ if (!isSnappingEnabled({ event, selectedElements: [newElement6], app })) {
21843
21843
  return {
21844
21844
  snapOffset: { x: 0, y: 0 },
21845
21845
  snapLines: []
@@ -21856,7 +21856,7 @@ var snapNewElement = (newElement5, app, event, origin, dragOffset, elementsMap)
21856
21856
  const nearestSnapsX = [];
21857
21857
  const nearestSnapsY = [];
21858
21858
  getPointSnaps(
21859
- [newElement5],
21859
+ [newElement6],
21860
21860
  selectionSnapPoints,
21861
21861
  app,
21862
21862
  event,
@@ -21872,12 +21872,12 @@ var snapNewElement = (newElement5, app, event, origin, dragOffset, elementsMap)
21872
21872
  minOffset.y = 0;
21873
21873
  nearestSnapsX.length = 0;
21874
21874
  nearestSnapsY.length = 0;
21875
- const corners = getElementsCorners([newElement5], elementsMap, {
21875
+ const corners = getElementsCorners([newElement6], elementsMap, {
21876
21876
  boundingBoxCorners: true,
21877
21877
  omitCenter: true
21878
21878
  });
21879
21879
  getPointSnaps(
21880
- [newElement5],
21880
+ [newElement6],
21881
21881
  corners,
21882
21882
  app,
21883
21883
  event,
@@ -36860,7 +36860,7 @@ import {
36860
36860
  var _renderNewElementScene = ({
36861
36861
  canvas,
36862
36862
  rc,
36863
- newElement: newElement5,
36863
+ newElement: newElement6,
36864
36864
  elementsMap,
36865
36865
  allElementsMap,
36866
36866
  scale,
@@ -36880,19 +36880,19 @@ var _renderNewElementScene = ({
36880
36880
  });
36881
36881
  context.save();
36882
36882
  context.scale(appState.zoom.value, appState.zoom.value);
36883
- if (newElement5 && newElement5.type !== "selection") {
36884
- if (isInvisiblySmallElement2(newElement5)) {
36883
+ if (newElement6 && newElement6.type !== "selection") {
36884
+ if (isInvisiblySmallElement2(newElement6)) {
36885
36885
  return;
36886
36886
  }
36887
- const frameId = newElement5.frameId || appState.frameToHighlight?.id;
36887
+ const frameId = newElement6.frameId || appState.frameToHighlight?.id;
36888
36888
  if (frameId && appState.frameRendering.enabled && appState.frameRendering.clip) {
36889
- const frame = getTargetFrame(newElement5, elementsMap, appState);
36890
- if (frame && shouldApplyFrameClip(newElement5, frame, appState, elementsMap)) {
36889
+ const frame = getTargetFrame(newElement6, elementsMap, appState);
36890
+ if (frame && shouldApplyFrameClip(newElement6, frame, appState, elementsMap)) {
36891
36891
  frameClip(frame, context, renderConfig, appState);
36892
36892
  }
36893
36893
  }
36894
36894
  renderElement(
36895
- newElement5,
36895
+ newElement6,
36896
36896
  elementsMap,
36897
36897
  allElementsMap,
36898
36898
  rc,
@@ -38157,13 +38157,13 @@ var App = class _App extends React52.Component {
38157
38157
  );
38158
38158
  }
38159
38159
  this.scene.replaceAllElements(nextElements);
38160
- duplicatedElements.forEach((newElement5) => {
38161
- if (isTextElement19(newElement5) && isBoundToContainer9(newElement5)) {
38160
+ duplicatedElements.forEach((newElement6) => {
38161
+ if (isTextElement19(newElement6) && isBoundToContainer9(newElement6)) {
38162
38162
  const container = getContainerElement5(
38163
- newElement5,
38163
+ newElement6,
38164
38164
  this.scene.getElementsMapIncludingDeleted()
38165
38165
  );
38166
- redrawTextBoundingBox8(newElement5, container, this.scene);
38166
+ redrawTextBoundingBox8(newElement6, container, this.scene);
38167
38167
  }
38168
38168
  });
38169
38169
  if (isSafari2) {
@@ -39705,8 +39705,8 @@ var App = class _App extends React52.Component {
39705
39705
  }
39706
39706
  }
39707
39707
  if (isBindingElementType(this.state.activeTool.type)) {
39708
- const { newElement: newElement5 } = this.state;
39709
- if (!newElement5 && isBindingEnabled2(this.state)) {
39708
+ const { newElement: newElement6 } = this.state;
39709
+ if (!newElement6 && isBindingEnabled2(this.state)) {
39710
39710
  const globalPoint = pointFrom29(
39711
39711
  scenePointerX,
39712
39712
  scenePointerY
@@ -41850,8 +41850,8 @@ var App = class _App extends React52.Component {
41850
41850
  });
41851
41851
  return;
41852
41852
  }
41853
- const newElement5 = this.state.newElement;
41854
- if (!newElement5) {
41853
+ const newElement6 = this.state.newElement;
41854
+ if (!newElement6) {
41855
41855
  return;
41856
41856
  }
41857
41857
  let [gridX, gridY] = getGridPoint2(
@@ -41859,11 +41859,11 @@ var App = class _App extends React52.Component {
41859
41859
  pointerCoords.y,
41860
41860
  event[KEYS55.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
41861
41861
  );
41862
- const image = isInitializedImageElement3(newElement5) && this.imageCache.get(newElement5.fileId)?.image;
41862
+ const image = isInitializedImageElement3(newElement6) && this.imageCache.get(newElement6.fileId)?.image;
41863
41863
  const aspectRatio = image && !(image instanceof Promise) ? image.width / image.height : null;
41864
- this.maybeCacheReferenceSnapPoints(event, [newElement5]);
41864
+ this.maybeCacheReferenceSnapPoints(event, [newElement6]);
41865
41865
  const { snapOffset, snapLines } = snapNewElement(
41866
- newElement5,
41866
+ newElement6,
41867
41867
  this,
41868
41868
  event,
41869
41869
  {
@@ -41881,9 +41881,9 @@ var App = class _App extends React52.Component {
41881
41881
  this.setState({
41882
41882
  snapLines
41883
41883
  });
41884
- if (!isBindingElement4(newElement5)) {
41884
+ if (!isBindingElement4(newElement6)) {
41885
41885
  dragNewElement({
41886
- newElement: newElement5,
41886
+ newElement: newElement6,
41887
41887
  elementType: this.state.activeTool.type,
41888
41888
  originX: pointerDownState.originInGrid.x,
41889
41889
  originY: pointerDownState.originInGrid.y,
@@ -41891,7 +41891,7 @@ var App = class _App extends React52.Component {
41891
41891
  y: gridY,
41892
41892
  width: distance2(pointerDownState.originInGrid.x, gridX),
41893
41893
  height: distance2(pointerDownState.originInGrid.y, gridY),
41894
- shouldMaintainAspectRatio: isImageElement9(newElement5) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event),
41894
+ shouldMaintainAspectRatio: isImageElement9(newElement6) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event),
41895
41895
  shouldResizeFromCenter: shouldResizeFromCenter(event),
41896
41896
  zoom: this.state.zoom.value,
41897
41897
  scene: this.scene,
@@ -41901,13 +41901,13 @@ var App = class _App extends React52.Component {
41901
41901
  });
41902
41902
  }
41903
41903
  this.setState({
41904
- newElement: newElement5
41904
+ newElement: newElement6
41905
41905
  });
41906
41906
  if (this.state.activeTool.type === TOOL_TYPE3.frame || this.state.activeTool.type === TOOL_TYPE3.magicframe) {
41907
41907
  this.setState({
41908
41908
  elementsToHighlight: getElementsInResizingFrame4(
41909
41909
  this.scene.getNonDeletedElements(),
41910
- newElement5,
41910
+ newElement6,
41911
41911
  this.state,
41912
41912
  this.scene.getNonDeletedElementsMap()
41913
41913
  )
@@ -44926,20 +44926,20 @@ var App = class _App extends React52.Component {
44926
44926
  );
44927
44927
  }
44928
44928
  } else {
44929
- const newElement5 = this.state.newElement;
44930
- if (!newElement5) {
44929
+ const newElement6 = this.state.newElement;
44930
+ if (!newElement6) {
44931
44931
  return;
44932
44932
  }
44933
- if (newElement5.type === "freedraw") {
44934
- const points = newElement5.points;
44935
- const dx = pointerCoords.x - newElement5.x;
44936
- const dy = pointerCoords.y - newElement5.y;
44933
+ if (newElement6.type === "freedraw") {
44934
+ const points = newElement6.points;
44935
+ const dx = pointerCoords.x - newElement6.x;
44936
+ const dy = pointerCoords.y - newElement6.y;
44937
44937
  const lastPoint = points.length > 0 && points[points.length - 1];
44938
44938
  const discardPoint = lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
44939
44939
  if (!discardPoint) {
44940
- const pressures = newElement5.simulatePressure ? newElement5.pressures : [...newElement5.pressures, event.pressure];
44940
+ const pressures = newElement6.simulatePressure ? newElement6.pressures : [...newElement6.pressures, event.pressure];
44941
44941
  this.scene.mutateElement(
44942
- newElement5,
44942
+ newElement6,
44943
44943
  {
44944
44944
  points: [...points, pointFrom29(dx, dy)],
44945
44945
  pressures
@@ -44950,12 +44950,12 @@ var App = class _App extends React52.Component {
44950
44950
  }
44951
44951
  );
44952
44952
  this.setState({
44953
- newElement: newElement5
44953
+ newElement: newElement6
44954
44954
  });
44955
44955
  }
44956
- } else if (isLinearElement12(newElement5) && !newElement5.isDeleted) {
44956
+ } else if (isLinearElement12(newElement6) && !newElement6.isDeleted) {
44957
44957
  pointerDownState.drag.hasOccurred = true;
44958
- const points = newElement5.points;
44958
+ const points = newElement6.points;
44959
44959
  invariant16(
44960
44960
  points.length > 1,
44961
44961
  "Do not create linear elements with less than 2 points"
@@ -44963,7 +44963,7 @@ var App = class _App extends React52.Component {
44963
44963
  let linearElementEditor = this.state.selectedLinearElement;
44964
44964
  if (!linearElementEditor) {
44965
44965
  linearElementEditor = new LinearElementEditor11(
44966
- newElement5,
44966
+ newElement6,
44967
44967
  this.scene.getNonDeletedElementsMap()
44968
44968
  );
44969
44969
  linearElementEditor = {
@@ -44976,7 +44976,7 @@ var App = class _App extends React52.Component {
44976
44976
  };
44977
44977
  }
44978
44978
  this.setState({
44979
- newElement: newElement5,
44979
+ newElement: newElement6,
44980
44980
  ...LinearElementEditor11.handlePointDragging(
44981
44981
  event,
44982
44982
  this,
@@ -45100,7 +45100,7 @@ var App = class _App extends React52.Component {
45100
45100
  pointerDownState.eventListeners.onMove.flush();
45101
45101
  }
45102
45102
  const {
45103
- newElement: newElement5,
45103
+ newElement: newElement6,
45104
45104
  resizingElement,
45105
45105
  croppingElementId,
45106
45106
  multiElement,
@@ -45264,28 +45264,28 @@ var App = class _App extends React52.Component {
45264
45264
  pointerDownState,
45265
45265
  childEvent
45266
45266
  );
45267
- if (newElement5?.type === "freedraw") {
45267
+ if (newElement6?.type === "freedraw") {
45268
45268
  const pointerCoords = viewportCoordsToSceneCoords3(
45269
45269
  childEvent,
45270
45270
  this.state
45271
45271
  );
45272
- const points = newElement5.points;
45273
- let dx = pointerCoords.x - newElement5.x;
45274
- let dy = pointerCoords.y - newElement5.y;
45272
+ const points = newElement6.points;
45273
+ let dx = pointerCoords.x - newElement6.x;
45274
+ let dy = pointerCoords.y - newElement6.y;
45275
45275
  if (dx === points[0][0] && dy === points[0][1]) {
45276
45276
  dy += 1e-4;
45277
45277
  dx += 1e-4;
45278
45278
  }
45279
- const pressures = newElement5.simulatePressure ? [] : [...newElement5.pressures, childEvent.pressure];
45280
- this.scene.mutateElement(newElement5, {
45279
+ const pressures = newElement6.simulatePressure ? [] : [...newElement6.pressures, childEvent.pressure];
45280
+ this.scene.mutateElement(newElement6, {
45281
45281
  points: [...points, pointFrom29(dx, dy)],
45282
45282
  pressures
45283
45283
  });
45284
45284
  this.actionManager.executeAction(actionFinalize);
45285
45285
  return;
45286
45286
  }
45287
- if (isLinearElement12(newElement5)) {
45288
- if (newElement5.points.length > 1 && newElement5.points[1][0] !== 0 && newElement5.points[1][1] !== 0) {
45287
+ if (isLinearElement12(newElement6)) {
45288
+ if (newElement6.points.length > 1 && newElement6.points[1][0] !== 0 && newElement6.points[1][1] !== 0) {
45289
45289
  this.store.scheduleCapture();
45290
45290
  }
45291
45291
  const pointerCoords = viewportCoordsToSceneCoords3(
@@ -45296,16 +45296,16 @@ var App = class _App extends React52.Component {
45296
45296
  pointFrom29(pointerCoords.x, pointerCoords.y),
45297
45297
  pointFrom29(pointerDownState.origin.x, pointerDownState.origin.y)
45298
45298
  ) * this.state.zoom.value;
45299
- if ((!pointerDownState.drag.hasOccurred || dragDistance < MINIMUM_ARROW_SIZE) && newElement5 && !multiElement) {
45299
+ if ((!pointerDownState.drag.hasOccurred || dragDistance < MINIMUM_ARROW_SIZE) && newElement6 && !multiElement) {
45300
45300
  if (this.editorInterface.isTouchScreen) {
45301
45301
  const FIXED_DELTA_X = Math.min(
45302
45302
  this.state.width * 0.7 / this.state.zoom.value,
45303
45303
  100
45304
45304
  );
45305
45305
  this.scene.mutateElement(
45306
- newElement5,
45306
+ newElement6,
45307
45307
  {
45308
- x: newElement5.x - FIXED_DELTA_X / 2,
45308
+ x: newElement6.x - FIXED_DELTA_X / 2,
45309
45309
  points: [
45310
45310
  pointFrom29(0, 0),
45311
45311
  pointFrom29(FIXED_DELTA_X, 0)
@@ -45315,22 +45315,22 @@ var App = class _App extends React52.Component {
45315
45315
  );
45316
45316
  this.actionManager.executeAction(actionFinalize);
45317
45317
  } else {
45318
- const dx = pointerCoords.x - newElement5.x;
45319
- const dy = pointerCoords.y - newElement5.y;
45318
+ const dx = pointerCoords.x - newElement6.x;
45319
+ const dy = pointerCoords.y - newElement6.y;
45320
45320
  this.scene.mutateElement(
45321
- newElement5,
45321
+ newElement6,
45322
45322
  {
45323
- points: [newElement5.points[0], pointFrom29(dx, dy)]
45323
+ points: [newElement6.points[0], pointFrom29(dx, dy)]
45324
45324
  },
45325
45325
  { informMutation: false, isDragging: false }
45326
45326
  );
45327
45327
  this.setState({
45328
- multiElement: newElement5,
45329
- newElement: newElement5
45328
+ multiElement: newElement6,
45329
+ newElement: newElement6
45330
45330
  });
45331
45331
  }
45332
45332
  } else if (pointerDownState.drag.hasOccurred && !multiElement) {
45333
- if (isLinearElement12(newElement5)) {
45333
+ if (isLinearElement12(newElement6)) {
45334
45334
  this.actionManager.executeAction(actionFinalize, "ui", {
45335
45335
  event: childEvent,
45336
45336
  sceneCoords
@@ -45347,12 +45347,12 @@ var App = class _App extends React52.Component {
45347
45347
  selectedElementIds: makeNextSelectedElementIds2(
45348
45348
  {
45349
45349
  ...prevState.selectedElementIds,
45350
- [newElement5.id]: true
45350
+ [newElement6.id]: true
45351
45351
  },
45352
45352
  prevState
45353
45353
  ),
45354
45354
  selectedLinearElement: new LinearElementEditor11(
45355
- newElement5,
45355
+ newElement6,
45356
45356
  this.scene.getNonDeletedElementsMap()
45357
45357
  )
45358
45358
  }));
@@ -45365,27 +45365,27 @@ var App = class _App extends React52.Component {
45365
45365
  }
45366
45366
  return;
45367
45367
  }
45368
- if (isTextElement19(newElement5)) {
45368
+ if (isTextElement19(newElement6)) {
45369
45369
  const minWidth = getMinTextElementWidth(
45370
45370
  getFontString9({
45371
- fontSize: newElement5.fontSize,
45372
- fontFamily: newElement5.fontFamily
45371
+ fontSize: newElement6.fontSize,
45372
+ fontFamily: newElement6.fontFamily
45373
45373
  }),
45374
- newElement5.lineHeight
45374
+ newElement6.lineHeight
45375
45375
  );
45376
- if (newElement5.width < minWidth) {
45377
- this.scene.mutateElement(newElement5, {
45376
+ if (newElement6.width < minWidth) {
45377
+ this.scene.mutateElement(newElement6, {
45378
45378
  autoResize: true
45379
45379
  });
45380
45380
  }
45381
45381
  this.resetCursor();
45382
- this.handleTextWysiwyg(newElement5, {
45382
+ this.handleTextWysiwyg(newElement6, {
45383
45383
  isExistingElement: true
45384
45384
  });
45385
45385
  }
45386
- if (activeTool.type !== "selection" && newElement5 && isInvisiblySmallElement3(newElement5)) {
45386
+ if (activeTool.type !== "selection" && newElement6 && isInvisiblySmallElement3(newElement6)) {
45387
45387
  this.updateScene({
45388
- elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== newElement5.id),
45388
+ elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== newElement6.id),
45389
45389
  appState: {
45390
45390
  newElement: null
45391
45391
  },
@@ -45393,25 +45393,25 @@ var App = class _App extends React52.Component {
45393
45393
  });
45394
45394
  return;
45395
45395
  }
45396
- if (isFrameLikeElement16(newElement5)) {
45396
+ if (isFrameLikeElement16(newElement6)) {
45397
45397
  const elementsInsideFrame = getElementsInNewFrame(
45398
45398
  this.scene.getElementsIncludingDeleted(),
45399
- newElement5,
45399
+ newElement6,
45400
45400
  this.scene.getNonDeletedElementsMap()
45401
45401
  );
45402
45402
  this.scene.replaceAllElements(
45403
45403
  addElementsToFrame2(
45404
45404
  this.scene.getElementsMapIncludingDeleted(),
45405
45405
  elementsInsideFrame,
45406
- newElement5,
45406
+ newElement6,
45407
45407
  this.state
45408
45408
  )
45409
45409
  );
45410
45410
  }
45411
- if (newElement5) {
45411
+ if (newElement6) {
45412
45412
  this.scene.mutateElement(
45413
- newElement5,
45414
- getNormalizedDimensions(newElement5),
45413
+ newElement6,
45414
+ getNormalizedDimensions(newElement6),
45415
45415
  {
45416
45416
  informMutation: false,
45417
45417
  isDragging: false
@@ -45741,16 +45741,16 @@ var App = class _App extends React52.Component {
45741
45741
  setCursor(this.interactiveCanvas, CURSOR_TYPE4.AUTO);
45742
45742
  return;
45743
45743
  }
45744
- if (!activeTool.locked && activeTool.type !== "freedraw" && newElement5) {
45744
+ if (!activeTool.locked && activeTool.type !== "freedraw" && newElement6) {
45745
45745
  this.setState((prevState) => ({
45746
45746
  selectedElementIds: makeNextSelectedElementIds2(
45747
45747
  {
45748
45748
  ...prevState.selectedElementIds,
45749
- [newElement5.id]: true
45749
+ [newElement6.id]: true
45750
45750
  },
45751
45751
  prevState
45752
45752
  ),
45753
- showHyperlinkPopup: isEmbeddableElement4(newElement5) && !newElement5.link ? "editor" : prevState.showHyperlinkPopup
45753
+ showHyperlinkPopup: isEmbeddableElement4(newElement6) && !newElement6.link ? "editor" : prevState.showHyperlinkPopup
45754
45754
  }));
45755
45755
  }
45756
45756
  if (activeTool.type !== "selection" || isSomeElementSelected(this.scene.getNonDeletedElements(), this.state) || !isShallowEqual9(
@@ -46518,7 +46518,1934 @@ var DiagramToCodePlugin = (props) => {
46518
46518
  // index.tsx
46519
46519
  import { isElementLink as isElementLink3 } from "@orangecatai/element";
46520
46520
  import { setCustomTextMetricsProvider } from "@orangecatai/element";
46521
+
46522
+ // components/AIChatPanel.tsx
46523
+ import {
46524
+ useCallback as useCallback24,
46525
+ useEffect as useEffect55,
46526
+ useRef as useRef49,
46527
+ useState as useState49
46528
+ } from "react";
46529
+ import {
46530
+ ArrowUp as ArrowUp3,
46531
+ AtSign,
46532
+ ChevronDown as ChevronDown2,
46533
+ ChevronRight,
46534
+ Copy,
46535
+ Globe,
46536
+ MessageSquare,
46537
+ Mic,
46538
+ MoreHorizontal,
46539
+ Pencil,
46540
+ Plus,
46541
+ Search,
46542
+ ThumbsDown,
46543
+ ThumbsUp,
46544
+ Trash,
46545
+ Wrench,
46546
+ X as X2
46547
+ } from "lucide-react";
46548
+
46549
+ // components/ai-chat/canvasTools.ts
46550
+ import { nanoid as nanoid2 } from "nanoid";
46551
+ import {
46552
+ newFrameElement as newFrameElement3,
46553
+ newElement as newElement5,
46554
+ newTextElement as newTextElement4,
46555
+ newImageElement as newImageElement2,
46556
+ getFrameChildren as getFrameChildren7,
46557
+ isFrameLikeElement as isFrameLikeElement17
46558
+ } from "@orangecatai/element";
46559
+ import { FRAME_STYLE as FRAME_STYLE5 } from "@orangecatai/common";
46560
+ var CANVAS_TOOLS = [
46561
+ {
46562
+ type: "function",
46563
+ function: {
46564
+ name: "create_frame",
46565
+ description: "Create a new frame on the canvas. Frames act as artboards that clip their children. Default size is 512x512.",
46566
+ parameters: {
46567
+ type: "object",
46568
+ properties: {
46569
+ width: {
46570
+ type: "number",
46571
+ description: "Frame width in pixels (default 512)"
46572
+ },
46573
+ height: {
46574
+ type: "number",
46575
+ description: "Frame height in pixels (default 512)"
46576
+ },
46577
+ name: {
46578
+ type: "string",
46579
+ description: "Optional display name for the frame"
46580
+ }
46581
+ },
46582
+ required: [],
46583
+ additionalProperties: false
46584
+ }
46585
+ }
46586
+ },
46587
+ {
46588
+ type: "function",
46589
+ function: {
46590
+ name: "add_rectangle",
46591
+ description: "Add a filled rectangle inside a frame. Commonly used for solid background fills behind images and text.",
46592
+ parameters: {
46593
+ type: "object",
46594
+ properties: {
46595
+ frameId: {
46596
+ type: "string",
46597
+ description: "ID of the parent frame"
46598
+ },
46599
+ x: {
46600
+ type: "number",
46601
+ description: "X position relative to canvas (use the frame's x for full coverage)"
46602
+ },
46603
+ y: {
46604
+ type: "number",
46605
+ description: "Y position relative to canvas (use the frame's y for full coverage)"
46606
+ },
46607
+ width: {
46608
+ type: "number",
46609
+ description: "Rectangle width in pixels"
46610
+ },
46611
+ height: {
46612
+ type: "number",
46613
+ description: "Rectangle height in pixels"
46614
+ },
46615
+ backgroundColor: {
46616
+ type: "string",
46617
+ description: 'Fill color as hex string e.g. "#1a1a2e". Use "transparent" for no fill.'
46618
+ },
46619
+ fillStyle: {
46620
+ type: "string",
46621
+ enum: ["solid", "hachure", "cross-hatch"],
46622
+ description: 'Fill style (default "solid")'
46623
+ },
46624
+ strokeColor: {
46625
+ type: "string",
46626
+ description: 'Stroke/border color as hex (default "transparent")'
46627
+ },
46628
+ strokeWidth: {
46629
+ type: "number",
46630
+ description: "Stroke width in pixels (default 0)"
46631
+ },
46632
+ opacity: {
46633
+ type: "number",
46634
+ description: "Opacity 0-100 (default 100)"
46635
+ }
46636
+ },
46637
+ required: ["frameId", "x", "y", "width", "height", "backgroundColor"],
46638
+ additionalProperties: false
46639
+ }
46640
+ }
46641
+ },
46642
+ {
46643
+ type: "function",
46644
+ function: {
46645
+ name: "add_text",
46646
+ description: "Add a text element inside a frame. Use for headlines, subheads, body copy, CTAs etc.",
46647
+ parameters: {
46648
+ type: "object",
46649
+ properties: {
46650
+ frameId: {
46651
+ type: "string",
46652
+ description: "ID of the parent frame"
46653
+ },
46654
+ x: {
46655
+ type: "number",
46656
+ description: "X position (canvas coordinates)"
46657
+ },
46658
+ y: {
46659
+ type: "number",
46660
+ description: "Y position (canvas coordinates)"
46661
+ },
46662
+ text: {
46663
+ type: "string",
46664
+ description: "The text content"
46665
+ },
46666
+ fontSize: {
46667
+ type: "number",
46668
+ description: "Font size in pixels (default 20)"
46669
+ },
46670
+ fontFamily: {
46671
+ type: "number",
46672
+ enum: [1, 2, 3, 4, 5],
46673
+ description: "Font family: 1=Excalifont(hand), 2=Nunito(sans), 3=Comic Shanns(code), 4=Liberation Sans(clean sans), 5=cascadia(code). Default 2."
46674
+ },
46675
+ strokeColor: {
46676
+ type: "string",
46677
+ description: 'Text color as hex string (default "#000000")'
46678
+ },
46679
+ textAlign: {
46680
+ type: "string",
46681
+ enum: ["left", "center", "right"],
46682
+ description: "Horizontal alignment (default left)"
46683
+ },
46684
+ width: {
46685
+ type: "number",
46686
+ description: "Optional fixed width for text wrapping"
46687
+ }
46688
+ },
46689
+ required: ["frameId", "x", "y", "text"],
46690
+ additionalProperties: false
46691
+ }
46692
+ }
46693
+ },
46694
+ {
46695
+ type: "function",
46696
+ function: {
46697
+ name: "generate_image",
46698
+ 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.",
46699
+ parameters: {
46700
+ type: "object",
46701
+ properties: {
46702
+ frameId: {
46703
+ type: "string",
46704
+ description: "ID of the parent frame to insert the image into"
46705
+ },
46706
+ prompt: {
46707
+ type: "string",
46708
+ description: "Detailed image generation prompt describing what to create"
46709
+ },
46710
+ x: {
46711
+ type: "number",
46712
+ description: "X position for the image (canvas coordinates)"
46713
+ },
46714
+ y: {
46715
+ type: "number",
46716
+ description: "Y position for the image (canvas coordinates)"
46717
+ },
46718
+ width: {
46719
+ type: "number",
46720
+ description: "Image width in pixels"
46721
+ },
46722
+ height: {
46723
+ type: "number",
46724
+ description: "Image height in pixels"
46725
+ }
46726
+ },
46727
+ required: ["frameId", "prompt", "x", "y", "width", "height"],
46728
+ additionalProperties: false
46729
+ }
46730
+ }
46731
+ },
46732
+ {
46733
+ type: "function",
46734
+ function: {
46735
+ name: "update_element",
46736
+ description: "Update properties of an existing element (move, resize, recolor, change text, etc.)",
46737
+ parameters: {
46738
+ type: "object",
46739
+ properties: {
46740
+ elementId: {
46741
+ type: "string",
46742
+ description: "ID of the element to update"
46743
+ },
46744
+ x: { type: "number", description: "New X position" },
46745
+ y: { type: "number", description: "New Y position" },
46746
+ width: { type: "number", description: "New width" },
46747
+ height: { type: "number", description: "New height" },
46748
+ backgroundColor: {
46749
+ type: "string",
46750
+ description: "New background/fill color"
46751
+ },
46752
+ strokeColor: {
46753
+ type: "string",
46754
+ description: "New stroke/text color"
46755
+ },
46756
+ opacity: { type: "number", description: "New opacity 0-100" },
46757
+ fontSize: {
46758
+ type: "number",
46759
+ description: "New font size (text elements only)"
46760
+ },
46761
+ text: {
46762
+ type: "string",
46763
+ description: "New text content (text elements only)"
46764
+ },
46765
+ fillStyle: {
46766
+ type: "string",
46767
+ enum: ["solid", "hachure", "cross-hatch"],
46768
+ description: "New fill style"
46769
+ }
46770
+ },
46771
+ required: ["elementId"],
46772
+ additionalProperties: false
46773
+ }
46774
+ }
46775
+ },
46776
+ {
46777
+ type: "function",
46778
+ function: {
46779
+ name: "delete_element",
46780
+ description: "Delete an element from the canvas",
46781
+ parameters: {
46782
+ type: "object",
46783
+ properties: {
46784
+ elementId: {
46785
+ type: "string",
46786
+ description: "ID of the element to delete"
46787
+ }
46788
+ },
46789
+ required: ["elementId"],
46790
+ additionalProperties: false
46791
+ }
46792
+ }
46793
+ },
46794
+ {
46795
+ type: "function",
46796
+ function: {
46797
+ name: "get_frame_elements",
46798
+ 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.",
46799
+ parameters: {
46800
+ type: "object",
46801
+ properties: {
46802
+ frameId: {
46803
+ type: "string",
46804
+ description: "ID of the frame to inspect"
46805
+ }
46806
+ },
46807
+ required: ["frameId"],
46808
+ additionalProperties: false
46809
+ }
46810
+ }
46811
+ }
46812
+ ];
46813
+ function serializeElement(el) {
46814
+ const base = {
46815
+ id: el.id,
46816
+ type: el.type,
46817
+ x: Math.round(el.x),
46818
+ y: Math.round(el.y),
46819
+ width: Math.round(el.width),
46820
+ height: Math.round(el.height)
46821
+ };
46822
+ if (el.backgroundColor && el.backgroundColor !== "transparent") {
46823
+ base.backgroundColor = el.backgroundColor;
46824
+ }
46825
+ if (el.strokeColor && el.strokeColor !== "#1e1e1e") {
46826
+ base.strokeColor = el.strokeColor;
46827
+ }
46828
+ if (el.opacity !== 100) {
46829
+ base.opacity = el.opacity;
46830
+ }
46831
+ if (el.fillStyle && el.fillStyle !== "solid") {
46832
+ base.fillStyle = el.fillStyle;
46833
+ }
46834
+ if (el.type === "text") {
46835
+ const textEl = el;
46836
+ base.text = textEl.text;
46837
+ base.fontSize = textEl.fontSize;
46838
+ base.fontFamily = textEl.fontFamily;
46839
+ base.textAlign = textEl.textAlign;
46840
+ }
46841
+ if (el.type === "image") {
46842
+ base.type = "image";
46843
+ }
46844
+ if (el.type === "frame") {
46845
+ const frameEl = el;
46846
+ if (frameEl.name) {
46847
+ base.name = frameEl.name;
46848
+ }
46849
+ }
46850
+ return base;
46851
+ }
46852
+ function serializeElements(elements) {
46853
+ return elements.map(serializeElement);
46854
+ }
46855
+ function listFrames(api) {
46856
+ const elements = api.getSceneElements();
46857
+ const frames = [];
46858
+ for (const el of elements) {
46859
+ if (isFrameLikeElement17(el)) {
46860
+ const children = getFrameChildren7(elements, el.id);
46861
+ const frameLike = el;
46862
+ frames.push({
46863
+ id: el.id,
46864
+ name: frameLike.name || `Frame ${Math.round(el.width)}\xD7${Math.round(el.height)}`,
46865
+ width: Math.round(el.width),
46866
+ height: Math.round(el.height),
46867
+ childCount: children.length
46868
+ });
46869
+ }
46870
+ }
46871
+ return frames;
46872
+ }
46873
+ async function captureFrameScreenshot(api, frameId) {
46874
+ const elements = api.getSceneElements();
46875
+ const frame = elements.find((el) => el.id === frameId);
46876
+ if (!frame || !isFrameLikeElement17(frame)) {
46877
+ return null;
46878
+ }
46879
+ const appState = api.getAppState();
46880
+ const files = api.getFiles();
46881
+ const children = getFrameChildren7(elements, frameId);
46882
+ try {
46883
+ const canvas = await exportToCanvas(
46884
+ [...children, frame],
46885
+ appState,
46886
+ files,
46887
+ {
46888
+ exportBackground: true,
46889
+ viewBackgroundColor: appState.viewBackgroundColor,
46890
+ exportingFrame: frame,
46891
+ exportPadding: 0
46892
+ }
46893
+ );
46894
+ return canvas.toDataURL("image/jpeg", 0.7);
46895
+ } catch {
46896
+ return null;
46897
+ }
46898
+ }
46899
+ function getFrameContext(api, frameId) {
46900
+ const elements = api.getSceneElements();
46901
+ const frame = elements.find((el) => el.id === frameId);
46902
+ if (!frame || !isFrameLikeElement17(frame)) {
46903
+ return null;
46904
+ }
46905
+ const children = getFrameChildren7(elements, frameId);
46906
+ const frameLike = frame;
46907
+ const frameInfo = {
46908
+ id: frame.id,
46909
+ name: frameLike.name || `Frame ${Math.round(frame.width)}\xD7${Math.round(frame.height)}`,
46910
+ width: Math.round(frame.width),
46911
+ height: Math.round(frame.height),
46912
+ childCount: children.length
46913
+ };
46914
+ const data = {
46915
+ frame: serializeElement(frame),
46916
+ children: serializeElements(children)
46917
+ };
46918
+ return {
46919
+ serialized: JSON.stringify(data, null, 2),
46920
+ frameInfo
46921
+ };
46922
+ }
46923
+ async function executeCanvasTool(toolName, rawArgs, ctx) {
46924
+ let args;
46925
+ try {
46926
+ args = JSON.parse(rawArgs);
46927
+ } catch {
46928
+ return {
46929
+ success: false,
46930
+ error: "Invalid JSON arguments",
46931
+ statusMessage: "Failed to parse tool arguments"
46932
+ };
46933
+ }
46934
+ switch (toolName) {
46935
+ case "create_frame":
46936
+ return execCreateFrame(args, ctx);
46937
+ case "add_rectangle":
46938
+ return execAddRectangle(args, ctx);
46939
+ case "add_text":
46940
+ return execAddText(args, ctx);
46941
+ case "generate_image":
46942
+ return execGenerateImage(args, ctx);
46943
+ case "update_element":
46944
+ return execUpdateElement(args, ctx);
46945
+ case "delete_element":
46946
+ return execDeleteElement(args, ctx);
46947
+ case "get_frame_elements":
46948
+ return execGetFrameElements(args, ctx);
46949
+ default:
46950
+ return {
46951
+ success: false,
46952
+ error: `Unknown tool: ${toolName}`,
46953
+ statusMessage: `Unknown tool: ${toolName}`
46954
+ };
46955
+ }
46956
+ }
46957
+ function getViewportCenter(api) {
46958
+ const appState = api.getAppState();
46959
+ const x = -appState.scrollX + appState.width / 2 / appState.zoom.value;
46960
+ const y = -appState.scrollY + appState.height / 2 / appState.zoom.value;
46961
+ return { x, y };
46962
+ }
46963
+ var COVER_TOLERANCE = 10;
46964
+ function coversFrame(elX, elY, elW, elH, frameEl) {
46965
+ 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;
46966
+ }
46967
+ function getElementTier(type, x, y, w, h, frame) {
46968
+ if (type === "text")
46969
+ return 3 /* TEXT */;
46970
+ if (type === "image")
46971
+ return 1 /* IMAGE */;
46972
+ if (coversFrame(x, y, w, h, frame))
46973
+ return 0 /* BG_FILL */;
46974
+ return 2 /* SHAPE */;
46975
+ }
46976
+ function findInsertIndexByTier(elements, frameIndex, tier) {
46977
+ const frame = elements[frameIndex];
46978
+ const frameId = frame.id;
46979
+ let insertAt = frameIndex + 1;
46980
+ for (let i = frameIndex + 1; i < elements.length; i++) {
46981
+ const el = elements[i];
46982
+ if (el.frameId !== frameId)
46983
+ continue;
46984
+ const elTier = getElementTier(el.type, el.x, el.y, el.width, el.height, frame);
46985
+ if (elTier > tier) {
46986
+ return i;
46987
+ }
46988
+ insertAt = i + 1;
46989
+ }
46990
+ return insertAt;
46991
+ }
46992
+ function execCreateFrame(args, ctx) {
46993
+ const width = args.width || 512;
46994
+ const height = args.height || 512;
46995
+ const name = args.name || null;
46996
+ const center = getViewportCenter(ctx.excalidrawAPI);
46997
+ const frame = newFrameElement3({
46998
+ ...FRAME_STYLE5,
46999
+ x: center.x - width / 2,
47000
+ y: center.y - height / 2,
47001
+ width,
47002
+ height,
47003
+ opacity: 100,
47004
+ locked: false,
47005
+ name: name ?? void 0
47006
+ });
47007
+ const elements = ctx.excalidrawAPI.getSceneElements();
47008
+ ctx.excalidrawAPI.updateScene({
47009
+ elements: [...elements, frame]
47010
+ });
47011
+ ctx.excalidrawAPI.scrollToContent(frame, { fitToViewport: false });
47012
+ return {
47013
+ success: true,
47014
+ data: {
47015
+ frameId: frame.id,
47016
+ x: Math.round(frame.x),
47017
+ y: Math.round(frame.y),
47018
+ width,
47019
+ height
47020
+ },
47021
+ statusMessage: `Created frame "${name || "Ad Frame"}" (${width}\xD7${height})`
47022
+ };
47023
+ }
47024
+ function execAddRectangle(args, ctx) {
47025
+ const frameId = args.frameId;
47026
+ if (!frameId) {
47027
+ return {
47028
+ success: false,
47029
+ error: "frameId is required",
47030
+ statusMessage: "Failed: missing frameId"
47031
+ };
47032
+ }
47033
+ const rect = newElement5({
47034
+ type: "rectangle",
47035
+ x: args.x,
47036
+ y: args.y,
47037
+ width: args.width,
47038
+ height: args.height,
47039
+ backgroundColor: args.backgroundColor || "transparent",
47040
+ fillStyle: args.fillStyle || "solid",
47041
+ strokeColor: args.strokeColor || "transparent",
47042
+ strokeWidth: args.strokeWidth ?? 0,
47043
+ opacity: args.opacity ?? 100
47044
+ });
47045
+ const elements = ctx.excalidrawAPI.getSceneElements();
47046
+ const frameIndex = elements.findIndex((el) => el.id === frameId);
47047
+ if (frameIndex === -1) {
47048
+ return {
47049
+ success: false,
47050
+ error: `Frame ${frameId} not found`,
47051
+ statusMessage: "Failed: frame not found"
47052
+ };
47053
+ }
47054
+ const frame = elements[frameIndex];
47055
+ const tier = getElementTier(
47056
+ "rectangle",
47057
+ args.x,
47058
+ args.y,
47059
+ args.width,
47060
+ args.height,
47061
+ frame
47062
+ );
47063
+ const updatedRect = { ...rect, frameId };
47064
+ const newElements = [...elements];
47065
+ const insertAt = findInsertIndexByTier(newElements, frameIndex, tier);
47066
+ newElements.splice(insertAt, 0, updatedRect);
47067
+ ctx.excalidrawAPI.updateScene({ elements: newElements });
47068
+ const isBg = tier === 0 /* BG_FILL */;
47069
+ return {
47070
+ success: true,
47071
+ data: {
47072
+ elementId: rect.id,
47073
+ type: "rectangle",
47074
+ x: Math.round(rect.x),
47075
+ y: Math.round(rect.y),
47076
+ width: Math.round(rect.width),
47077
+ height: Math.round(rect.height),
47078
+ backgroundColor: rect.backgroundColor,
47079
+ isBackground: isBg
47080
+ },
47081
+ statusMessage: `Added ${isBg ? "background " : ""}rectangle (${Math.round(
47082
+ rect.width
47083
+ )}\xD7${Math.round(rect.height)}) with fill ${rect.backgroundColor}`
47084
+ };
47085
+ }
47086
+ function execAddText(args, ctx) {
47087
+ const frameId = args.frameId;
47088
+ if (!frameId) {
47089
+ return {
47090
+ success: false,
47091
+ error: "frameId is required",
47092
+ statusMessage: "Failed: missing frameId"
47093
+ };
47094
+ }
47095
+ const textEl = newTextElement4({
47096
+ x: args.x,
47097
+ y: args.y,
47098
+ text: args.text,
47099
+ fontSize: args.fontSize || 20,
47100
+ fontFamily: args.fontFamily || 2,
47101
+ strokeColor: args.strokeColor || "#000000",
47102
+ textAlign: args.textAlign || "left",
47103
+ width: args.width
47104
+ });
47105
+ const elements = ctx.excalidrawAPI.getSceneElements();
47106
+ const frameIndex = elements.findIndex((el) => el.id === frameId);
47107
+ if (frameIndex === -1) {
47108
+ return {
47109
+ success: false,
47110
+ error: `Frame ${frameId} not found`,
47111
+ statusMessage: "Failed: frame not found"
47112
+ };
47113
+ }
47114
+ const updatedText = { ...textEl, frameId };
47115
+ const newElements = [...elements];
47116
+ const insertAt = findInsertIndexByTier(newElements, frameIndex, 3 /* TEXT */);
47117
+ newElements.splice(insertAt, 0, updatedText);
47118
+ ctx.excalidrawAPI.updateScene({ elements: newElements });
47119
+ return {
47120
+ success: true,
47121
+ data: {
47122
+ elementId: textEl.id,
47123
+ type: "text",
47124
+ x: Math.round(textEl.x),
47125
+ y: Math.round(textEl.y),
47126
+ text: args.text,
47127
+ fontSize: args.fontSize || 20
47128
+ },
47129
+ statusMessage: `Added text "${args.text.slice(0, 40)}${args.text.length > 40 ? "\u2026" : ""}"`
47130
+ };
47131
+ }
47132
+ async function execGenerateImage(args, ctx) {
47133
+ const frameId = args.frameId;
47134
+ const prompt = args.prompt;
47135
+ const x = args.x;
47136
+ const y = args.y;
47137
+ const width = args.width;
47138
+ const height = args.height;
47139
+ if (!frameId || !prompt) {
47140
+ return {
47141
+ success: false,
47142
+ error: "frameId and prompt are required",
47143
+ statusMessage: "Failed: missing required parameters"
47144
+ };
47145
+ }
47146
+ if (!ctx.geminiApiKey) {
47147
+ return {
47148
+ success: false,
47149
+ error: "No Gemini API key configured. Set geminiApiKey prop or VITE_APP_GEMINI_API_KEY.",
47150
+ statusMessage: "Failed: no Gemini API key"
47151
+ };
47152
+ }
47153
+ const elements = ctx.excalidrawAPI.getSceneElements();
47154
+ const frameIndex = elements.findIndex((el) => el.id === frameId);
47155
+ if (frameIndex === -1) {
47156
+ return {
47157
+ success: false,
47158
+ error: `Frame ${frameId} not found`,
47159
+ statusMessage: "Failed: frame not found"
47160
+ };
47161
+ }
47162
+ const aspectRatio = width >= height ? width / height > 1.3 ? "16:9" : "1:1" : "9:16";
47163
+ try {
47164
+ const imageDataUrl = await callGeminiAPI(
47165
+ prompt,
47166
+ "gemini-2.5-flash-image",
47167
+ aspectRatio,
47168
+ "1K",
47169
+ false,
47170
+ false,
47171
+ ctx.geminiApiKey,
47172
+ void 0,
47173
+ ctx.signal
47174
+ );
47175
+ if (ctx.signal?.aborted) {
47176
+ return {
47177
+ success: false,
47178
+ error: "Cancelled",
47179
+ statusMessage: "Image generation cancelled"
47180
+ };
47181
+ }
47182
+ const fileId = nanoid2();
47183
+ const imageEl = newImageElement2({
47184
+ type: "image",
47185
+ x,
47186
+ y,
47187
+ width,
47188
+ height,
47189
+ fileId,
47190
+ status: "pending",
47191
+ scale: [1, 1]
47192
+ });
47193
+ const currentElements = ctx.excalidrawAPI.getSceneElements();
47194
+ const currentFrameIndex = currentElements.findIndex(
47195
+ (el) => el.id === frameId
47196
+ );
47197
+ if (currentFrameIndex === -1) {
47198
+ return {
47199
+ success: false,
47200
+ error: `Frame ${frameId} was removed during image generation`,
47201
+ statusMessage: "Failed: frame no longer exists"
47202
+ };
47203
+ }
47204
+ const updatedImage = { ...imageEl, frameId };
47205
+ const newElements = [...currentElements];
47206
+ const insertAt = findInsertIndexByTier(newElements, currentFrameIndex, 1 /* IMAGE */);
47207
+ newElements.splice(insertAt, 0, updatedImage);
47208
+ ctx.excalidrawAPI.updateScene({ elements: newElements });
47209
+ ctx.excalidrawAPI.addFiles([
47210
+ {
47211
+ id: fileId,
47212
+ dataURL: imageDataUrl,
47213
+ mimeType: getMimeTypeFromDataURL(imageDataUrl),
47214
+ created: Date.now(),
47215
+ lastRetrieved: Date.now()
47216
+ }
47217
+ ]);
47218
+ return {
47219
+ success: true,
47220
+ data: {
47221
+ elementId: imageEl.id,
47222
+ type: "image",
47223
+ x: Math.round(x),
47224
+ y: Math.round(y),
47225
+ width: Math.round(width),
47226
+ height: Math.round(height)
47227
+ },
47228
+ statusMessage: `Generated image from prompt "${prompt.slice(0, 50)}${prompt.length > 50 ? "\u2026" : ""}"`
47229
+ };
47230
+ } catch (err) {
47231
+ const message = err instanceof Error ? err.message : "Image generation failed";
47232
+ return {
47233
+ success: false,
47234
+ error: message,
47235
+ statusMessage: `Image generation failed: ${message}`
47236
+ };
47237
+ }
47238
+ }
47239
+ function execUpdateElement(args, ctx) {
47240
+ const elementId = args.elementId;
47241
+ if (!elementId) {
47242
+ return {
47243
+ success: false,
47244
+ error: "elementId is required",
47245
+ statusMessage: "Failed: missing elementId"
47246
+ };
47247
+ }
47248
+ const elements = ctx.excalidrawAPI.getSceneElements();
47249
+ const element = elements.find((el) => el.id === elementId);
47250
+ if (!element) {
47251
+ return {
47252
+ success: false,
47253
+ error: `Element ${elementId} not found`,
47254
+ statusMessage: "Failed: element not found"
47255
+ };
47256
+ }
47257
+ const updates = {};
47258
+ const allowedKeys = [
47259
+ "x",
47260
+ "y",
47261
+ "width",
47262
+ "height",
47263
+ "backgroundColor",
47264
+ "strokeColor",
47265
+ "opacity",
47266
+ "fontSize",
47267
+ "text",
47268
+ "fillStyle"
47269
+ ];
47270
+ for (const key of allowedKeys) {
47271
+ if (args[key] !== void 0) {
47272
+ updates[key] = args[key];
47273
+ }
47274
+ }
47275
+ if (Object.keys(updates).length === 0) {
47276
+ return {
47277
+ success: false,
47278
+ error: "No valid properties to update",
47279
+ statusMessage: "Failed: nothing to update"
47280
+ };
47281
+ }
47282
+ if (updates.text !== void 0 && element.type === "text" && (updates.width === void 0 || updates.height === void 0)) {
47283
+ return {
47284
+ success: false,
47285
+ error: "Updating text content requires providing new width and height to avoid clipping.",
47286
+ statusMessage: "Failed: text update requires width and height"
47287
+ };
47288
+ }
47289
+ ctx.excalidrawAPI.mutateElement(element, updates);
47290
+ return {
47291
+ success: true,
47292
+ data: { elementId, updated: Object.keys(updates) },
47293
+ statusMessage: `Updated element (${Object.keys(updates).join(", ")})`
47294
+ };
47295
+ }
47296
+ function execDeleteElement(args, ctx) {
47297
+ const elementId = args.elementId;
47298
+ if (!elementId) {
47299
+ return {
47300
+ success: false,
47301
+ error: "elementId is required",
47302
+ statusMessage: "Failed: missing elementId"
47303
+ };
47304
+ }
47305
+ const elements = ctx.excalidrawAPI.getSceneElements();
47306
+ const element = elements.find((el) => el.id === elementId);
47307
+ if (!element) {
47308
+ return {
47309
+ success: false,
47310
+ error: `Element ${elementId} not found`,
47311
+ statusMessage: "Failed: element not found"
47312
+ };
47313
+ }
47314
+ ctx.excalidrawAPI.mutateElement(element, { isDeleted: true });
47315
+ return {
47316
+ success: true,
47317
+ data: { elementId },
47318
+ statusMessage: `Deleted element ${elementId}`
47319
+ };
47320
+ }
47321
+ function execGetFrameElements(args, ctx) {
47322
+ const frameId = args.frameId;
47323
+ if (!frameId) {
47324
+ return {
47325
+ success: false,
47326
+ error: "frameId is required",
47327
+ statusMessage: "Failed: missing frameId"
47328
+ };
47329
+ }
47330
+ const elements = ctx.excalidrawAPI.getSceneElements();
47331
+ const frame = elements.find((el) => el.id === frameId);
47332
+ if (!frame) {
47333
+ return {
47334
+ success: false,
47335
+ error: `Frame ${frameId} not found`,
47336
+ statusMessage: "Failed: frame not found"
47337
+ };
47338
+ }
47339
+ const children = getFrameChildren7(elements, frameId);
47340
+ const serialized = serializeElements(children);
47341
+ return {
47342
+ success: true,
47343
+ data: {
47344
+ frameId,
47345
+ frame: serializeElement(frame),
47346
+ children: serialized,
47347
+ childCount: serialized.length
47348
+ },
47349
+ statusMessage: `Found ${serialized.length} elements in frame`
47350
+ };
47351
+ }
47352
+
47353
+ // components/ai-chat/agentLoop.ts
47354
+ 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.
47355
+
47356
+ ## Your Capabilities
47357
+ You have tools to: create frames, add rectangles (for backgrounds/shapes), add text, generate AI images, update elements, delete elements, and inspect frame contents.
47358
+
47359
+ ## Design Process
47360
+ When asked to create an ad or visual from scratch:
47361
+ 1. **Create a frame** first (default 512\xD7512 unless the user specifies a size). This is the artboard.
47362
+ 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.
47363
+ 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.
47364
+ 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).
47365
+ 5. **Add subhead text** \u2014 smaller supporting text below the headline.
47366
+
47367
+ ## Layout & Z-ordering Rules
47368
+ - Elements are rendered in insertion order. Add background rectangle FIRST, then images, then text ON TOP.
47369
+ - All element positions use absolute canvas coordinates. The frame's x,y is the top-left corner.
47370
+ - To place an element at a frame's top-left: use the frame's x,y values.
47371
+ - To center text horizontally in a frame: set text x = frame.x + (frame.width - estimated_text_width) / 2
47372
+ - Leave padding from edges (at least 20px from frame borders for text).
47373
+ - Headline font size: 28-48px depending on frame size. Subhead: 16-24px.
47374
+
47375
+ ## Color & Typography
47376
+ - Use font family 2 (Nunito/clean sans-serif) for professional ads.
47377
+ - For dark backgrounds: use white (#ffffff) or light text.
47378
+ - For light backgrounds: use dark (#1a1a2e or #000000) text.
47379
+ - Popular ad background colors: deep navy (#1a1a2e), warm red (#c0392b), forest green (#27ae60), elegant black (#111111), soft cream (#fdf6e3).
47380
+
47381
+ ## When modifying existing elements (@ context provided)
47382
+ - The user may reference existing elements via @ mentions. Their current state will be described in the message.
47383
+ - Use get_frame_elements to see the full current state of a frame before making changes.
47384
+ - Use update_element to modify properties. Use delete_element to remove things.
47385
+ - Preserve existing elements unless explicitly asked to change them.
47386
+
47387
+ ## Image Generation Tips
47388
+ - Write detailed, descriptive prompts for generate_image. Include style, mood, composition, colors.
47389
+ - Example: "A steaming bowl of creamy pasta on a rustic wooden table, warm golden lighting, top-down view, food photography style, appetizing, high quality"
47390
+ - The image will be generated by Gemini AI and placed at the coordinates you specify.
47391
+
47392
+ ## Important
47393
+ - Always respond with a brief summary of what you created/modified after completing the tools.
47394
+ - If the user's request is unclear, use the available tools to create a reasonable default and explain what you did.
47395
+ - Coordinates: frame x,y is the top-left corner. Width extends right, height extends down.`;
47396
+ var MAX_ITERATIONS = 15;
47397
+ async function callOpenRouter(messages, apiKey, signal) {
47398
+ const response = await fetch(
47399
+ "https://openrouter.ai/api/v1/chat/completions",
47400
+ {
47401
+ method: "POST",
47402
+ signal,
47403
+ headers: {
47404
+ Authorization: `Bearer ${apiKey}`,
47405
+ "Content-Type": "application/json"
47406
+ },
47407
+ body: JSON.stringify({
47408
+ model: "openai/gpt-4.1-mini",
47409
+ messages: messages.map((m) => {
47410
+ if (m.role === "assistant" && m.tool_calls) {
47411
+ return {
47412
+ role: m.role,
47413
+ content: m.content,
47414
+ tool_calls: m.tool_calls
47415
+ };
47416
+ }
47417
+ if (m.role === "tool") {
47418
+ return {
47419
+ role: m.role,
47420
+ tool_call_id: m.tool_call_id,
47421
+ content: m.content
47422
+ };
47423
+ }
47424
+ return { role: m.role, content: m.content };
47425
+ }),
47426
+ tools: CANVAS_TOOLS,
47427
+ tool_choice: "auto"
47428
+ })
47429
+ }
47430
+ );
47431
+ if (!response.ok) {
47432
+ let message = `OpenRouter error ${response.status}`;
47433
+ try {
47434
+ const err = await response.json();
47435
+ if (err?.error?.message) {
47436
+ message = err.error.message;
47437
+ }
47438
+ } catch {
47439
+ }
47440
+ throw new Error(message);
47441
+ }
47442
+ const data = await response.json();
47443
+ const choice = data?.choices?.[0]?.message;
47444
+ if (!choice) {
47445
+ throw new Error("No response returned by the API.");
47446
+ }
47447
+ return {
47448
+ content: choice.content ?? null,
47449
+ tool_calls: choice.tool_calls ?? void 0
47450
+ };
47451
+ }
47452
+ async function runAgentLoop(opts) {
47453
+ const {
47454
+ userMessages,
47455
+ elementContext,
47456
+ frameScreenshot,
47457
+ apiKey,
47458
+ toolCtx,
47459
+ onUpdate,
47460
+ signal
47461
+ } = opts;
47462
+ const messages = [{ role: "system", content: SYSTEM_PROMPT }];
47463
+ if (elementContext) {
47464
+ messages.push({
47465
+ role: "system",
47466
+ 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:
47467
+
47468
+ ${elementContext}`
47469
+ });
47470
+ }
47471
+ for (let i = 0; i < userMessages.length; i++) {
47472
+ const msg = userMessages[i];
47473
+ const isLast = i === userMessages.length - 1;
47474
+ if (isLast && msg.role === "user" && frameScreenshot) {
47475
+ messages.push({
47476
+ role: "user",
47477
+ content: [
47478
+ {
47479
+ type: "image_url",
47480
+ image_url: { url: frameScreenshot }
47481
+ },
47482
+ {
47483
+ type: "text",
47484
+ text: `[Screenshot of the referenced frame is attached above]
47485
+
47486
+ ${msg.content}`
47487
+ }
47488
+ ]
47489
+ });
47490
+ } else {
47491
+ messages.push({ role: msg.role, content: msg.content });
47492
+ }
47493
+ }
47494
+ const toolActions = [];
47495
+ let iterations = 0;
47496
+ while (iterations < MAX_ITERATIONS) {
47497
+ iterations++;
47498
+ onUpdate({
47499
+ type: "status",
47500
+ message: iterations === 1 ? "Thinking\u2026" : "Processing tool results\u2026"
47501
+ });
47502
+ const response = await callOpenRouter(messages, apiKey, signal);
47503
+ if (!response.tool_calls || response.tool_calls.length === 0) {
47504
+ const reply = response.content || "Done!";
47505
+ onUpdate({ type: "final", message: reply });
47506
+ return { reply, toolActions };
47507
+ }
47508
+ messages.push({
47509
+ role: "assistant",
47510
+ content: response.content,
47511
+ tool_calls: response.tool_calls
47512
+ });
47513
+ for (const toolCall of response.tool_calls) {
47514
+ const { name, arguments: rawArgs } = toolCall.function;
47515
+ let parsedArgs = {};
47516
+ try {
47517
+ parsedArgs = JSON.parse(rawArgs);
47518
+ } catch {
47519
+ }
47520
+ onUpdate({
47521
+ type: "tool_start",
47522
+ message: formatToolStartMessage(name, parsedArgs)
47523
+ });
47524
+ const result = await executeCanvasTool(name, rawArgs, toolCtx);
47525
+ const action = {
47526
+ toolName: name,
47527
+ args: parsedArgs,
47528
+ result
47529
+ };
47530
+ toolActions.push(action);
47531
+ onUpdate({
47532
+ type: "tool_done",
47533
+ message: result.statusMessage,
47534
+ toolAction: action
47535
+ });
47536
+ messages.push({
47537
+ role: "tool",
47538
+ tool_call_id: toolCall.id,
47539
+ content: JSON.stringify(
47540
+ result.success ? { success: true, ...result.data } : { success: false, error: result.error }
47541
+ )
47542
+ });
47543
+ }
47544
+ }
47545
+ const fallbackReply = "I've completed the available operations. Let me know if you'd like any adjustments!";
47546
+ onUpdate({ type: "final", message: fallbackReply });
47547
+ return { reply: fallbackReply, toolActions };
47548
+ }
47549
+ function formatToolStartMessage(name, args) {
47550
+ switch (name) {
47551
+ case "create_frame":
47552
+ return `Creating frame (${args.width || 512}\xD7${args.height || 512})\u2026`;
47553
+ case "add_rectangle":
47554
+ return `Adding background rectangle\u2026`;
47555
+ case "add_text":
47556
+ return `Adding text "${String(args.text || "").slice(0, 30)}"\u2026`;
47557
+ case "generate_image":
47558
+ return `Generating image \u2014 this may take a moment\u2026`;
47559
+ case "update_element":
47560
+ return `Updating element\u2026`;
47561
+ case "delete_element":
47562
+ return `Deleting element\u2026`;
47563
+ case "get_frame_elements":
47564
+ return `Inspecting frame contents\u2026`;
47565
+ default:
47566
+ return `Running ${name}\u2026`;
47567
+ }
47568
+ }
47569
+
47570
+ // components/ui/chat-container.tsx
47571
+ import { StickToBottom } from "use-stick-to-bottom";
46521
47572
  import { jsx as jsx177 } from "react/jsx-runtime";
47573
+ function ChatContainerRoot({
47574
+ children,
47575
+ className,
47576
+ ...props
47577
+ }) {
47578
+ return /* @__PURE__ */ jsx177(
47579
+ StickToBottom,
47580
+ {
47581
+ className: cn("flex overflow-y-auto", className),
47582
+ resize: "smooth",
47583
+ initial: "instant",
47584
+ role: "log",
47585
+ ...props,
47586
+ children
47587
+ }
47588
+ );
47589
+ }
47590
+ function ChatContainerContent({
47591
+ children,
47592
+ className,
47593
+ ...props
47594
+ }) {
47595
+ return /* @__PURE__ */ jsx177(
47596
+ StickToBottom.Content,
47597
+ {
47598
+ className: cn("flex w-full flex-col", className),
47599
+ ...props,
47600
+ children
47601
+ }
47602
+ );
47603
+ }
47604
+
47605
+ // components/ui/markdown.tsx
47606
+ import { marked } from "marked";
47607
+ import { memo as memo6, useId, useMemo as useMemo13 } from "react";
47608
+ import ReactMarkdown from "react-markdown";
47609
+ import remarkBreaks from "remark-breaks";
47610
+ import remarkGfm from "remark-gfm";
47611
+
47612
+ // components/ui/code-block.tsx
47613
+ import { useEffect as useEffect54, useState as useState48 } from "react";
47614
+ import { codeToHtml } from "shiki";
47615
+ import { jsx as jsx178 } from "react/jsx-runtime";
47616
+ function CodeBlock({ children, className, ...props }) {
47617
+ return /* @__PURE__ */ jsx178(
47618
+ "div",
47619
+ {
47620
+ className: cn(
47621
+ "not-prose flex w-full flex-col overflow-clip border",
47622
+ "border-zinc-200 bg-white text-zinc-950 rounded-xl dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
47623
+ className
47624
+ ),
47625
+ ...props,
47626
+ children
47627
+ }
47628
+ );
47629
+ }
47630
+ function CodeBlockCode({
47631
+ code,
47632
+ language = "tsx",
47633
+ theme = "github-light",
47634
+ className,
47635
+ ...props
47636
+ }) {
47637
+ const [highlightedHtml, setHighlightedHtml] = useState48(null);
47638
+ useEffect54(() => {
47639
+ async function highlight() {
47640
+ if (!code) {
47641
+ setHighlightedHtml("<pre><code></code></pre>");
47642
+ return;
47643
+ }
47644
+ const html = await codeToHtml(code, { lang: language, theme });
47645
+ setHighlightedHtml(html);
47646
+ }
47647
+ highlight();
47648
+ }, [code, language, theme]);
47649
+ const classNames = cn(
47650
+ "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4",
47651
+ className
47652
+ );
47653
+ return highlightedHtml ? /* @__PURE__ */ jsx178(
47654
+ "div",
47655
+ {
47656
+ className: classNames,
47657
+ dangerouslySetInnerHTML: { __html: highlightedHtml },
47658
+ ...props
47659
+ }
47660
+ ) : /* @__PURE__ */ jsx178("div", { className: classNames, ...props, children: /* @__PURE__ */ jsx178("pre", { children: /* @__PURE__ */ jsx178("code", { children: code }) }) });
47661
+ }
47662
+
47663
+ // components/ui/markdown.tsx
47664
+ import { Fragment as Fragment33, jsx as jsx179 } from "react/jsx-runtime";
47665
+ function parseMarkdownIntoBlocks(markdown) {
47666
+ const tokens = marked.lexer(markdown);
47667
+ return tokens.map((token) => token.raw);
47668
+ }
47669
+ function extractLanguage(className) {
47670
+ if (!className) {
47671
+ return "plaintext";
47672
+ }
47673
+ const match = className.match(/language-(\w+)/);
47674
+ return match ? match[1] : "plaintext";
47675
+ }
47676
+ var INITIAL_COMPONENTS = {
47677
+ code: function CodeComponent({ className, children, ...props }) {
47678
+ const isInline = !props.node?.position?.start.line || props.node?.position?.start.line === props.node?.position?.end.line;
47679
+ if (isInline) {
47680
+ return /* @__PURE__ */ jsx179(
47681
+ "span",
47682
+ {
47683
+ className: cn(
47684
+ "bg-zinc-50 rounded-sm px-1 font-mono text-sm dark:bg-zinc-900",
47685
+ className
47686
+ ),
47687
+ ...props,
47688
+ children
47689
+ }
47690
+ );
47691
+ }
47692
+ const language = extractLanguage(className);
47693
+ return /* @__PURE__ */ jsx179(CodeBlock, { className, children: /* @__PURE__ */ jsx179(CodeBlockCode, { code: children, language }) });
47694
+ },
47695
+ pre: function PreComponent({ children }) {
47696
+ return /* @__PURE__ */ jsx179(Fragment33, { children });
47697
+ }
47698
+ };
47699
+ var MemoizedMarkdownBlock = memo6(
47700
+ ({
47701
+ content,
47702
+ components = INITIAL_COMPONENTS
47703
+ }) => {
47704
+ return /* @__PURE__ */ jsx179(
47705
+ ReactMarkdown,
47706
+ {
47707
+ remarkPlugins: [remarkGfm, remarkBreaks],
47708
+ components,
47709
+ children: content
47710
+ }
47711
+ );
47712
+ },
47713
+ (prevProps, nextProps) => {
47714
+ return prevProps.content === nextProps.content;
47715
+ }
47716
+ );
47717
+ MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
47718
+ function MarkdownComponent({
47719
+ children,
47720
+ id,
47721
+ className,
47722
+ components = INITIAL_COMPONENTS
47723
+ }) {
47724
+ const generatedId = useId();
47725
+ const blockId = id ?? generatedId;
47726
+ const blocks = useMemo13(() => parseMarkdownIntoBlocks(children), [children]);
47727
+ return /* @__PURE__ */ jsx179("div", { className, children: blocks.map((block, index) => /* @__PURE__ */ jsx179(
47728
+ MemoizedMarkdownBlock,
47729
+ {
47730
+ content: block,
47731
+ components
47732
+ },
47733
+ `${blockId}-block-${index}`
47734
+ )) });
47735
+ }
47736
+ var Markdown = memo6(MarkdownComponent);
47737
+ Markdown.displayName = "Markdown";
47738
+
47739
+ // components/ui/avatar.tsx
47740
+ import * as React55 from "react";
47741
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
47742
+ import { jsx as jsx180 } from "react/jsx-runtime";
47743
+ var Avatar2 = React55.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx180(
47744
+ AvatarPrimitive.Root,
47745
+ {
47746
+ ref,
47747
+ className: cn(
47748
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
47749
+ className
47750
+ ),
47751
+ ...props
47752
+ }
47753
+ ));
47754
+ Avatar2.displayName = AvatarPrimitive.Root.displayName;
47755
+ var AvatarImage = React55.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx180(
47756
+ AvatarPrimitive.Image,
47757
+ {
47758
+ ref,
47759
+ className: cn("aspect-square h-full w-full", className),
47760
+ ...props
47761
+ }
47762
+ ));
47763
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
47764
+ var AvatarFallback = React55.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx180(
47765
+ AvatarPrimitive.Fallback,
47766
+ {
47767
+ ref,
47768
+ className: cn(
47769
+ "flex h-full w-full items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800",
47770
+ className
47771
+ ),
47772
+ ...props
47773
+ }
47774
+ ));
47775
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
47776
+
47777
+ // components/ui/message.tsx
47778
+ import { jsx as jsx181, jsxs as jsxs99 } from "react/jsx-runtime";
47779
+ var Message = ({ children, className, ...props }) => /* @__PURE__ */ jsx181("div", { className: cn("flex gap-3", className), ...props, children });
47780
+ var MessageContent = ({
47781
+ children,
47782
+ markdown = false,
47783
+ className,
47784
+ ...props
47785
+ }) => {
47786
+ const classNames = cn(
47787
+ "rounded-lg p-2 text-zinc-950 bg-zinc-100 prose break-words whitespace-normal dark:text-zinc-50 dark:bg-zinc-800",
47788
+ className
47789
+ );
47790
+ return markdown ? /* @__PURE__ */ jsx181(Markdown, { className: classNames, ...props, children }) : /* @__PURE__ */ jsx181("div", { className: classNames, ...props, children });
47791
+ };
47792
+ var MessageActions = ({
47793
+ children,
47794
+ className,
47795
+ ...props
47796
+ }) => /* @__PURE__ */ jsx181(
47797
+ "div",
47798
+ {
47799
+ className: cn(
47800
+ "text-zinc-500 flex items-center gap-2 dark:text-zinc-400",
47801
+ className
47802
+ ),
47803
+ ...props,
47804
+ children
47805
+ }
47806
+ );
47807
+ var MessageAction = ({
47808
+ tooltip,
47809
+ children,
47810
+ className,
47811
+ side = "top",
47812
+ ...props
47813
+ }) => {
47814
+ return /* @__PURE__ */ jsx181(TooltipProvider, { children: /* @__PURE__ */ jsxs99(Tooltip2, { ...props, children: [
47815
+ /* @__PURE__ */ jsx181(TooltipTrigger, { asChild: true, children }),
47816
+ /* @__PURE__ */ jsx181(TooltipContent, { side, className, children: tooltip })
47817
+ ] }) });
47818
+ };
47819
+
47820
+ // components/ui/scroll-button.tsx
47821
+ import { ChevronDown } from "lucide-react";
47822
+ import { useStickToBottomContext } from "use-stick-to-bottom";
47823
+
47824
+ // components/ui/button.tsx
47825
+ import * as React56 from "react";
47826
+ import { Slot } from "@radix-ui/react-slot";
47827
+ import { cva } from "class-variance-authority";
47828
+ import { jsx as jsx182 } from "react/jsx-runtime";
47829
+ var buttonVariants = cva(
47830
+ "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",
47831
+ {
47832
+ variants: {
47833
+ variant: {
47834
+ 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",
47835
+ 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",
47836
+ 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",
47837
+ 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",
47838
+ ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
47839
+ link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50"
47840
+ },
47841
+ size: {
47842
+ default: "h-9 px-4 py-2",
47843
+ sm: "h-8 rounded-md px-3 text-xs",
47844
+ lg: "h-10 rounded-md px-8",
47845
+ icon: "h-9 w-9"
47846
+ }
47847
+ },
47848
+ defaultVariants: {
47849
+ variant: "default",
47850
+ size: "default"
47851
+ }
47852
+ }
47853
+ );
47854
+ var Button2 = React56.forwardRef(
47855
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
47856
+ const Comp = asChild ? Slot : "button";
47857
+ return /* @__PURE__ */ jsx182(
47858
+ Comp,
47859
+ {
47860
+ className: cn(buttonVariants({ variant, size, className })),
47861
+ ref,
47862
+ ...props
47863
+ }
47864
+ );
47865
+ }
47866
+ );
47867
+ Button2.displayName = "Button";
47868
+
47869
+ // components/ui/scroll-button.tsx
47870
+ import { jsx as jsx183 } from "react/jsx-runtime";
47871
+ function ScrollButton({
47872
+ className,
47873
+ variant = "outline",
47874
+ size = "sm",
47875
+ ...props
47876
+ }) {
47877
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
47878
+ return /* @__PURE__ */ jsx183(
47879
+ Button2,
47880
+ {
47881
+ variant,
47882
+ size,
47883
+ className: cn(
47884
+ "h-10 w-10 rounded-full transition-all duration-150 ease-out",
47885
+ !isAtBottom ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-95 opacity-0",
47886
+ className
47887
+ ),
47888
+ onClick: () => scrollToBottom(),
47889
+ ...props,
47890
+ children: /* @__PURE__ */ jsx183(ChevronDown, { className: "h-5 w-5" })
47891
+ }
47892
+ );
47893
+ }
47894
+
47895
+ // components/AIChatPanel.tsx
47896
+ import { jsx as jsx184, jsxs as jsxs100 } from "react/jsx-runtime";
47897
+ function genId() {
47898
+ return Math.random().toString(36).slice(2, 10);
47899
+ }
47900
+ var SUGGESTIONS = [
47901
+ "Create a pasta ad",
47902
+ "Design a sale banner",
47903
+ "Make a product showcase",
47904
+ "Help me design"
47905
+ ];
47906
+ var AIChatPanel = ({
47907
+ isOpen,
47908
+ onClose,
47909
+ apiKey,
47910
+ excalidrawAPI,
47911
+ geminiApiKey
47912
+ }) => {
47913
+ const [prompt, setPrompt] = useState49("");
47914
+ const [isLoading, setIsLoading] = useState49(false);
47915
+ const [statusText, setStatusText] = useState49("");
47916
+ const [messages, setMessages] = useState49([]);
47917
+ const [sessions, setSessions] = useState49([]);
47918
+ const [currentSessionId, setCurrentSessionId] = useState49(genId);
47919
+ const [historyOpen, setHistoryOpen] = useState49(false);
47920
+ const [historySearch, setHistorySearch] = useState49("");
47921
+ const [error, setError] = useState49(null);
47922
+ const [frameRef, setFrameRef] = useState49(null);
47923
+ const [mentionMenuOpen, setMentionMenuOpen] = useState49(false);
47924
+ const [availableFrames, setAvailableFrames] = useState49([]);
47925
+ const historyRef = useRef49(null);
47926
+ const abortControllerRef = useRef49(null);
47927
+ const mentionMenuRef = useRef49(null);
47928
+ useEffect55(() => {
47929
+ const handler = (e) => {
47930
+ if (historyRef.current && !historyRef.current.contains(e.target)) {
47931
+ setHistoryOpen(false);
47932
+ }
47933
+ if (mentionMenuRef.current && !mentionMenuRef.current.contains(e.target)) {
47934
+ setMentionMenuOpen(false);
47935
+ }
47936
+ };
47937
+ document.addEventListener("mousedown", handler);
47938
+ return () => document.removeEventListener("mousedown", handler);
47939
+ }, []);
47940
+ useEffect55(() => {
47941
+ if (!isOpen) {
47942
+ abortControllerRef.current?.abort();
47943
+ }
47944
+ }, [isOpen]);
47945
+ const currentTitle = messages.length > 0 ? messages[0].content.slice(0, 30) + (messages[0].content.length > 30 ? "\u2026" : "") : "New chat";
47946
+ const saveCurrentSession = useCallback24(() => {
47947
+ if (messages.length === 0) {
47948
+ return;
47949
+ }
47950
+ const title = messages[0].content.slice(0, 40) + (messages[0].content.length > 40 ? "\u2026" : "");
47951
+ setSessions((prev) => {
47952
+ const existing = prev.findIndex((s) => s.id === currentSessionId);
47953
+ const updated = { id: currentSessionId, title, messages };
47954
+ if (existing >= 0) {
47955
+ const next = [...prev];
47956
+ next[existing] = updated;
47957
+ return next;
47958
+ }
47959
+ return [updated, ...prev];
47960
+ });
47961
+ }, [messages, currentSessionId]);
47962
+ const handleNewChat = useCallback24(() => {
47963
+ saveCurrentSession();
47964
+ setMessages([]);
47965
+ setCurrentSessionId(genId());
47966
+ setHistoryOpen(false);
47967
+ setError(null);
47968
+ setPrompt("");
47969
+ setFrameRef(null);
47970
+ setStatusText("");
47971
+ }, [saveCurrentSession]);
47972
+ const handleSwitchSession = useCallback24(
47973
+ (session) => {
47974
+ saveCurrentSession();
47975
+ setMessages(session.messages);
47976
+ setCurrentSessionId(session.id);
47977
+ setHistoryOpen(false);
47978
+ setError(null);
47979
+ },
47980
+ [saveCurrentSession]
47981
+ );
47982
+ const handleAtMention = useCallback24(() => {
47983
+ if (!excalidrawAPI) {
47984
+ return;
47985
+ }
47986
+ setAvailableFrames(listFrames(excalidrawAPI));
47987
+ setMentionMenuOpen(true);
47988
+ }, [excalidrawAPI]);
47989
+ const handlePickFrame = useCallback24(
47990
+ async (frame) => {
47991
+ if (!excalidrawAPI) {
47992
+ return;
47993
+ }
47994
+ setMentionMenuOpen(false);
47995
+ const ctx = getFrameContext(excalidrawAPI, frame.id);
47996
+ if (!ctx) {
47997
+ return;
47998
+ }
47999
+ const screenshot = await captureFrameScreenshot(
48000
+ excalidrawAPI,
48001
+ frame.id
48002
+ );
48003
+ setFrameRef({
48004
+ frameId: frame.id,
48005
+ label: ctx.frameInfo.name,
48006
+ serialized: ctx.serialized,
48007
+ screenshot: screenshot ?? void 0
48008
+ });
48009
+ },
48010
+ [excalidrawAPI]
48011
+ );
48012
+ const handleRemoveRef = useCallback24(() => {
48013
+ setFrameRef(null);
48014
+ }, []);
48015
+ const handleSend = useCallback24(async () => {
48016
+ const text = prompt.trim();
48017
+ const normalizedKey = apiKey.trim();
48018
+ if (!text || isLoading) {
48019
+ return;
48020
+ }
48021
+ if (!normalizedKey) {
48022
+ setError(
48023
+ "No OpenRouter API key. Set VITE_APP_OPENROUTER_API_KEY in .env or pass the key via the `apiKey` prop."
48024
+ );
48025
+ return;
48026
+ }
48027
+ const userMsg = {
48028
+ id: genId(),
48029
+ role: "user",
48030
+ content: text,
48031
+ timestamp: /* @__PURE__ */ new Date()
48032
+ };
48033
+ const nextMessages = [...messages, userMsg];
48034
+ setMessages(nextMessages);
48035
+ setPrompt("");
48036
+ const controller = new AbortController();
48037
+ abortControllerRef.current = controller;
48038
+ setIsLoading(true);
48039
+ setError(null);
48040
+ setStatusText("Thinking\u2026");
48041
+ const capturedFrameRef = frameRef;
48042
+ setFrameRef(null);
48043
+ if (!excalidrawAPI) {
48044
+ try {
48045
+ const reply = await callPlainChatAPI(
48046
+ nextMessages,
48047
+ normalizedKey,
48048
+ controller.signal
48049
+ );
48050
+ setMessages((prev) => [
48051
+ ...prev,
48052
+ {
48053
+ id: genId(),
48054
+ role: "assistant",
48055
+ content: reply,
48056
+ timestamp: /* @__PURE__ */ new Date()
48057
+ }
48058
+ ]);
48059
+ } catch (err) {
48060
+ if (err instanceof Error && err.name !== "AbortError") {
48061
+ setError(err.message);
48062
+ }
48063
+ } finally {
48064
+ abortControllerRef.current = null;
48065
+ setIsLoading(false);
48066
+ setStatusText("");
48067
+ }
48068
+ return;
48069
+ }
48070
+ try {
48071
+ const result = await runAgentLoop({
48072
+ userMessages: nextMessages.map((m) => ({
48073
+ role: m.role,
48074
+ content: m.content
48075
+ })),
48076
+ elementContext: capturedFrameRef?.serialized,
48077
+ frameScreenshot: capturedFrameRef?.screenshot,
48078
+ apiKey: normalizedKey,
48079
+ toolCtx: {
48080
+ excalidrawAPI,
48081
+ geminiApiKey: geminiApiKey || "",
48082
+ signal: controller.signal
48083
+ },
48084
+ onUpdate: (update) => {
48085
+ if (update.type !== "final") {
48086
+ setStatusText(update.message);
48087
+ }
48088
+ },
48089
+ signal: controller.signal
48090
+ });
48091
+ setMessages((prev) => [
48092
+ ...prev,
48093
+ {
48094
+ id: genId(),
48095
+ role: "assistant",
48096
+ content: result.reply,
48097
+ timestamp: /* @__PURE__ */ new Date(),
48098
+ toolActions: result.toolActions.length > 0 ? result.toolActions : void 0
48099
+ }
48100
+ ]);
48101
+ } catch (err) {
48102
+ if (err instanceof Error && err.name !== "AbortError") {
48103
+ setError(err.message);
48104
+ }
48105
+ } finally {
48106
+ abortControllerRef.current = null;
48107
+ setIsLoading(false);
48108
+ setStatusText("");
48109
+ }
48110
+ }, [
48111
+ prompt,
48112
+ isLoading,
48113
+ messages,
48114
+ apiKey,
48115
+ excalidrawAPI,
48116
+ geminiApiKey,
48117
+ frameRef
48118
+ ]);
48119
+ const handleStop = useCallback24(() => {
48120
+ abortControllerRef.current?.abort();
48121
+ }, []);
48122
+ const handleChip = useCallback24((chip) => {
48123
+ setPrompt(chip);
48124
+ }, []);
48125
+ const filteredSessions = sessions.filter(
48126
+ (s) => historySearch ? s.title.toLowerCase().includes(historySearch.toLowerCase()) : true
48127
+ );
48128
+ if (!isOpen) {
48129
+ return null;
48130
+ }
48131
+ return /* @__PURE__ */ jsx184(
48132
+ "div",
48133
+ {
48134
+ className: "acp",
48135
+ onPointerDown: (e) => e.stopPropagation(),
48136
+ onClick: (e) => e.stopPropagation(),
48137
+ children: /* @__PURE__ */ jsxs100("div", { className: "acp-panel", children: [
48138
+ /* @__PURE__ */ jsxs100("div", { className: "acp-header", ref: historyRef, children: [
48139
+ /* @__PURE__ */ jsxs100(
48140
+ "button",
48141
+ {
48142
+ className: "acp-title-btn",
48143
+ onClick: () => setHistoryOpen((v) => !v),
48144
+ title: "Chat history",
48145
+ children: [
48146
+ /* @__PURE__ */ jsx184("span", { children: currentTitle }),
48147
+ /* @__PURE__ */ jsx184(ChevronDown2, { size: 13 })
48148
+ ]
48149
+ }
48150
+ ),
48151
+ /* @__PURE__ */ jsxs100("div", { className: "acp-header-right", children: [
48152
+ /* @__PURE__ */ jsx184(
48153
+ "button",
48154
+ {
48155
+ className: "acp-icon-btn",
48156
+ onClick: handleNewChat,
48157
+ title: "New chat",
48158
+ children: /* @__PURE__ */ jsx184(Plus, { size: 15 })
48159
+ }
48160
+ ),
48161
+ /* @__PURE__ */ jsx184("button", { className: "acp-icon-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsx184(X2, { size: 15 }) })
48162
+ ] }),
48163
+ historyOpen && /* @__PURE__ */ jsxs100("div", { className: "acp-history-dropdown", children: [
48164
+ /* @__PURE__ */ jsxs100("div", { className: "acp-history-search", children: [
48165
+ /* @__PURE__ */ jsx184(Search, { size: 13 }),
48166
+ /* @__PURE__ */ jsx184(
48167
+ "input",
48168
+ {
48169
+ type: "search",
48170
+ placeholder: "Search chats\u2026",
48171
+ value: historySearch,
48172
+ onChange: (e) => setHistorySearch(e.target.value),
48173
+ autoFocus: true
48174
+ }
48175
+ )
48176
+ ] }),
48177
+ /* @__PURE__ */ jsxs100("div", { className: "acp-history-list", children: [
48178
+ /* @__PURE__ */ jsxs100(
48179
+ "button",
48180
+ {
48181
+ className: "acp-history-item acp-history-item--new",
48182
+ onClick: handleNewChat,
48183
+ children: [
48184
+ /* @__PURE__ */ jsx184(Plus, { size: 13 }),
48185
+ /* @__PURE__ */ jsx184("span", { children: "New chat" })
48186
+ ]
48187
+ }
48188
+ ),
48189
+ filteredSessions.map((session) => /* @__PURE__ */ jsxs100(
48190
+ "button",
48191
+ {
48192
+ className: "acp-history-item",
48193
+ onClick: () => handleSwitchSession(session),
48194
+ children: [
48195
+ /* @__PURE__ */ jsx184(MessageSquare, { size: 13 }),
48196
+ /* @__PURE__ */ jsx184("span", { children: session.title })
48197
+ ]
48198
+ },
48199
+ session.id
48200
+ )),
48201
+ filteredSessions.length === 0 && historySearch && /* @__PURE__ */ jsx184("div", { className: "acp-history-empty", children: "No matching chats" })
48202
+ ] })
48203
+ ] })
48204
+ ] }),
48205
+ /* @__PURE__ */ jsx184("div", { className: "acp-messages-wrap", children: messages.length === 0 && !isLoading ? /* @__PURE__ */ jsxs100("div", { className: "acp-empty", children: [
48206
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-icon", children: /* @__PURE__ */ jsx184(MessageSquare, { size: 22 }) }),
48207
+ /* @__PURE__ */ jsxs100("div", { children: [
48208
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-title", children: "AI Ad Designer" }),
48209
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-sub", children: "Describe an ad and I'll create it on the canvas. Use @ to reference selected elements." })
48210
+ ] }),
48211
+ /* @__PURE__ */ jsx184("div", { className: "acp-chips", children: SUGGESTIONS.map((chip) => /* @__PURE__ */ jsx184(
48212
+ "button",
48213
+ {
48214
+ className: "acp-chip",
48215
+ onClick: () => handleChip(chip),
48216
+ children: chip
48217
+ },
48218
+ chip
48219
+ )) })
48220
+ ] }) : /* @__PURE__ */ jsxs100(ChatContainerRoot, { className: "acp-chat-root", children: [
48221
+ /* @__PURE__ */ jsxs100(ChatContainerContent, { className: "acp-chat-content", children: [
48222
+ messages.map((msg, index) => {
48223
+ const isAssistant = msg.role === "assistant";
48224
+ const isLast = index === messages.length - 1;
48225
+ return /* @__PURE__ */ jsx184(
48226
+ Message,
48227
+ {
48228
+ className: `acp-msg ${isAssistant ? "acp-msg--assistant" : "acp-msg--user"}`,
48229
+ children: isAssistant ? /* @__PURE__ */ jsxs100("div", { className: "acp-msg-inner", children: [
48230
+ msg.toolActions && msg.toolActions.length > 0 && /* @__PURE__ */ jsx184(ToolActionsDisplay, { actions: msg.toolActions }),
48231
+ /* @__PURE__ */ jsx184(
48232
+ MessageContent,
48233
+ {
48234
+ markdown: true,
48235
+ className: "acp-content-assistant",
48236
+ children: msg.content
48237
+ }
48238
+ ),
48239
+ /* @__PURE__ */ jsxs100(
48240
+ MessageActions,
48241
+ {
48242
+ className: `acp-msg-actions${isLast ? " acp-msg-actions--visible" : ""}`,
48243
+ children: [
48244
+ /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Copy", delayDuration: 100, children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(Copy, { size: 14 }) }) }),
48245
+ /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Upvote", delayDuration: 100, children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(ThumbsUp, { size: 14 }) }) }),
48246
+ /* @__PURE__ */ jsx184(
48247
+ MessageAction,
48248
+ {
48249
+ tooltip: "Downvote",
48250
+ delayDuration: 100,
48251
+ children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(ThumbsDown, { size: 14 }) })
48252
+ }
48253
+ )
48254
+ ]
48255
+ }
48256
+ )
48257
+ ] }) : /* @__PURE__ */ jsxs100("div", { className: "acp-msg-inner", children: [
48258
+ /* @__PURE__ */ jsx184(MessageContent, { className: "acp-content-user", children: msg.content }),
48259
+ /* @__PURE__ */ jsxs100(MessageActions, { className: "acp-msg-actions", children: [
48260
+ /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Edit", delayDuration: 100, children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(Pencil, { size: 14 }) }) }),
48261
+ /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Delete", delayDuration: 100, children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(Trash, { size: 14 }) }) }),
48262
+ /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Copy", delayDuration: 100, children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(Copy, { size: 14 }) }) })
48263
+ ] })
48264
+ ] })
48265
+ },
48266
+ msg.id
48267
+ );
48268
+ }),
48269
+ 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: [
48270
+ /* @__PURE__ */ jsxs100("div", { className: "acp-loading-dots", children: [
48271
+ /* @__PURE__ */ jsx184("span", {}),
48272
+ /* @__PURE__ */ jsx184("span", {}),
48273
+ /* @__PURE__ */ jsx184("span", {})
48274
+ ] }),
48275
+ statusText && /* @__PURE__ */ jsx184("span", { className: "acp-status-text", children: statusText })
48276
+ ] }) }) })
48277
+ ] }),
48278
+ /* @__PURE__ */ jsx184("div", { className: "acp-scroll-anchor", children: /* @__PURE__ */ jsx184(ScrollButton, { className: "acp-scroll-btn" }) })
48279
+ ] }) }),
48280
+ error && /* @__PURE__ */ jsx184("div", { className: "acp-error", children: error }),
48281
+ frameRef && /* @__PURE__ */ jsx184("div", { className: "acp-ref-area", children: /* @__PURE__ */ jsxs100("div", { className: "acp-ref-pill", children: [
48282
+ /* @__PURE__ */ jsx184(AtSign, { size: 12 }),
48283
+ /* @__PURE__ */ jsx184("span", { children: frameRef.label }),
48284
+ /* @__PURE__ */ jsx184(
48285
+ "button",
48286
+ {
48287
+ className: "acp-ref-remove",
48288
+ onClick: handleRemoveRef,
48289
+ title: "Remove reference",
48290
+ children: /* @__PURE__ */ jsx184(X2, { size: 10 })
48291
+ }
48292
+ )
48293
+ ] }) }),
48294
+ /* @__PURE__ */ jsx184("div", { className: "acp-input-area", children: /* @__PURE__ */ jsxs100("div", { className: "acp-input-wrapper", ref: mentionMenuRef, children: [
48295
+ mentionMenuOpen && /* @__PURE__ */ jsx184("div", { className: "acp-mention-menu", children: availableFrames.length > 0 ? availableFrames.map((f) => /* @__PURE__ */ jsxs100(
48296
+ "button",
48297
+ {
48298
+ className: "acp-mention-item",
48299
+ onClick: () => handlePickFrame(f),
48300
+ children: [
48301
+ /* @__PURE__ */ jsx184(AtSign, { size: 14 }),
48302
+ /* @__PURE__ */ jsxs100("span", { children: [
48303
+ f.name,
48304
+ " ",
48305
+ /* @__PURE__ */ jsxs100("span", { className: "acp-mention-dim", children: [
48306
+ "(",
48307
+ f.width,
48308
+ "x",
48309
+ f.height,
48310
+ ", ",
48311
+ f.childCount,
48312
+ " items)"
48313
+ ] })
48314
+ ] })
48315
+ ]
48316
+ },
48317
+ f.id
48318
+ )) : /* @__PURE__ */ jsx184("div", { className: "acp-mention-empty", children: "No frames on the canvas. Create a frame first, then use @ to reference it." }) }),
48319
+ /* @__PURE__ */ jsxs100(
48320
+ PromptInput,
48321
+ {
48322
+ isLoading,
48323
+ value: prompt,
48324
+ onValueChange: setPrompt,
48325
+ onSubmit: handleSend,
48326
+ className: "acp-prompt-input",
48327
+ children: [
48328
+ /* @__PURE__ */ jsx184(
48329
+ PromptInputTextarea,
48330
+ {
48331
+ placeholder: excalidrawAPI ? "Describe an ad to create\u2026" : "Ask anything",
48332
+ className: "acp-textarea"
48333
+ }
48334
+ ),
48335
+ /* @__PURE__ */ jsxs100(PromptInputActions, { className: "acp-actions-bar", children: [
48336
+ /* @__PURE__ */ jsxs100("div", { className: "acp-actions-left", children: [
48337
+ excalidrawAPI && /* @__PURE__ */ jsx184(PromptInputAction, { tooltip: "Reference selection (@)", children: /* @__PURE__ */ jsx184(
48338
+ "button",
48339
+ {
48340
+ className: "acp-pill-btn acp-pill-btn--icon",
48341
+ onClick: handleAtMention,
48342
+ children: /* @__PURE__ */ jsx184(AtSign, { size: 16 })
48343
+ }
48344
+ ) }),
48345
+ /* @__PURE__ */ jsx184(PromptInputAction, { tooltip: "Search", children: /* @__PURE__ */ jsxs100("button", { className: "acp-pill-btn", children: [
48346
+ /* @__PURE__ */ jsx184(Globe, { size: 15 }),
48347
+ /* @__PURE__ */ jsx184("span", { children: "Search" })
48348
+ ] }) }),
48349
+ /* @__PURE__ */ jsx184(PromptInputAction, { tooltip: "More", children: /* @__PURE__ */ jsx184("button", { className: "acp-pill-btn acp-pill-btn--icon", children: /* @__PURE__ */ jsx184(MoreHorizontal, { size: 16 }) }) })
48350
+ ] }),
48351
+ /* @__PURE__ */ jsxs100("div", { className: "acp-actions-right", children: [
48352
+ /* @__PURE__ */ jsx184(PromptInputAction, { tooltip: "Voice input", children: /* @__PURE__ */ jsx184("button", { className: "acp-pill-btn acp-pill-btn--icon", children: /* @__PURE__ */ jsx184(Mic, { size: 16 }) }) }),
48353
+ /* @__PURE__ */ jsx184(
48354
+ "button",
48355
+ {
48356
+ className: "acp-send-btn",
48357
+ onClick: isLoading ? handleStop : handleSend,
48358
+ disabled: !isLoading && !prompt.trim(),
48359
+ title: isLoading ? "Stop" : "Send",
48360
+ children: isLoading ? /* @__PURE__ */ jsx184("span", { className: "acp-stop-square" }) : /* @__PURE__ */ jsx184(ArrowUp3, { size: 16 })
48361
+ }
48362
+ )
48363
+ ] })
48364
+ ] })
48365
+ ]
48366
+ }
48367
+ )
48368
+ ] }) })
48369
+ ] })
48370
+ }
48371
+ );
48372
+ };
48373
+ function ToolActionsDisplay({ actions: actions2 }) {
48374
+ const [expanded, setExpanded] = useState49(false);
48375
+ return /* @__PURE__ */ jsxs100("div", { className: "acp-tool-actions", children: [
48376
+ /* @__PURE__ */ jsxs100(
48377
+ "button",
48378
+ {
48379
+ className: "acp-tool-actions-toggle",
48380
+ onClick: () => setExpanded((v) => !v),
48381
+ children: [
48382
+ /* @__PURE__ */ jsx184(Wrench, { size: 13 }),
48383
+ /* @__PURE__ */ jsxs100("span", { children: [
48384
+ actions2.length,
48385
+ " action",
48386
+ actions2.length !== 1 ? "s" : "",
48387
+ " performed"
48388
+ ] }),
48389
+ /* @__PURE__ */ jsx184(
48390
+ ChevronRight,
48391
+ {
48392
+ size: 12,
48393
+ className: `acp-tool-chevron ${expanded ? "acp-tool-chevron--open" : ""}`
48394
+ }
48395
+ )
48396
+ ]
48397
+ }
48398
+ ),
48399
+ expanded && /* @__PURE__ */ jsx184("div", { className: "acp-tool-actions-list", children: actions2.map((action, i) => /* @__PURE__ */ jsxs100(
48400
+ "div",
48401
+ {
48402
+ className: `acp-tool-action-item ${action.result.success ? "acp-tool-action-item--ok" : "acp-tool-action-item--err"}`,
48403
+ children: [
48404
+ /* @__PURE__ */ jsx184("span", { className: "acp-tool-action-dot" }),
48405
+ /* @__PURE__ */ jsx184("span", { children: action.result.statusMessage })
48406
+ ]
48407
+ },
48408
+ i
48409
+ )) })
48410
+ ] });
48411
+ }
48412
+ async function callPlainChatAPI(messages, apiKey, signal) {
48413
+ const response = await fetch(
48414
+ "https://openrouter.ai/api/v1/chat/completions",
48415
+ {
48416
+ method: "POST",
48417
+ signal,
48418
+ headers: {
48419
+ Authorization: `Bearer ${apiKey}`,
48420
+ "Content-Type": "application/json"
48421
+ },
48422
+ body: JSON.stringify({
48423
+ model: "openai/gpt-4.1-mini",
48424
+ messages: messages.map((m) => ({ role: m.role, content: m.content }))
48425
+ })
48426
+ }
48427
+ );
48428
+ if (!response.ok) {
48429
+ let message = `OpenRouter error ${response.status}`;
48430
+ try {
48431
+ const err = await response.json();
48432
+ if (err?.error?.message) {
48433
+ message = err.error.message;
48434
+ }
48435
+ } catch {
48436
+ }
48437
+ throw new Error(message);
48438
+ }
48439
+ const data = await response.json();
48440
+ const content = data?.choices?.[0]?.message?.content;
48441
+ if (!content) {
48442
+ throw new Error("No response returned by the API.");
48443
+ }
48444
+ return content;
48445
+ }
48446
+
48447
+ // index.tsx
48448
+ import { jsx as jsx185 } from "react/jsx-runtime";
46522
48449
  polyfill_default();
46523
48450
  var ExcalidrawBase = (props) => {
46524
48451
  const {
@@ -46574,7 +48501,7 @@ var ExcalidrawBase = (props) => {
46574
48501
  if (UIOptions.canvasActions.toggleTheme === null && typeof theme === "undefined") {
46575
48502
  UIOptions.canvasActions.toggleTheme = true;
46576
48503
  }
46577
- useEffect54(() => {
48504
+ useEffect56(() => {
46578
48505
  const importPolyfill = async () => {
46579
48506
  await import("canvas-roundrect-polyfill");
46580
48507
  };
@@ -46591,7 +48518,7 @@ var ExcalidrawBase = (props) => {
46591
48518
  document.removeEventListener("touchmove", handleTouchMove);
46592
48519
  };
46593
48520
  }, []);
46594
- return /* @__PURE__ */ jsx177(EditorJotaiProvider, { store: editorJotaiStore, children: /* @__PURE__ */ jsx177(InitializeApp, { langCode, theme, children: /* @__PURE__ */ jsx177(
48521
+ return /* @__PURE__ */ jsx185(EditorJotaiProvider, { store: editorJotaiStore, children: /* @__PURE__ */ jsx185(InitializeApp, { langCode, theme, children: /* @__PURE__ */ jsx185(
46595
48522
  App_default,
46596
48523
  {
46597
48524
  onChange,
@@ -46670,9 +48597,10 @@ var areEqual5 = (prevProps, nextProps) => {
46670
48597
  });
46671
48598
  return isUIOptionsSame && isShallowEqual10(prev, next);
46672
48599
  };
46673
- var Excalidraw = React54.memo(ExcalidrawBase, areEqual5);
48600
+ var Excalidraw = React58.memo(ExcalidrawBase, areEqual5);
46674
48601
  Excalidraw.displayName = "Excalidraw";
46675
48602
  export {
48603
+ AIChatPanel,
46676
48604
  Button,
46677
48605
  CaptureUpdateAction39 as CaptureUpdateAction,
46678
48606
  CommandPalette,