@orangecatai/adgen-canvas 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/dev/{chunk-YOQUQ6O5.js → chunk-3FGOYDLK.js} +3 -3
  2. package/dist/dev/{chunk-EP27W34X.js → chunk-IRIUFXMO.js} +2 -2
  3. package/dist/dev/data/{image-IZS5VEYX.js → image-HH4XNQRO.js} +3 -3
  4. package/dist/dev/index.css +149 -1
  5. package/dist/dev/index.css.map +2 -2
  6. package/dist/dev/index.js +1486 -599
  7. package/dist/dev/index.js.map +4 -4
  8. package/dist/dev/subset-shared.chunk.js +1 -1
  9. package/dist/dev/subset-worker.chunk.js +1 -1
  10. package/dist/prod/{chunk-2ALGWY4R.js → chunk-LUZI7MFZ.js} +2 -2
  11. package/dist/prod/{chunk-I4SUBR2Z.js → chunk-SG4RCQVC.js} +1 -1
  12. package/dist/prod/data/image-J2ZJZU4A.js +1 -0
  13. package/dist/prod/index.css +1 -1
  14. package/dist/prod/index.js +42 -32
  15. package/dist/prod/subset-shared.chunk.js +1 -1
  16. package/dist/prod/subset-worker.chunk.js +1 -1
  17. package/dist/types/excalidraw/actions/actionAddToLibrary.d.ts +1 -1
  18. package/dist/types/excalidraw/actions/actionCanvas.d.ts +177 -0
  19. package/dist/types/excalidraw/actions/actionClipboard.d.ts +7 -7
  20. package/dist/types/excalidraw/actions/actionCropEditor.d.ts +2 -2
  21. package/dist/types/excalidraw/actions/actionDuplicateSelection.d.ts +2 -2
  22. package/dist/types/excalidraw/actions/actionElementLink.d.ts +6 -6
  23. package/dist/types/excalidraw/actions/actionEmbeddable.d.ts +1 -1
  24. package/dist/types/excalidraw/actions/actionLinearEditor.d.ts +7 -7
  25. package/dist/types/excalidraw/actions/actionLink.d.ts +3 -3
  26. package/dist/types/excalidraw/actions/actionMenu.d.ts +1 -1
  27. package/dist/types/excalidraw/actions/actionSelectAll.d.ts +4 -4
  28. package/dist/types/excalidraw/actions/actionStyles.d.ts +2 -2
  29. package/dist/types/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +4 -4
  30. package/dist/types/excalidraw/actions/actionToggleShapeSwitch.d.ts +3 -3
  31. package/dist/types/excalidraw/actions/actionToggleStats.d.ts +2 -2
  32. package/dist/types/excalidraw/actions/actionToggleViewMode.d.ts +4 -4
  33. package/dist/types/excalidraw/actions/actionToggleZenMode.d.ts +4 -4
  34. package/dist/types/excalidraw/actions/actionZindex.d.ts +8 -8
  35. package/dist/types/excalidraw/actions/index.d.ts +1 -1
  36. package/dist/types/excalidraw/actions/types.d.ts +1 -1
  37. package/dist/types/excalidraw/components/AIChatPanel.d.ts +49 -3
  38. package/dist/types/excalidraw/components/Actions.d.ts +3 -0
  39. package/dist/types/excalidraw/components/DefaultSidebar.d.ts +1 -1
  40. package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +6 -1
  41. package/dist/types/excalidraw/components/ImageQuickEditPanel.d.ts +6 -1
  42. package/dist/types/excalidraw/components/Sidebar/Sidebar.d.ts +2 -2
  43. package/dist/types/excalidraw/components/ai-chat/agentLoop.d.ts +13 -0
  44. package/dist/types/excalidraw/components/ai-chat/audioUtils.d.ts +10 -0
  45. package/dist/types/excalidraw/index.d.ts +4 -1
  46. package/dist/types/excalidraw/types.d.ts +14 -0
  47. package/dist/types/excalidraw/utils/openRouterApiKey.d.ts +1 -0
  48. package/package.json +1 -1
  49. package/dist/prod/data/image-PKF4YK4A.js +0 -1
  50. /package/dist/dev/{chunk-YOQUQ6O5.js.map → chunk-3FGOYDLK.js.map} +0 -0
  51. /package/dist/dev/{chunk-EP27W34X.js.map → chunk-IRIUFXMO.js.map} +0 -0
  52. /package/dist/dev/data/{image-IZS5VEYX.js.map → image-HH4XNQRO.js.map} +0 -0
package/dist/dev/index.js CHANGED
@@ -66,10 +66,10 @@ import {
66
66
  serializeAsJSON,
67
67
  serializeLibraryAsJSON,
68
68
  strokeRectWithRotation_simple
69
- } from "./chunk-YOQUQ6O5.js";
69
+ } from "./chunk-3FGOYDLK.js";
70
70
  import {
71
71
  define_import_meta_env_default
72
- } from "./chunk-EP27W34X.js";
72
+ } from "./chunk-IRIUFXMO.js";
73
73
  import {
74
74
  en_default
75
75
  } from "./chunk-IFMURN5W.js";
@@ -158,7 +158,7 @@ import {
158
158
  isWritableElement as isWritableElement5,
159
159
  sceneCoordsToViewportCoords as sceneCoordsToViewportCoords10,
160
160
  tupleToCoors,
161
- viewportCoordsToSceneCoords as viewportCoordsToSceneCoords3,
161
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords4,
162
162
  wrapEvent as wrapEvent2,
163
163
  updateObject as updateObject2,
164
164
  updateActiveTool as updateActiveTool7,
@@ -197,7 +197,7 @@ import {
197
197
  import {
198
198
  getObservedAppState,
199
199
  getCommonBounds as getCommonBounds11,
200
- getElementAbsoluteCoords as getElementAbsoluteCoords11,
200
+ getElementAbsoluteCoords as getElementAbsoluteCoords12,
201
201
  bindOrUnbindBindingElements as bindOrUnbindBindingElements2,
202
202
  fixBindingsAfterDeletion as fixBindingsAfterDeletion2,
203
203
  getHoveredElementForBinding as getHoveredElementForBinding2,
@@ -7687,9 +7687,14 @@ import {
7687
7687
  ZOOM_STEP,
7688
7688
  updateActiveTool as updateActiveTool2,
7689
7689
  CODES as CODES2,
7690
- KEYS as KEYS12
7690
+ KEYS as KEYS12,
7691
+ viewportCoordsToSceneCoords
7691
7692
  } from "@orangecatai/common";
7692
- import { getNonDeletedElements as getNonDeletedElements5 } from "@orangecatai/element";
7693
+ import {
7694
+ getElementAbsoluteCoords,
7695
+ getNonDeletedElements as getNonDeletedElements5,
7696
+ getVisibleElements
7697
+ } from "@orangecatai/element";
7693
7698
  import { newElementWith as newElementWith3 } from "@orangecatai/element";
7694
7699
  import { getCommonBounds } from "@orangecatai/element";
7695
7700
  import { CaptureUpdateAction as CaptureUpdateAction6 } from "@orangecatai/element";
@@ -8070,6 +8075,77 @@ var zoomValueToFitBoundsOnViewport = (bounds, viewportDimensions, viewportZoomFa
8070
8075
  const adjustedZoomValue = smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
8071
8076
  return Math.min(adjustedZoomValue, 1);
8072
8077
  };
8078
+ var COMFORTABLE_VIEWPORT_ZOOM_FACTOR = 0.8;
8079
+ var intersectsBounds = (a, b) => a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
8080
+ var expandBounds = (bounds, margin) => [
8081
+ bounds[0] - margin,
8082
+ bounds[1] - margin,
8083
+ bounds[2] + margin,
8084
+ bounds[3] + margin
8085
+ ];
8086
+ var getBoundsCenterDistance = (bounds, point) => {
8087
+ const centerX = (bounds[0] + bounds[2]) / 2;
8088
+ const centerY = (bounds[1] + bounds[3]) / 2;
8089
+ return Math.hypot(centerX - point.x, centerY - point.y);
8090
+ };
8091
+ var toSceneBounds = (bounds) => [bounds[0], bounds[1], bounds[2], bounds[3]];
8092
+ var getNearestContentCluster = (elements, appState) => {
8093
+ const nonDeletedElements = getNonDeletedElements5(elements);
8094
+ if (!nonDeletedElements.length) {
8095
+ return [];
8096
+ }
8097
+ const candidateElements = getVisibleElements(nonDeletedElements);
8098
+ const clusterCandidates = candidateElements.length ? candidateElements : nonDeletedElements;
8099
+ const elementsMap = new Map(
8100
+ nonDeletedElements.map((element) => [element.id, element])
8101
+ );
8102
+ const viewportCenter = viewportCoordsToSceneCoords(
8103
+ {
8104
+ clientX: appState.offsetLeft + appState.width / 2,
8105
+ clientY: appState.offsetTop + appState.height / 2
8106
+ },
8107
+ appState
8108
+ );
8109
+ const anchor = clusterCandidates.reduce((closest, element) => {
8110
+ const bounds = toSceneBounds(
8111
+ getElementAbsoluteCoords(element, elementsMap)
8112
+ );
8113
+ if (!closest) {
8114
+ return { element, bounds };
8115
+ }
8116
+ return getBoundsCenterDistance(bounds, viewportCenter) < getBoundsCenterDistance(closest.bounds, viewportCenter) ? { element, bounds } : closest;
8117
+ }, null);
8118
+ if (!anchor) {
8119
+ return [];
8120
+ }
8121
+ const proximityMargin = Math.max(
8122
+ 160 / appState.zoom.value,
8123
+ 0.12 * Math.min(appState.width, appState.height) / appState.zoom.value
8124
+ );
8125
+ const cluster = /* @__PURE__ */ new Map([
8126
+ [anchor.element.id, anchor.element]
8127
+ ]);
8128
+ let clusterBounds = anchor.bounds;
8129
+ let changed = true;
8130
+ while (changed) {
8131
+ changed = false;
8132
+ const expandedBounds = expandBounds(clusterBounds, proximityMargin);
8133
+ for (const element of clusterCandidates) {
8134
+ if (cluster.has(element.id)) {
8135
+ continue;
8136
+ }
8137
+ const elementBounds = toSceneBounds(
8138
+ getElementAbsoluteCoords(element, elementsMap)
8139
+ );
8140
+ if (intersectsBounds(elementBounds, expandedBounds)) {
8141
+ cluster.set(element.id, element);
8142
+ clusterBounds = getCommonBounds([...cluster.values()]);
8143
+ changed = true;
8144
+ }
8145
+ }
8146
+ }
8147
+ return [...cluster.values()];
8148
+ };
8073
8149
  var zoomToFitBounds = ({
8074
8150
  bounds,
8075
8151
  appState,
@@ -8207,6 +8283,50 @@ var actionZoomToFit = register({
8207
8283
  }),
8208
8284
  keyTest: (event) => event.code === CODES2.ONE && event.shiftKey && !event.altKey && !event[KEYS12.CTRL_OR_CMD]
8209
8285
  });
8286
+ var actionScrollBackToContent = register({
8287
+ name: "scrollBackToContent",
8288
+ label: "buttons.scrollBackToContent",
8289
+ icon: zoomAreaIcon,
8290
+ viewMode: true,
8291
+ trackEvent: { category: "canvas" },
8292
+ perform: (elements, appState, _, app) => {
8293
+ const selectedElements = app.scene.getSelectedElements(appState);
8294
+ const targetElements = selectedElements.length ? selectedElements : getNearestContentCluster(elements, appState);
8295
+ if (!targetElements.length) {
8296
+ return false;
8297
+ }
8298
+ return zoomToFit({
8299
+ targetElements,
8300
+ appState: {
8301
+ ...appState,
8302
+ userToFollow: null
8303
+ },
8304
+ fitToViewport: true,
8305
+ viewportZoomFactor: COMFORTABLE_VIEWPORT_ZOOM_FACTOR,
8306
+ canvasOffsets: app.getEditorUIOffsets()
8307
+ });
8308
+ },
8309
+ PanelComponent: ({ updateData }) => /* @__PURE__ */ jsx36(
8310
+ Tooltip,
8311
+ {
8312
+ label: t("buttons.scrollBackToContent"),
8313
+ style: { height: "100%" },
8314
+ children: /* @__PURE__ */ jsx36(
8315
+ ToolButton,
8316
+ {
8317
+ type: "button",
8318
+ className: "scroll-back-to-content-button zoom-button",
8319
+ icon: zoomAreaIcon,
8320
+ title: t("buttons.scrollBackToContent"),
8321
+ "aria-label": t("buttons.scrollBackToContent"),
8322
+ onClick: () => {
8323
+ updateData(null);
8324
+ }
8325
+ }
8326
+ )
8327
+ }
8328
+ )
8329
+ });
8210
8330
  var actionToggleTheme = register({
8211
8331
  name: "toggleTheme",
8212
8332
  label: (_, appState) => {
@@ -9626,7 +9746,7 @@ var exportCanvas = async (type, elements, appState, files, {
9626
9746
  let blob = canvasToBlob(tempCanvas);
9627
9747
  if (appState.exportEmbedScene) {
9628
9748
  blob = blob.then(
9629
- (blob2) => import("./data/image-IZS5VEYX.js").then(
9749
+ (blob2) => import("./data/image-HH4XNQRO.js").then(
9630
9750
  ({ encodePngMetadata: encodePngMetadata2 }) => encodePngMetadata2({
9631
9751
  blob: blob2,
9632
9752
  metadata: serializeAsJSON(elements, appState, files, "local")
@@ -11863,13 +11983,13 @@ import {
11863
11983
  useState as useState10
11864
11984
  } from "react";
11865
11985
  import { EVENT as EVENT5, HYPERLINK_TOOLTIP_DELAY, KEYS as KEYS27 } from "@orangecatai/common";
11866
- import { getElementAbsoluteCoords } from "@orangecatai/element";
11986
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords2 } from "@orangecatai/element";
11867
11987
  import { hitElementBoundingBox } from "@orangecatai/element";
11868
11988
  import { isElementLink } from "@orangecatai/element";
11869
11989
  import { getEmbedLink, embeddableURLValidator } from "@orangecatai/element";
11870
11990
  import {
11871
11991
  sceneCoordsToViewportCoords,
11872
- viewportCoordsToSceneCoords,
11992
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords2,
11873
11993
  wrapEvent,
11874
11994
  isLocalLink,
11875
11995
  normalizeLink
@@ -12122,7 +12242,7 @@ var Hyperlink = ({
12122
12242
  );
12123
12243
  };
12124
12244
  var getCoordsForPopover = (element, appState, elementsMap) => {
12125
- const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
12245
+ const [x1, y1] = getElementAbsoluteCoords2(element, elementsMap);
12126
12246
  const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
12127
12247
  { sceneX: x1 + element.width / 2, sceneY: y1 },
12128
12248
  appState
@@ -12154,7 +12274,7 @@ var renderTooltip = (element, appState, elementsMap) => {
12154
12274
  tooltipDiv.classList.add("excalidraw-tooltip--visible");
12155
12275
  tooltipDiv.style.maxWidth = "20rem";
12156
12276
  tooltipDiv.textContent = isElementLink(element.link) ? t("labels.link.goToElement") : element.link;
12157
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
12277
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords2(element, elementsMap);
12158
12278
  const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
12159
12279
  [x1, y1, x2, y2],
12160
12280
  element.angle,
@@ -12187,7 +12307,7 @@ var hideHyperlinkToolip = () => {
12187
12307
  }
12188
12308
  };
12189
12309
  var shouldHideLinkPopup = (element, elementsMap, appState, [clientX, clientY]) => {
12190
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
12310
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords2(
12191
12311
  { clientX, clientY },
12192
12312
  appState
12193
12313
  );
@@ -12195,7 +12315,7 @@ var shouldHideLinkPopup = (element, elementsMap, appState, [clientX, clientY]) =
12195
12315
  if (hitElementBoundingBox(pointFrom4(sceneX, sceneY), element, elementsMap)) {
12196
12316
  return false;
12197
12317
  }
12198
- const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
12318
+ const [x1, y1, x2] = getElementAbsoluteCoords2(element, elementsMap);
12199
12319
  if (sceneX >= x1 && sceneX <= x2 && sceneY >= y1 - SPACE_BOTTOM && sceneY <= y1) {
12200
12320
  return false;
12201
12321
  }
@@ -12444,7 +12564,7 @@ import {
12444
12564
  } from "@orangecatai/element";
12445
12565
  import {
12446
12566
  getCommonBoundingBox as getCommonBoundingBox2,
12447
- getElementAbsoluteCoords as getElementAbsoluteCoords2
12567
+ getElementAbsoluteCoords as getElementAbsoluteCoords3
12448
12568
  } from "@orangecatai/element";
12449
12569
  import {
12450
12570
  getBoundTextElement as getBoundTextElement4,
@@ -12547,7 +12667,7 @@ var Panel = ({
12547
12667
  positionRef.current = newPositionRef;
12548
12668
  let bottomLeft;
12549
12669
  if (elements2.length === 1) {
12550
- const [x1, , , y2, cx, cy] = getElementAbsoluteCoords2(
12670
+ const [x1, , , y2, cx, cy] = getElementAbsoluteCoords3(
12551
12671
  elements2[0],
12552
12672
  app.scene.getNonDeletedElementsMap()
12553
12673
  );
@@ -17769,14 +17889,17 @@ var ShapesSwitcher = ({
17769
17889
  ] })
17770
17890
  ] });
17771
17891
  };
17892
+ var ScrollBackToContentAction = ({
17893
+ renderAction
17894
+ }) => renderAction("scrollBackToContent");
17772
17895
  var ZoomActions = ({
17773
17896
  renderAction,
17774
17897
  zoom
17775
- }) => /* @__PURE__ */ jsx75(Stack_default.Col, { gap: 1, className: CLASSES5.ZOOM_ACTIONS, children: /* @__PURE__ */ jsxs42(Stack_default.Row, { align: "center", children: [
17898
+ }) => /* @__PURE__ */ jsxs42(Stack_default.Row, { align: "center", className: CLASSES5.ZOOM_ACTIONS, children: [
17776
17899
  renderAction("zoomOut"),
17777
17900
  renderAction("resetZoom"),
17778
17901
  renderAction("zoomIn")
17779
- ] }) });
17902
+ ] });
17780
17903
  var UndoRedoActions = ({
17781
17904
  renderAction,
17782
17905
  className
@@ -18679,7 +18802,7 @@ import {
18679
18802
  vectorFromPoint,
18680
18803
  vectorScale
18681
18804
  } from "@orangecatai/math";
18682
- import { getElementAbsoluteCoords as getElementAbsoluteCoords3 } from "@orangecatai/element";
18805
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords4 } from "@orangecatai/element";
18683
18806
  var getCurvePathOps = (shape) => {
18684
18807
  if (!shape) {
18685
18808
  return [];
@@ -19011,7 +19134,7 @@ var FOCUS_POINT_SIZE = 10 / 1.5;
19011
19134
  // ../element/src/sizeHelpers.ts
19012
19135
  import {
19013
19136
  SHIFT_LOCKING_ANGLE,
19014
- viewportCoordsToSceneCoords as viewportCoordsToSceneCoords2
19137
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords3
19015
19138
  } from "@orangecatai/common";
19016
19139
  import {
19017
19140
  normalizeRadians,
@@ -20988,7 +21111,7 @@ import { TOOL_TYPE, KEYS as KEYS43 } from "@orangecatai/common";
20988
21111
  import {
20989
21112
  getCommonBounds as getCommonBounds4,
20990
21113
  getDraggedElementsBounds,
20991
- getElementAbsoluteCoords as getElementAbsoluteCoords5
21114
+ getElementAbsoluteCoords as getElementAbsoluteCoords6
20992
21115
  } from "@orangecatai/element";
20993
21116
  import { isBoundToContainer as isBoundToContainer6 } from "@orangecatai/element";
20994
21117
  import { getMaximumGroups } from "@orangecatai/element";
@@ -21051,7 +21174,7 @@ var getElementsCorners = (elements, elementsMap, {
21051
21174
  let result = [];
21052
21175
  if (elements.length === 1) {
21053
21176
  const element = elements[0];
21054
- let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords5(
21177
+ let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords6(
21055
21178
  element,
21056
21179
  elementsMap
21057
21180
  );
@@ -22051,11 +22174,11 @@ var Renderer = class {
22051
22174
 
22052
22175
  // components/ElementCanvasButtons.tsx
22053
22176
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords3 } from "@orangecatai/common";
22054
- import { getElementAbsoluteCoords as getElementAbsoluteCoords6 } from "@orangecatai/element";
22177
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords7 } from "@orangecatai/element";
22055
22178
  import { jsx as jsx82 } from "react/jsx-runtime";
22056
22179
  var CONTAINER_PADDING = 5;
22057
22180
  var getContainerCoords2 = (element, appState, elementsMap) => {
22058
- const [x1, y1] = getElementAbsoluteCoords6(element, elementsMap);
22181
+ const [x1, y1] = getElementAbsoluteCoords7(element, elementsMap);
22059
22182
  const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords3(
22060
22183
  { sceneX: x1 + element.width, sceneY: y1 },
22061
22184
  appState
@@ -22092,7 +22215,7 @@ var ElementCanvasButtons = ({
22092
22215
  // components/FrameToolbar.tsx
22093
22216
  import { useCallback as useCallback14, useEffect as useEffect29, useRef as useRef24, useState as useState27 } from "react";
22094
22217
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords4 } from "@orangecatai/common";
22095
- import { getElementAbsoluteCoords as getElementAbsoluteCoords7 } from "@orangecatai/element";
22218
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords8 } from "@orangecatai/element";
22096
22219
  import { isFrameLikeElement as isFrameLikeElement9 } from "@orangecatai/element";
22097
22220
  import { Fragment as Fragment11, jsx as jsx83, jsxs as jsxs45 } from "react/jsx-runtime";
22098
22221
  var ASPECT_RATIOS = [
@@ -22289,7 +22412,7 @@ var FrameToolbar = ({
22289
22412
  Math.round(element.width),
22290
22413
  Math.round(element.height)
22291
22414
  );
22292
- const [x1, y1] = getElementAbsoluteCoords7(
22415
+ const [x1, y1] = getElementAbsoluteCoords8(
22293
22416
  element,
22294
22417
  app.scene.getNonDeletedElementsMap()
22295
22418
  );
@@ -22482,13 +22605,14 @@ import { useCallback as useCallback15, useEffect as useEffect30, useRef as useRe
22482
22605
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords5 } from "@orangecatai/common";
22483
22606
  import {
22484
22607
  CaptureUpdateAction as CaptureUpdateAction36,
22485
- getElementAbsoluteCoords as getElementAbsoluteCoords8
22608
+ getElementAbsoluteCoords as getElementAbsoluteCoords9
22486
22609
  } from "@orangecatai/element";
22487
22610
  import { ArrowUp, ImagePlus, X } from "lucide-react";
22488
22611
 
22489
22612
  // utils/geminiApiKey.ts
22490
22613
  function resolveGeminiApiKey(propKey) {
22491
- return propKey ?? (typeof import.meta !== "undefined" && define_import_meta_env_default?.VITE_APP_GEMINI_API_KEY ? define_import_meta_env_default.VITE_APP_GEMINI_API_KEY : "") ?? "";
22614
+ const normalizedPropKey = propKey?.trim();
22615
+ return (normalizedPropKey ? normalizedPropKey : void 0) ?? (typeof import.meta !== "undefined" && define_import_meta_env_default?.VITE_APP_GEMINI_API_KEY ? define_import_meta_env_default.VITE_APP_GEMINI_API_KEY : "") ?? "";
22492
22616
  }
22493
22617
 
22494
22618
  // components/image-generation/pendingGenerations.ts
@@ -23144,14 +23268,28 @@ var RatioShapeIcon = ({ label }) => {
23144
23268
  };
23145
23269
  var ImageGeneratorPanel = ({
23146
23270
  element,
23147
- app
23271
+ app,
23272
+ onBeforeImageGen,
23273
+ onAfterImageGen
23148
23274
  }) => {
23275
+ const getStoredSettings = (frame) => {
23276
+ const stored = frame.customData?.imageGenerator;
23277
+ if (!stored) {
23278
+ return null;
23279
+ }
23280
+ return {
23281
+ model: stored.model,
23282
+ ratio: stored.ratio,
23283
+ resolution: stored.resolution
23284
+ };
23285
+ };
23149
23286
  const appState = app.state;
23150
23287
  const resolvedApiKey = resolveGeminiApiKey(app.props.geminiApiKey);
23151
23288
  const [prompt, setPrompt] = useState29(() => getPendingPrompt(element.id));
23152
23289
  const [selectedModel, setSelectedModel] = useState29(() => {
23153
- const s = getPendingSettings(element.id);
23154
- return s?.model ?? "Gemini 3.1 Flash";
23290
+ const stored = getStoredSettings(element);
23291
+ const pending = getPendingSettings(element.id);
23292
+ return stored?.model ?? pending?.model ?? "Gemini 3.1 Flash";
23155
23293
  });
23156
23294
  const modelCfg = MODEL_CONFIG[selectedModel];
23157
23295
  const availableRatios = modelCfg.supportedRatios;
@@ -23159,15 +23297,39 @@ var ImageGeneratorPanel = ({
23159
23297
  const defaultRatio = availableRatios.includes("2:3") ? "2:3" : availableRatios[0];
23160
23298
  const defaultResolution = availableResolutions.includes("2K") ? "2K" : availableResolutions[availableResolutions.length - 1];
23161
23299
  const [selectedRatio, setSelectedRatio] = useState29(() => {
23162
- const s = getPendingSettings(element.id);
23163
- return s?.ratio ?? defaultRatio;
23300
+ const stored = getStoredSettings(element);
23301
+ const pending = getPendingSettings(element.id);
23302
+ return stored?.ratio ?? pending?.ratio ?? defaultRatio;
23164
23303
  });
23165
23304
  const [selectedResolution, setSelectedResolution] = useState29(
23166
23305
  () => {
23167
- const s = getPendingSettings(element.id);
23168
- return s?.resolution ?? defaultResolution;
23306
+ const stored = getStoredSettings(element);
23307
+ const pending = getPendingSettings(element.id);
23308
+ return stored?.resolution ?? pending?.resolution ?? defaultResolution;
23169
23309
  }
23170
23310
  );
23311
+ const persistFrameSettings = useCallback15(
23312
+ (settings) => {
23313
+ app.scene.mutateElement(element, {
23314
+ customData: {
23315
+ ...element.customData ?? {},
23316
+ imageGenerator: settings
23317
+ }
23318
+ });
23319
+ },
23320
+ [app.scene, element]
23321
+ );
23322
+ const fitFrameIntoView = useCallback15(
23323
+ (frame, animate = false) => {
23324
+ app.scrollToContent(frame, {
23325
+ fitToViewport: true,
23326
+ viewportZoomFactor: 0.8,
23327
+ animate,
23328
+ canvasOffsets: app.getEditorUIOffsets()
23329
+ });
23330
+ },
23331
+ [app]
23332
+ );
23171
23333
  const isInitialModelMount = useRef26(true);
23172
23334
  useEffect30(() => {
23173
23335
  if (isInitialModelMount.current) {
@@ -23196,8 +23358,13 @@ var ImageGeneratorPanel = ({
23196
23358
  width: dims.width,
23197
23359
  height: dims.height
23198
23360
  });
23361
+ persistFrameSettings({
23362
+ model: selectedModel,
23363
+ ratio: newRatio,
23364
+ resolution: newRes
23365
+ });
23199
23366
  app.syncActionResult({ captureUpdate: CaptureUpdateAction36.IMMEDIATELY });
23200
- }, [selectedModel]);
23367
+ }, [selectedModel, app, element, persistFrameSettings]);
23201
23368
  const [isGenerating, setIsGenerating] = useState29(
23202
23369
  () => hasPendingGeneration(element.id)
23203
23370
  );
@@ -23217,6 +23384,32 @@ var ImageGeneratorPanel = ({
23217
23384
  elementRef.current = element;
23218
23385
  appRef.current = app;
23219
23386
  });
23387
+ useEffect30(() => {
23388
+ if (!element.customData?.imageGeneratorAutoFitPending) {
23389
+ return;
23390
+ }
23391
+ let frameId = 0;
23392
+ let cancelled = false;
23393
+ const run = () => {
23394
+ if (cancelled) {
23395
+ return;
23396
+ }
23397
+ fitFrameIntoView(element);
23398
+ app.scene.mutateElement(element, {
23399
+ customData: {
23400
+ ...element.customData ?? {},
23401
+ imageGeneratorAutoFitPending: false
23402
+ }
23403
+ });
23404
+ };
23405
+ frameId = window.requestAnimationFrame(() => {
23406
+ frameId = window.requestAnimationFrame(run);
23407
+ });
23408
+ return () => {
23409
+ cancelled = true;
23410
+ window.cancelAnimationFrame(frameId);
23411
+ };
23412
+ }, [app.scene, element, fitFrameIntoView]);
23220
23413
  const applyRatio = useCallback15(
23221
23414
  (ratioLabel) => {
23222
23415
  setSelectedRatio(ratioLabel);
@@ -23224,7 +23417,15 @@ var ImageGeneratorPanel = ({
23224
23417
  const dims = getDimensions(selectedModel, ratioLabel, selectedResolution);
23225
23418
  appRef.current.scene.mutateElement(elementRef.current, {
23226
23419
  width: dims.width,
23227
- height: dims.height
23420
+ height: dims.height,
23421
+ customData: {
23422
+ ...elementRef.current.customData ?? {},
23423
+ imageGenerator: {
23424
+ model: selectedModel,
23425
+ ratio: ratioLabel,
23426
+ resolution: selectedResolution
23427
+ }
23428
+ }
23228
23429
  });
23229
23430
  appRef.current.syncActionResult({
23230
23431
  captureUpdate: CaptureUpdateAction36.IMMEDIATELY
@@ -23239,7 +23440,15 @@ var ImageGeneratorPanel = ({
23239
23440
  const dims = getDimensions(selectedModel, selectedRatio, res);
23240
23441
  appRef.current.scene.mutateElement(elementRef.current, {
23241
23442
  width: dims.width,
23242
- height: dims.height
23443
+ height: dims.height,
23444
+ customData: {
23445
+ ...elementRef.current.customData ?? {},
23446
+ imageGenerator: {
23447
+ model: selectedModel,
23448
+ ratio: selectedRatio,
23449
+ resolution: res
23450
+ }
23451
+ }
23243
23452
  });
23244
23453
  appRef.current.syncActionResult({
23245
23454
  captureUpdate: CaptureUpdateAction36.IMMEDIATELY
@@ -23301,6 +23510,20 @@ var ImageGeneratorPanel = ({
23301
23510
  );
23302
23511
  return;
23303
23512
  }
23513
+ if (onBeforeImageGen) {
23514
+ try {
23515
+ const { allowed, error } = await onBeforeImageGen();
23516
+ if (!allowed) {
23517
+ setGenerateError(
23518
+ error || "Insufficient credits for image generation"
23519
+ );
23520
+ return;
23521
+ }
23522
+ } catch {
23523
+ setGenerateError("Credit check failed");
23524
+ return;
23525
+ }
23526
+ }
23304
23527
  const controller = new AbortController();
23305
23528
  abortControllerRef.current = controller;
23306
23529
  startPendingGeneration(
@@ -23330,6 +23553,7 @@ var ImageGeneratorPanel = ({
23330
23553
  controller.signal
23331
23554
  );
23332
23555
  await app.insertGeneratedImageIntoFrame(imageDataUrl, element);
23556
+ onAfterImageGen?.();
23333
23557
  } catch (err) {
23334
23558
  if (err instanceof Error) {
23335
23559
  if (err.name === "AbortError") {
@@ -23359,12 +23583,14 @@ var ImageGeneratorPanel = ({
23359
23583
  selectedResolution,
23360
23584
  referenceImage,
23361
23585
  app,
23362
- element
23586
+ element,
23587
+ onBeforeImageGen,
23588
+ onAfterImageGen
23363
23589
  ]);
23364
23590
  if (appState.contextMenu || appState.newElement || appState.resizingElement || appState.isRotating || appState.openMenu || appState.viewModeEnabled || appState.selectedElementsAreBeingDragged) {
23365
23591
  return null;
23366
23592
  }
23367
- const [x1, , , y2] = getElementAbsoluteCoords8(
23593
+ const [x1, , , y2] = getElementAbsoluteCoords9(
23368
23594
  element,
23369
23595
  app.scene.getNonDeletedElementsMap()
23370
23596
  );
@@ -23440,6 +23666,11 @@ var ImageGeneratorPanel = ({
23440
23666
  {
23441
23667
  className: "igp-dropdown-item",
23442
23668
  onClick: () => {
23669
+ persistFrameSettings({
23670
+ model: m,
23671
+ ratio: selectedRatio,
23672
+ resolution: selectedResolution
23673
+ });
23443
23674
  setSelectedModel(m);
23444
23675
  setModelOpen(false);
23445
23676
  },
@@ -23568,7 +23799,7 @@ var ImageGeneratorPanel = ({
23568
23799
  // components/ImageQuickEditPanel.tsx
23569
23800
  import { useCallback as useCallback16, useEffect as useEffect31, useRef as useRef27, useState as useState30 } from "react";
23570
23801
  import { sceneCoordsToViewportCoords as sceneCoordsToViewportCoords6 } from "@orangecatai/common";
23571
- import { getElementAbsoluteCoords as getElementAbsoluteCoords9 } from "@orangecatai/element";
23802
+ import { getElementAbsoluteCoords as getElementAbsoluteCoords10 } from "@orangecatai/element";
23572
23803
  import { ArrowUp as ArrowUp2 } from "lucide-react";
23573
23804
  import { Fragment as Fragment12, jsx as jsx88, jsxs as jsxs48 } from "react/jsx-runtime";
23574
23805
  var ChevronDownIcon3 = () => /* @__PURE__ */ jsx88("svg", { width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx88(
@@ -23671,7 +23902,9 @@ var RatioShapeIcon2 = ({ label }) => {
23671
23902
  };
23672
23903
  var ImageQuickEditPanel = ({
23673
23904
  element,
23674
- app
23905
+ app,
23906
+ onBeforeImageGen,
23907
+ onAfterImageGen
23675
23908
  }) => {
23676
23909
  const appState = app.state;
23677
23910
  const resolvedApiKey = resolveGeminiApiKey(app.props.geminiApiKey);
@@ -23770,6 +24003,20 @@ var ImageQuickEditPanel = ({
23770
24003
  );
23771
24004
  return;
23772
24005
  }
24006
+ if (onBeforeImageGen) {
24007
+ try {
24008
+ const { allowed, error } = await onBeforeImageGen();
24009
+ if (!allowed) {
24010
+ setGenerateError(
24011
+ error || "Insufficient credits for image generation"
24012
+ );
24013
+ return;
24014
+ }
24015
+ } catch {
24016
+ setGenerateError("Credit check failed");
24017
+ return;
24018
+ }
24019
+ }
23773
24020
  const controller = new AbortController();
23774
24021
  abortControllerRef.current = controller;
23775
24022
  startPendingGeneration(
@@ -23810,6 +24057,7 @@ var ImageQuickEditPanel = ({
23810
24057
  dims.width,
23811
24058
  dims.height
23812
24059
  );
24060
+ onAfterImageGen?.();
23813
24061
  } catch (err) {
23814
24062
  if (err instanceof Error) {
23815
24063
  if (err.name === "AbortError") {
@@ -23838,13 +24086,15 @@ var ImageQuickEditPanel = ({
23838
24086
  selectedRatio,
23839
24087
  selectedResolution,
23840
24088
  app,
23841
- element
24089
+ element,
24090
+ onBeforeImageGen,
24091
+ onAfterImageGen
23842
24092
  ]);
23843
24093
  if (appState.contextMenu || appState.newElement || appState.resizingElement || appState.isRotating || appState.openMenu || appState.viewModeEnabled || appState.selectedElementsAreBeingDragged) {
23844
24094
  return null;
23845
24095
  }
23846
24096
  const elementsMap = app.scene.getNonDeletedElementsMap();
23847
- const [x1, y1, , y2] = getElementAbsoluteCoords9(element, elementsMap);
24097
+ const [x1, y1, , y2] = getElementAbsoluteCoords10(element, elementsMap);
23848
24098
  const { x: triggerVpX, y: triggerVpY } = sceneCoordsToViewportCoords6(
23849
24099
  { sceneX: x1 + element.width / 2, sceneY: y1 },
23850
24100
  appState
@@ -26767,24 +27017,40 @@ var Footer = ({
26767
27017
  className: clsx47("layer-ui__wrapper__footer-left zen-mode-transition", {
26768
27018
  "layer-ui__wrapper__footer-left--transition-left": appState.zenModeEnabled
26769
27019
  }),
26770
- children: /* @__PURE__ */ jsx104(Stack_default.Col, { gap: 2, children: /* @__PURE__ */ jsxs60(Section, { heading: "canvasActions", children: [
27020
+ children: /* @__PURE__ */ jsx104(Stack_default.Col, { gap: 2, children: /* @__PURE__ */ jsx104(Section, { heading: "canvasActions", children: /* @__PURE__ */ jsxs60(Stack_default.Col, { gap: 1, className: "footer-canvas-actions", children: [
26771
27021
  /* @__PURE__ */ jsx104(
26772
- ZoomActions,
27022
+ ScrollBackToContentAction,
26773
27023
  {
26774
- renderAction: actionManager.renderAction,
26775
- zoom: appState.zoom
27024
+ renderAction: actionManager.renderAction
26776
27025
  }
26777
27026
  ),
26778
- !appState.viewModeEnabled && /* @__PURE__ */ jsx104(
26779
- UndoRedoActions,
27027
+ /* @__PURE__ */ jsxs60(
27028
+ Stack_default.Row,
26780
27029
  {
26781
- renderAction: actionManager.renderAction,
26782
- className: clsx47("zen-mode-transition", {
26783
- "layer-ui__wrapper__footer-left--transition-bottom": appState.zenModeEnabled
26784
- })
27030
+ align: "center",
27031
+ gap: 1,
27032
+ className: "footer-canvas-actions-row",
27033
+ children: [
27034
+ /* @__PURE__ */ jsx104(
27035
+ ZoomActions,
27036
+ {
27037
+ renderAction: actionManager.renderAction,
27038
+ zoom: appState.zoom
27039
+ }
27040
+ ),
27041
+ !appState.viewModeEnabled && /* @__PURE__ */ jsx104(
27042
+ UndoRedoActions,
27043
+ {
27044
+ renderAction: actionManager.renderAction,
27045
+ className: clsx47("zen-mode-transition", {
27046
+ "layer-ui__wrapper__footer-left--transition-bottom": appState.zenModeEnabled
27047
+ })
27048
+ }
27049
+ )
27050
+ ]
26785
27051
  }
26786
27052
  )
26787
- ] }) })
27053
+ ] }) }) })
26788
27054
  }
26789
27055
  ),
26790
27056
  /* @__PURE__ */ jsx104(FooterCenterTunnel.Out, {}),
@@ -35050,7 +35316,7 @@ import {
35050
35316
  } from "@orangecatai/element";
35051
35317
  import {
35052
35318
  getCommonBounds as getCommonBounds9,
35053
- getElementAbsoluteCoords as getElementAbsoluteCoords10
35319
+ getElementAbsoluteCoords as getElementAbsoluteCoords11
35054
35320
  } from "@orangecatai/element";
35055
35321
  import {
35056
35322
  getGlobalFixedPointForBindableElement as getGlobalFixedPointForBindableElement2,
@@ -35837,7 +36103,7 @@ var renderSelectionBorder = (context, appState, elementProperties) => {
35837
36103
  context.restore();
35838
36104
  };
35839
36105
  var renderFrameHighlight = (context, appState, frame, elementsMap) => {
35840
- const [x1, y1, x2, y2] = getElementAbsoluteCoords10(frame, elementsMap);
36106
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords11(frame, elementsMap);
35841
36107
  const width = x2 - x1;
35842
36108
  const height = y2 - y1;
35843
36109
  context.strokeStyle = "rgb(0,118,255)";
@@ -36098,7 +36364,7 @@ var renderTransformHandles = (context, renderConfig, appState, transformHandles,
36098
36364
  });
36099
36365
  };
36100
36366
  var renderCropHandles = (context, renderConfig, appState, croppingElement, elementsMap) => {
36101
- const [x1, y1, , , cx, cy] = getElementAbsoluteCoords10(
36367
+ const [x1, y1, , , cx, cy] = getElementAbsoluteCoords11(
36102
36368
  croppingElement,
36103
36369
  elementsMap
36104
36370
  );
@@ -36379,7 +36645,7 @@ var _renderInteractiveScene = ({
36379
36645
  }
36380
36646
  }
36381
36647
  if (selectionColors.length) {
36382
- const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords10(
36648
+ const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords11(
36383
36649
  element,
36384
36650
  elementsMap,
36385
36651
  true
@@ -36506,7 +36772,7 @@ var _renderInteractiveScene = ({
36506
36772
  appState.searchMatches?.matches.forEach(({ id, focus, matchedLines }) => {
36507
36773
  const element = elementsMap.get(id);
36508
36774
  if (element) {
36509
- const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords10(
36775
+ const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords11(
36510
36776
  element,
36511
36777
  elementsMap,
36512
36778
  true
@@ -37275,11 +37541,11 @@ var App = class _App extends React52.Component {
37275
37541
  );
37276
37542
  if (frameNameDiv) {
37277
37543
  const box = frameNameDiv.getBoundingClientRect();
37278
- const boxSceneTopLeft = viewportCoordsToSceneCoords3(
37544
+ const boxSceneTopLeft = viewportCoordsToSceneCoords4(
37279
37545
  { clientX: box.x, clientY: box.y },
37280
37546
  this.state
37281
37547
  );
37282
- const boxSceneBottomRight = viewportCoordsToSceneCoords3(
37548
+ const boxSceneBottomRight = viewportCoordsToSceneCoords4(
37283
37549
  { clientX: box.right, clientY: box.bottom },
37284
37550
  this.state
37285
37551
  );
@@ -37569,7 +37835,10 @@ var App = class _App extends React52.Component {
37569
37835
  locked: false,
37570
37836
  backgroundColor: "#cce8f5",
37571
37837
  fillStyle: "solid",
37572
- customData: { type: "image-generator" }
37838
+ customData: {
37839
+ type: "image-generator",
37840
+ imageGeneratorAutoFitPending: true
37841
+ }
37573
37842
  });
37574
37843
  this.scene.insertElement(frame);
37575
37844
  this.setActiveTool({ type: "selection" });
@@ -37614,7 +37883,7 @@ var App = class _App extends React52.Component {
37614
37883
  const CLEARANCE = 25;
37615
37884
  const elementsMap = this.scene.getNonDeletedElementsMap();
37616
37885
  const allElements = this.scene.getNonDeletedElements().filter((el) => el.id !== sourceElement.id);
37617
- const [sx1, sy1, sx2, sy2] = getElementAbsoluteCoords11(
37886
+ const [sx1, sy1, sx2, sy2] = getElementAbsoluteCoords12(
37618
37887
  sourceElement,
37619
37888
  elementsMap
37620
37889
  );
@@ -37632,7 +37901,7 @@ var App = class _App extends React52.Component {
37632
37901
  const right = cRight + CLEARANCE;
37633
37902
  const bottom = cBottom + CLEARANCE;
37634
37903
  return allElements.some((el) => {
37635
- const [ex1, ey1, ex2, ey2] = getElementAbsoluteCoords11(el, elementsMap);
37904
+ const [ex1, ey1, ex2, ey2] = getElementAbsoluteCoords12(el, elementsMap);
37636
37905
  return ex1 < right && ex2 > left && ey1 < bottom && ey2 > top;
37637
37906
  });
37638
37907
  };
@@ -38118,7 +38387,7 @@ var App = class _App extends React52.Component {
38118
38387
  const elementsCenterY = distance2(minY, maxY) / 2;
38119
38388
  const clientX = typeof opts.position === "object" ? opts.position.clientX : opts.position === "cursor" ? this.lastViewportPosition.x : this.state.width / 2 + this.state.offsetLeft;
38120
38389
  const clientY = typeof opts.position === "object" ? opts.position.clientY : opts.position === "cursor" ? this.lastViewportPosition.y : this.state.height / 2 + this.state.offsetTop;
38121
- const { x, y } = viewportCoordsToSceneCoords3(
38390
+ const { x, y } = viewportCoordsToSceneCoords4(
38122
38391
  { clientX, clientY },
38123
38392
  this.state
38124
38393
  );
@@ -38970,7 +39239,7 @@ var App = class _App extends React52.Component {
38970
39239
  bindMode: "orbit"
38971
39240
  });
38972
39241
  if (this.lastPointerMoveEvent && getFeatureFlag4("COMPLEX_BINDINGS")) {
38973
- const scenePointer = viewportCoordsToSceneCoords3(
39242
+ const scenePointer = viewportCoordsToSceneCoords4(
38974
39243
  {
38975
39244
  clientX: this.lastPointerMoveEvent.clientX,
38976
39245
  clientY: this.lastPointerMoveEvent.clientY
@@ -39371,7 +39640,7 @@ var App = class _App extends React52.Component {
39371
39640
  return;
39372
39641
  }
39373
39642
  const selectedElements = this.scene.getSelectedElements(this.state);
39374
- let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
39643
+ let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
39375
39644
  event,
39376
39645
  this.state
39377
39646
  );
@@ -39536,7 +39805,7 @@ var App = class _App extends React52.Component {
39536
39805
  if (!this.hitLinkElement || draggedDistance > DRAGGING_THRESHOLD3) {
39537
39806
  return;
39538
39807
  }
39539
- const lastPointerDownCoords = viewportCoordsToSceneCoords3(
39808
+ const lastPointerDownCoords = viewportCoordsToSceneCoords4(
39540
39809
  this.lastPointerDownEvent,
39541
39810
  this.state
39542
39811
  );
@@ -39548,7 +39817,7 @@ var App = class _App extends React52.Component {
39548
39817
  pointFrom29(lastPointerDownCoords.x, lastPointerDownCoords.y),
39549
39818
  this.editorInterface.formFactor === "phone"
39550
39819
  );
39551
- const lastPointerUpCoords = viewportCoordsToSceneCoords3(
39820
+ const lastPointerUpCoords = viewportCoordsToSceneCoords4(
39552
39821
  this.lastPointerUpEvent,
39553
39822
  this.state
39554
39823
  );
@@ -39596,7 +39865,7 @@ var App = class _App extends React52.Component {
39596
39865
  __publicField(this, "handleCanvasPointerMove", (event) => {
39597
39866
  this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
39598
39867
  this.lastPointerMoveEvent = event.nativeEvent;
39599
- const scenePointer = viewportCoordsToSceneCoords3(event, this.state);
39868
+ const scenePointer = viewportCoordsToSceneCoords4(event, this.state);
39600
39869
  const { x: scenePointerX, y: scenePointerY } = scenePointer;
39601
39870
  this.lastPointerMoveCoords = {
39602
39871
  x: scenePointerX,
@@ -40072,7 +40341,7 @@ var App = class _App extends React52.Component {
40072
40341
  invalidateContextMenu = true;
40073
40342
  });
40074
40343
  __publicField(this, "handleCanvasPointerDown", (event) => {
40075
- const scenePointer = viewportCoordsToSceneCoords3(event, this.state);
40344
+ const scenePointer = viewportCoordsToSceneCoords4(event, this.state);
40076
40345
  const { x: scenePointerX, y: scenePointerY } = scenePointer;
40077
40346
  this.lastPointerMoveCoords = {
40078
40347
  x: scenePointerX,
@@ -40364,7 +40633,7 @@ var App = class _App extends React52.Component {
40364
40633
  }
40365
40634
  this.removePointer(event);
40366
40635
  this.lastPointerUpEvent = event;
40367
- const scenePointer = viewportCoordsToSceneCoords3(
40636
+ const scenePointer = viewportCoordsToSceneCoords4(
40368
40637
  { clientX: event.clientX, clientY: event.clientY },
40369
40638
  this.state
40370
40639
  );
@@ -41444,7 +41713,7 @@ var App = class _App extends React52.Component {
41444
41713
  try {
41445
41714
  const clientX = this.state.width / 2 + this.state.offsetLeft;
41446
41715
  const clientY = this.state.height / 2 + this.state.offsetTop;
41447
- const { x, y } = viewportCoordsToSceneCoords3(
41716
+ const { x, y } = viewportCoordsToSceneCoords4(
41448
41717
  { clientX, clientY },
41449
41718
  this.state
41450
41719
  );
@@ -41619,7 +41888,7 @@ var App = class _App extends React52.Component {
41619
41888
  });
41620
41889
  });
41621
41890
  __publicField(this, "handleAppOnDrop", async (event) => {
41622
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
41891
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
41623
41892
  event,
41624
41893
  this.state
41625
41894
  );
@@ -41786,7 +42055,7 @@ var App = class _App extends React52.Component {
41786
42055
  event.button !== POINTER_BUTTON2.SECONDARY) && this.state.activeTool.type !== this.state.preferredSelectionTool.type) {
41787
42056
  return;
41788
42057
  }
41789
- const { x, y } = viewportCoordsToSceneCoords3(event, this.state);
42058
+ const { x, y } = viewportCoordsToSceneCoords4(event, this.state);
41790
42059
  const element = this.getElementAtPosition(x, y, {
41791
42060
  preferSelected: true,
41792
42061
  includeLockedElements: true
@@ -42206,7 +42475,7 @@ var App = class _App extends React52.Component {
42206
42475
  if (!x || !y) {
42207
42476
  return;
42208
42477
  }
42209
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
42478
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
42210
42479
  { clientX: x, clientY: y },
42211
42480
  this.state
42212
42481
  );
@@ -42631,7 +42900,7 @@ var App = class _App extends React52.Component {
42631
42900
  return false;
42632
42901
  }
42633
42902
  const viewportClickStart_scenePoint = pointFrom29(
42634
- viewportCoordsToSceneCoords3(
42903
+ viewportCoordsToSceneCoords4(
42635
42904
  {
42636
42905
  clientX: this.lastPointerDownEvent.clientX,
42637
42906
  clientY: this.lastPointerDownEvent.clientY
@@ -42640,7 +42909,7 @@ var App = class _App extends React52.Component {
42640
42909
  )
42641
42910
  );
42642
42911
  const viewportClickEnd_scenePoint = pointFrom29(
42643
- viewportCoordsToSceneCoords3(
42912
+ viewportCoordsToSceneCoords4(
42644
42913
  {
42645
42914
  clientX: this.lastPointerUpEvent.clientX,
42646
42915
  clientY: this.lastPointerUpEvent.clientY
@@ -43161,14 +43430,18 @@ var App = class _App extends React52.Component {
43161
43430
  ImageGeneratorPanel,
43162
43431
  {
43163
43432
  element: firstSelectedElement,
43164
- app: this
43433
+ app: this,
43434
+ onBeforeImageGen: this.props.onBeforeImageGen,
43435
+ onAfterImageGen: this.props.onAfterImageGen
43165
43436
  }
43166
43437
  ),
43167
43438
  selectedElements.length === 1 && isImageElement9(firstSelectedElement) && !this.state.viewModeEnabled && /* @__PURE__ */ jsx168(
43168
43439
  ImageQuickEditPanel,
43169
43440
  {
43170
43441
  element: firstSelectedElement,
43171
- app: this
43442
+ app: this,
43443
+ onBeforeImageGen: this.props.onBeforeImageGen,
43444
+ onAfterImageGen: this.props.onAfterImageGen
43172
43445
  }
43173
43446
  ),
43174
43447
  this.state.toast !== null && /* @__PURE__ */ jsx168(
@@ -43702,7 +43975,7 @@ var App = class _App extends React52.Component {
43702
43975
  }
43703
43976
  // TODO: Cover with tests
43704
43977
  async insertClipboardContent(data, dataTransferFiles, isPlainPaste) {
43705
- const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords3(
43978
+ const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords4(
43706
43979
  {
43707
43980
  clientX: this.lastViewportPosition.x,
43708
43981
  clientY: this.lastViewportPosition.y
@@ -43849,7 +44122,7 @@ var App = class _App extends React52.Component {
43849
44122
  }
43850
44123
  }
43851
44124
  addTextFromPaste(text, isPlainPaste = false) {
43852
- const { x, y } = viewportCoordsToSceneCoords3(
44125
+ const { x, y } = viewportCoordsToSceneCoords4(
43853
44126
  {
43854
44127
  clientX: this.lastViewportPosition.x,
43855
44128
  clientY: this.lastViewportPosition.y
@@ -44153,7 +44426,7 @@ var App = class _App extends React52.Component {
44153
44426
  if (elements[index].isDeleted) {
44154
44427
  continue;
44155
44428
  }
44156
- const [x1, y1, x2, y2] = getElementAbsoluteCoords11(
44429
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords12(
44157
44430
  elements[index],
44158
44431
  this.scene.getNonDeletedElementsMap()
44159
44432
  );
@@ -44286,7 +44559,7 @@ var App = class _App extends React52.Component {
44286
44559
  }
44287
44560
  }
44288
44561
  initialPointerDownState(event) {
44289
- const origin = viewportCoordsToSceneCoords3(event, this.state);
44562
+ const origin = viewportCoordsToSceneCoords4(event, this.state);
44290
44563
  const selectedElements = this.scene.getSelectedElements(this.state);
44291
44564
  const [minX, minY, maxX, maxY] = getCommonBounds11(selectedElements);
44292
44565
  const isElbowArrowOnly = selectedElements.findIndex(isElbowArrow10) === 0;
@@ -44450,7 +44723,7 @@ var App = class _App extends React52.Component {
44450
44723
  if (this.state.openDialog?.name === "elementLinkSelector") {
44451
44724
  return;
44452
44725
  }
44453
- const pointerCoords = viewportCoordsToSceneCoords3(event, this.state);
44726
+ const pointerCoords = viewportCoordsToSceneCoords4(event, this.state);
44454
44727
  if (this.state.activeLockedId) {
44455
44728
  this.setState({
44456
44729
  activeLockedId: null
@@ -44699,7 +44972,7 @@ var App = class _App extends React52.Component {
44699
44972
  );
44700
44973
  instantDragOffset[0] *= image.naturalWidth / uncroppedSize.width;
44701
44974
  instantDragOffset[1] *= image.naturalHeight / uncroppedSize.height;
44702
- const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords11(
44975
+ const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords12(
44703
44976
  croppingElement,
44704
44977
  elementsMap
44705
44978
  );
@@ -44867,7 +45140,7 @@ var App = class _App extends React52.Component {
44867
45140
  },
44868
45141
  []
44869
45142
  );
44870
- pointerDownState.drag.origin = viewportCoordsToSceneCoords3(
45143
+ pointerDownState.drag.origin = viewportCoordsToSceneCoords4(
44871
45144
  event,
44872
45145
  this.state
44873
45146
  );
@@ -45127,7 +45400,7 @@ var App = class _App extends React52.Component {
45127
45400
  SnapCache.setVisibleGaps(null);
45128
45401
  this.savePointer(childEvent.clientX, childEvent.clientY, "up");
45129
45402
  const hitElements = pointerDownState.hit.allHitElements;
45130
- const sceneCoords = viewportCoordsToSceneCoords3(
45403
+ const sceneCoords = viewportCoordsToSceneCoords4(
45131
45404
  { clientX: childEvent.clientX, clientY: childEvent.clientY },
45132
45405
  this.state
45133
45406
  );
@@ -45265,7 +45538,7 @@ var App = class _App extends React52.Component {
45265
45538
  childEvent
45266
45539
  );
45267
45540
  if (newElement6?.type === "freedraw") {
45268
- const pointerCoords = viewportCoordsToSceneCoords3(
45541
+ const pointerCoords = viewportCoordsToSceneCoords4(
45269
45542
  childEvent,
45270
45543
  this.state
45271
45544
  );
@@ -45288,7 +45561,7 @@ var App = class _App extends React52.Component {
45288
45561
  if (newElement6.points.length > 1 && newElement6.points[1][0] !== 0 && newElement6.points[1][1] !== 0) {
45289
45562
  this.store.scheduleCapture();
45290
45563
  }
45291
- const pointerCoords = viewportCoordsToSceneCoords3(
45564
+ const pointerCoords = viewportCoordsToSceneCoords4(
45292
45565
  childEvent,
45293
45566
  this.state
45294
45567
  );
@@ -45420,7 +45693,7 @@ var App = class _App extends React52.Component {
45420
45693
  this.scene.triggerUpdate();
45421
45694
  }
45422
45695
  if (pointerDownState.drag.hasOccurred) {
45423
- const sceneCoords2 = viewportCoordsToSceneCoords3(childEvent, this.state);
45696
+ const sceneCoords2 = viewportCoordsToSceneCoords4(childEvent, this.state);
45424
45697
  if (this.state.selectedLinearElement && this.state.selectedLinearElement.isDragging) {
45425
45698
  const linearElement = this.scene.getElement(
45426
45699
  this.state.selectedLinearElement.elementId
@@ -45573,7 +45846,7 @@ var App = class _App extends React52.Component {
45573
45846
  pointFrom29(pointerEnd.clientX, pointerEnd.clientY)
45574
45847
  );
45575
45848
  if (draggedDistance === 0) {
45576
- const scenePointer = viewportCoordsToSceneCoords3(
45849
+ const scenePointer = viewportCoordsToSceneCoords4(
45577
45850
  {
45578
45851
  clientX: pointerEnd.clientX,
45579
45852
  clientY: pointerEnd.clientY
@@ -46300,7 +46573,7 @@ import {
46300
46573
  UserIdleState as UserIdleState2,
46301
46574
  normalizeLink as normalizeLink4,
46302
46575
  sceneCoordsToViewportCoords as sceneCoordsToViewportCoords2,
46303
- viewportCoordsToSceneCoords as viewportCoordsToSceneCoords4,
46576
+ viewportCoordsToSceneCoords as viewportCoordsToSceneCoords5,
46304
46577
  getFormFactor as getFormFactor2
46305
46578
  } from "@orangecatai/common";
46306
46579
  import {
@@ -46520,32 +46793,37 @@ import { isElementLink as isElementLink3 } from "@orangecatai/element";
46520
46793
  import { setCustomTextMetricsProvider } from "@orangecatai/element";
46521
46794
 
46522
46795
  // components/AIChatPanel.tsx
46523
- import {
46796
+ import React57, {
46524
46797
  useCallback as useCallback24,
46525
46798
  useEffect as useEffect55,
46799
+ useImperativeHandle as useImperativeHandle4,
46526
46800
  useRef as useRef49,
46527
46801
  useState as useState49
46528
46802
  } from "react";
46529
46803
  import {
46530
46804
  ArrowUp as ArrowUp3,
46531
46805
  AtSign,
46806
+ Check,
46532
46807
  ChevronDown as ChevronDown2,
46533
46808
  ChevronRight,
46534
46809
  Copy,
46535
46810
  Globe,
46536
46811
  MessageSquare,
46537
46812
  Mic,
46538
- MoreHorizontal,
46539
- Pencil,
46813
+ MicOff,
46814
+ Paperclip,
46540
46815
  Plus,
46541
46816
  Search,
46542
- ThumbsDown,
46543
- ThumbsUp,
46544
- Trash,
46545
46817
  Wrench,
46546
46818
  X as X2
46547
46819
  } from "lucide-react";
46548
46820
 
46821
+ // utils/openRouterApiKey.ts
46822
+ function resolveOpenRouterApiKey(propKey) {
46823
+ const normalizedPropKey = propKey?.trim();
46824
+ return (normalizedPropKey ? normalizedPropKey : void 0) ?? (typeof import.meta !== "undefined" && define_import_meta_env_default?.VITE_APP_OPENROUTER_API_KEY ? define_import_meta_env_default.VITE_APP_OPENROUTER_API_KEY : "") ?? "";
46825
+ }
46826
+
46549
46827
  // components/ai-chat/canvasTools.ts
46550
46828
  import { nanoid as nanoid2 } from "nanoid";
46551
46829
  import {
@@ -46880,17 +47158,12 @@ async function captureFrameScreenshot(api, frameId) {
46880
47158
  const files = api.getFiles();
46881
47159
  const children = getFrameChildren7(elements, frameId);
46882
47160
  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
- );
47161
+ const canvas = await exportToCanvas([...children, frame], appState, files, {
47162
+ exportBackground: true,
47163
+ viewBackgroundColor: appState.viewBackgroundColor,
47164
+ exportingFrame: frame,
47165
+ exportPadding: 0
47166
+ });
46894
47167
  return canvas.toDataURL("image/jpeg", 0.7);
46895
47168
  } catch {
46896
47169
  return null;
@@ -46965,12 +47238,15 @@ function coversFrame(elX, elY, elW, elH, frameEl) {
46965
47238
  return Math.abs(elX - frameEl.x) < COVER_TOLERANCE && Math.abs(elY - frameEl.y) < COVER_TOLERANCE && Math.abs(elW - frameEl.width) < COVER_TOLERANCE && Math.abs(elH - frameEl.height) < COVER_TOLERANCE;
46966
47239
  }
46967
47240
  function getElementTier(type, x, y, w, h, frame) {
46968
- if (type === "text")
47241
+ if (type === "text") {
46969
47242
  return 3 /* TEXT */;
46970
- if (type === "image")
47243
+ }
47244
+ if (type === "image") {
46971
47245
  return 1 /* IMAGE */;
46972
- if (coversFrame(x, y, w, h, frame))
47246
+ }
47247
+ if (coversFrame(x, y, w, h, frame)) {
46973
47248
  return 0 /* BG_FILL */;
47249
+ }
46974
47250
  return 2 /* SHAPE */;
46975
47251
  }
46976
47252
  function findInsertIndexByTier(elements, frameIndex, tier) {
@@ -46979,9 +47255,17 @@ function findInsertIndexByTier(elements, frameIndex, tier) {
46979
47255
  let insertAt = frameIndex + 1;
46980
47256
  for (let i = frameIndex + 1; i < elements.length; i++) {
46981
47257
  const el = elements[i];
46982
- if (el.frameId !== frameId)
47258
+ if (el.frameId !== frameId) {
46983
47259
  continue;
46984
- const elTier = getElementTier(el.type, el.x, el.y, el.width, el.height, frame);
47260
+ }
47261
+ const elTier = getElementTier(
47262
+ el.type,
47263
+ el.x,
47264
+ el.y,
47265
+ el.width,
47266
+ el.height,
47267
+ frame
47268
+ );
46985
47269
  if (elTier > tier) {
46986
47270
  return i;
46987
47271
  }
@@ -47113,7 +47397,11 @@ function execAddText(args, ctx) {
47113
47397
  }
47114
47398
  const updatedText = { ...textEl, frameId };
47115
47399
  const newElements = [...elements];
47116
- const insertAt = findInsertIndexByTier(newElements, frameIndex, 3 /* TEXT */);
47400
+ const insertAt = findInsertIndexByTier(
47401
+ newElements,
47402
+ frameIndex,
47403
+ 3 /* TEXT */
47404
+ );
47117
47405
  newElements.splice(insertAt, 0, updatedText);
47118
47406
  ctx.excalidrawAPI.updateScene({ elements: newElements });
47119
47407
  return {
@@ -47203,7 +47491,11 @@ async function execGenerateImage(args, ctx) {
47203
47491
  }
47204
47492
  const updatedImage = { ...imageEl, frameId };
47205
47493
  const newElements = [...currentElements];
47206
- const insertAt = findInsertIndexByTier(newElements, currentFrameIndex, 1 /* IMAGE */);
47494
+ const insertAt = findInsertIndexByTier(
47495
+ newElements,
47496
+ currentFrameIndex,
47497
+ 1 /* IMAGE */
47498
+ );
47207
47499
  newElements.splice(insertAt, 0, updatedImage);
47208
47500
  ctx.excalidrawAPI.updateScene({ elements: newElements });
47209
47501
  ctx.excalidrawAPI.addFiles([
@@ -47394,7 +47686,33 @@ When asked to create an ad or visual from scratch:
47394
47686
  - If the user's request is unclear, use the available tools to create a reasonable default and explain what you did.
47395
47687
  - Coordinates: frame x,y is the top-left corner. Width extends right, height extends down.`;
47396
47688
  var MAX_ITERATIONS = 15;
47397
- async function callOpenRouter(messages, apiKey, signal) {
47689
+ async function callOpenRouter(messages, apiKey, signal, webSearchEnabled) {
47690
+ const model = webSearchEnabled ? "openai/gpt-4.1-mini:online" : "openai/gpt-4.1-mini";
47691
+ const body = {
47692
+ model,
47693
+ messages: messages.map((m) => {
47694
+ if (m.role === "assistant" && m.tool_calls) {
47695
+ return {
47696
+ role: m.role,
47697
+ content: m.content,
47698
+ tool_calls: m.tool_calls
47699
+ };
47700
+ }
47701
+ if (m.role === "tool") {
47702
+ return {
47703
+ role: m.role,
47704
+ tool_call_id: m.tool_call_id,
47705
+ content: m.content
47706
+ };
47707
+ }
47708
+ return { role: m.role, content: m.content };
47709
+ }),
47710
+ tools: CANVAS_TOOLS,
47711
+ tool_choice: "auto"
47712
+ };
47713
+ if (webSearchEnabled) {
47714
+ body.plugins = [{ id: "web", max_results: 5 }];
47715
+ }
47398
47716
  const response = await fetch(
47399
47717
  "https://openrouter.ai/api/v1/chat/completions",
47400
47718
  {
@@ -47404,28 +47722,7 @@ async function callOpenRouter(messages, apiKey, signal) {
47404
47722
  Authorization: `Bearer ${apiKey}`,
47405
47723
  "Content-Type": "application/json"
47406
47724
  },
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
- })
47725
+ body: JSON.stringify(body)
47429
47726
  }
47430
47727
  );
47431
47728
  if (!response.ok) {
@@ -47454,10 +47751,14 @@ async function runAgentLoop(opts) {
47454
47751
  userMessages,
47455
47752
  elementContext,
47456
47753
  frameScreenshot,
47754
+ fileAttachments,
47755
+ webSearchEnabled,
47457
47756
  apiKey,
47458
47757
  toolCtx,
47459
47758
  onUpdate,
47460
- signal
47759
+ signal,
47760
+ onBeforeImageGen,
47761
+ onAfterImageGen
47461
47762
  } = opts;
47462
47763
  const messages = [{ role: "system", content: SYSTEM_PROMPT }];
47463
47764
  if (elementContext) {
@@ -47471,22 +47772,43 @@ ${elementContext}`
47471
47772
  for (let i = 0; i < userMessages.length; i++) {
47472
47773
  const msg = userMessages[i];
47473
47774
  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}`
47775
+ const hasMultimodal = isLast && msg.role === "user" && (frameScreenshot || fileAttachments && fileAttachments.some((a) => a.type === "image"));
47776
+ const hasTextFiles = isLast && msg.role === "user" && fileAttachments && fileAttachments.some((a) => a.type === "text");
47777
+ if (hasMultimodal || hasTextFiles) {
47778
+ const contentParts = [];
47779
+ if (frameScreenshot) {
47780
+ contentParts.push({
47781
+ type: "image_url",
47782
+ image_url: { url: frameScreenshot }
47783
+ });
47784
+ }
47785
+ if (fileAttachments) {
47786
+ for (const att of fileAttachments) {
47787
+ if (att.type === "image" && att.dataUrl) {
47788
+ contentParts.push({
47789
+ type: "image_url",
47790
+ image_url: { url: att.dataUrl }
47791
+ });
47487
47792
  }
47488
- ]
47489
- });
47793
+ }
47794
+ }
47795
+ let text = msg.content;
47796
+ if (frameScreenshot) {
47797
+ text = `[Screenshot of the referenced frame is attached above]
47798
+
47799
+ ${text}`;
47800
+ }
47801
+ if (fileAttachments) {
47802
+ const textFiles = fileAttachments.filter((a) => a.type === "text");
47803
+ if (textFiles.length > 0) {
47804
+ text += `
47805
+
47806
+ ${textFiles.map((f) => `[File: ${f.name}]
47807
+ ${f.textContent}`).join("\n\n")}`;
47808
+ }
47809
+ }
47810
+ contentParts.push({ type: "text", text });
47811
+ messages.push({ role: "user", content: contentParts });
47490
47812
  } else {
47491
47813
  messages.push({ role: msg.role, content: msg.content });
47492
47814
  }
@@ -47499,7 +47821,12 @@ ${msg.content}`
47499
47821
  type: "status",
47500
47822
  message: iterations === 1 ? "Thinking\u2026" : "Processing tool results\u2026"
47501
47823
  });
47502
- const response = await callOpenRouter(messages, apiKey, signal);
47824
+ const response = await callOpenRouter(
47825
+ messages,
47826
+ apiKey,
47827
+ signal,
47828
+ webSearchEnabled
47829
+ );
47503
47830
  if (!response.tool_calls || response.tool_calls.length === 0) {
47504
47831
  const reply = response.content || "Done!";
47505
47832
  onUpdate({ type: "final", message: reply });
@@ -47521,7 +47848,46 @@ ${msg.content}`
47521
47848
  type: "tool_start",
47522
47849
  message: formatToolStartMessage(name, parsedArgs)
47523
47850
  });
47851
+ if (name === "generate_image" && onBeforeImageGen) {
47852
+ let gateResult;
47853
+ try {
47854
+ gateResult = await onBeforeImageGen();
47855
+ } catch {
47856
+ gateResult = { allowed: false, error: "Credit check failed" };
47857
+ }
47858
+ const { allowed, error } = gateResult;
47859
+ if (!allowed) {
47860
+ const gatedResult = {
47861
+ success: false,
47862
+ error: error || "Insufficient credits for image generation",
47863
+ statusMessage: error || "Insufficient credits for image generation"
47864
+ };
47865
+ const gatedAction = {
47866
+ toolName: name,
47867
+ args: parsedArgs,
47868
+ result: gatedResult
47869
+ };
47870
+ toolActions.push(gatedAction);
47871
+ onUpdate({
47872
+ type: "tool_done",
47873
+ message: gatedResult.statusMessage,
47874
+ toolAction: gatedAction
47875
+ });
47876
+ messages.push({
47877
+ role: "tool",
47878
+ tool_call_id: toolCall.id,
47879
+ content: JSON.stringify({
47880
+ success: false,
47881
+ error: gatedResult.error
47882
+ })
47883
+ });
47884
+ continue;
47885
+ }
47886
+ }
47524
47887
  const result = await executeCanvasTool(name, rawArgs, toolCtx);
47888
+ if (name === "generate_image" && result.success && onAfterImageGen) {
47889
+ onAfterImageGen();
47890
+ }
47525
47891
  const action = {
47526
47892
  toolName: name,
47527
47893
  args: parsedArgs,
@@ -47567,6 +47933,100 @@ function formatToolStartMessage(name, args) {
47567
47933
  }
47568
47934
  }
47569
47935
 
47936
+ // components/ai-chat/audioUtils.ts
47937
+ async function blobToWavBase64(blob) {
47938
+ const arrayBuffer = await blob.arrayBuffer();
47939
+ const audioCtx = new AudioContext();
47940
+ const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
47941
+ await audioCtx.close();
47942
+ const numChannels = audioBuffer.numberOfChannels;
47943
+ const sampleRate = audioBuffer.sampleRate;
47944
+ const numSamples = audioBuffer.length;
47945
+ const bytesPerSample = 2;
47946
+ const dataSize = numSamples * numChannels * bytesPerSample;
47947
+ const bufferSize = 44 + dataSize;
47948
+ const wavBuffer = new ArrayBuffer(bufferSize);
47949
+ const view = new DataView(wavBuffer);
47950
+ const writeStr = (offset2, str) => {
47951
+ for (let i = 0; i < str.length; i++) {
47952
+ view.setUint8(offset2 + i, str.charCodeAt(i));
47953
+ }
47954
+ };
47955
+ writeStr(0, "RIFF");
47956
+ view.setUint32(4, 36 + dataSize, true);
47957
+ writeStr(8, "WAVE");
47958
+ writeStr(12, "fmt ");
47959
+ view.setUint32(16, 16, true);
47960
+ view.setUint16(20, 1, true);
47961
+ view.setUint16(22, numChannels, true);
47962
+ view.setUint32(24, sampleRate, true);
47963
+ view.setUint32(28, sampleRate * numChannels * bytesPerSample, true);
47964
+ view.setUint16(32, numChannels * bytesPerSample, true);
47965
+ view.setUint16(34, 16, true);
47966
+ writeStr(36, "data");
47967
+ view.setUint32(40, dataSize, true);
47968
+ let offset = 44;
47969
+ for (let i = 0; i < numSamples; i++) {
47970
+ for (let ch = 0; ch < numChannels; ch++) {
47971
+ const sample = audioBuffer.getChannelData(ch)[i];
47972
+ const clamped = Math.max(-1, Math.min(1, sample));
47973
+ const int16 = clamped < 0 ? clamped * 32768 : clamped * 32767;
47974
+ view.setInt16(offset, int16, true);
47975
+ offset += 2;
47976
+ }
47977
+ }
47978
+ const uint8 = new Uint8Array(wavBuffer);
47979
+ const CHUNK = 8192;
47980
+ let binary = "";
47981
+ for (let i = 0; i < uint8.length; i += CHUNK) {
47982
+ binary += String.fromCharCode(...uint8.subarray(i, i + CHUNK));
47983
+ }
47984
+ return btoa(binary);
47985
+ }
47986
+ async function transcribeAudio(base64Audio, format, apiKey) {
47987
+ const response = await fetch(
47988
+ "https://openrouter.ai/api/v1/chat/completions",
47989
+ {
47990
+ method: "POST",
47991
+ headers: {
47992
+ Authorization: `Bearer ${apiKey}`,
47993
+ "Content-Type": "application/json"
47994
+ },
47995
+ body: JSON.stringify({
47996
+ model: "mistralai/voxtral-small-24b-2507",
47997
+ messages: [
47998
+ {
47999
+ role: "user",
48000
+ content: [
48001
+ {
48002
+ type: "text",
48003
+ text: "Transcribe this audio. Return only the transcription text, nothing else."
48004
+ },
48005
+ {
48006
+ type: "input_audio",
48007
+ input_audio: { data: base64Audio, format }
48008
+ }
48009
+ ]
48010
+ }
48011
+ ]
48012
+ })
48013
+ }
48014
+ );
48015
+ if (!response.ok) {
48016
+ let message = `Transcription error ${response.status}`;
48017
+ try {
48018
+ const err = await response.json();
48019
+ if (err?.error?.message) {
48020
+ message = err.error.message;
48021
+ }
48022
+ } catch {
48023
+ }
48024
+ throw new Error(message);
48025
+ }
48026
+ const data = await response.json();
48027
+ return data?.choices?.[0]?.message?.content || "";
48028
+ }
48029
+
47570
48030
  // components/ui/chat-container.tsx
47571
48031
  import { StickToBottom } from "use-stick-to-bottom";
47572
48032
  import { jsx as jsx177 } from "react/jsx-runtime";
@@ -47897,479 +48357,873 @@ import { jsx as jsx184, jsxs as jsxs100 } from "react/jsx-runtime";
47897
48357
  function genId() {
47898
48358
  return Math.random().toString(36).slice(2, 10);
47899
48359
  }
48360
+ var MAX_ATTACHED_FILES = 3;
48361
+ var MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
47900
48362
  var SUGGESTIONS = [
47901
48363
  "Create a pasta ad",
47902
48364
  "Design a sale banner",
47903
48365
  "Make a product showcase",
47904
48366
  "Help me design"
47905
48367
  ];
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;
48368
+ var AIChatPanel = React57.forwardRef(
48369
+ ({
48370
+ isOpen,
48371
+ onClose,
48372
+ apiKey,
48373
+ excalidrawAPI,
48374
+ geminiApiKey,
48375
+ initialMessages,
48376
+ initialSessionId,
48377
+ initialSessions,
48378
+ onMessagesChange,
48379
+ onSessionCreate,
48380
+ onSessionSwitch,
48381
+ onBeforeSend,
48382
+ onAfterSend,
48383
+ onBeforeImageGen,
48384
+ onAfterImageGen
48385
+ }, ref) => {
48386
+ const [prompt, setPrompt] = useState49("");
48387
+ const [isLoading, setIsLoading] = useState49(false);
48388
+ const [statusText, setStatusText] = useState49("");
48389
+ const [messages, setMessages] = useState49(
48390
+ initialMessages ?? []
48391
+ );
48392
+ const [sessions, setSessions] = useState49(
48393
+ initialSessions ?? []
48394
+ );
48395
+ const [currentSessionId, setCurrentSessionId] = useState49(
48396
+ initialSessionId ?? genId
48397
+ );
48398
+ const [historyOpen, setHistoryOpen] = useState49(false);
48399
+ const [historySearch, setHistorySearch] = useState49("");
48400
+ const [error, setError] = useState49(null);
48401
+ const [frameRef, setFrameRef] = useState49(null);
48402
+ const [mentionMenuOpen, setMentionMenuOpen] = useState49(false);
48403
+ const [availableFrames, setAvailableFrames] = useState49([]);
48404
+ const [attachedFiles, setAttachedFiles] = useState49([]);
48405
+ const [webSearchEnabled, setWebSearchEnabled] = useState49(false);
48406
+ const [isSendPending, setIsSendPending] = useState49(false);
48407
+ const [isLoadingSession, setIsLoadingSession] = useState49(false);
48408
+ const [isRecording, setIsRecording] = useState49(false);
48409
+ const [isTranscribing, setIsTranscribing] = useState49(false);
48410
+ const [copiedMsgId, setCopiedMsgId] = useState49(null);
48411
+ const historyRef = useRef49(null);
48412
+ const abortControllerRef = useRef49(null);
48413
+ const mentionMenuRef = useRef49(null);
48414
+ const fileInputRef = useRef49(null);
48415
+ const mediaRecorderRef = useRef49(null);
48416
+ const audioChunksRef = useRef49([]);
48417
+ const prevPromptRef = useRef49("");
48418
+ const voiceUsedRef = useRef49(false);
48419
+ const currentSessionIdRef = useRef49(currentSessionId);
48420
+ useEffect55(() => {
48421
+ currentSessionIdRef.current = currentSessionId;
48422
+ }, [currentSessionId]);
48423
+ useEffect55(() => {
48424
+ const handler = (e) => {
48425
+ if (historyRef.current && !historyRef.current.contains(e.target)) {
48426
+ setHistoryOpen(false);
48427
+ }
48428
+ if (mentionMenuRef.current && !mentionMenuRef.current.contains(e.target)) {
48429
+ setMentionMenuOpen(false);
48430
+ }
48431
+ };
48432
+ document.addEventListener("mousedown", handler);
48433
+ return () => document.removeEventListener("mousedown", handler);
48434
+ }, []);
48435
+ useEffect55(() => {
48436
+ if (!isOpen) {
48437
+ abortControllerRef.current?.abort();
48438
+ }
48439
+ }, [isOpen]);
48440
+ const currentTitle = (() => {
48441
+ const sessionTitle = sessions.find(
48442
+ (s) => s.id === currentSessionId
48443
+ )?.title;
48444
+ if (sessionTitle) {
48445
+ return sessionTitle;
48446
+ }
48447
+ if (messages.length > 0) {
48448
+ return messages[0].content.slice(0, 30) + (messages[0].content.length > 30 ? "\u2026" : "");
48449
+ }
48450
+ return "New chat";
48451
+ })();
48452
+ const saveCurrentSession = useCallback24(() => {
48453
+ if (messages.length === 0) {
48454
+ return;
47958
48455
  }
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) => {
48456
+ const title = messages[0].content.slice(0, 40) + (messages[0].content.length > 40 ? "\u2026" : "");
48457
+ setSessions((prev) => {
48458
+ const existing = prev.findIndex((s) => s.id === currentSessionId);
48459
+ const updated = { id: currentSessionId, title, messages };
48460
+ if (existing >= 0) {
48461
+ const next = [...prev];
48462
+ next[existing] = updated;
48463
+ return next;
48464
+ }
48465
+ return [updated, ...prev];
48466
+ });
48467
+ }, [messages, currentSessionId]);
48468
+ const handleNewChat = useCallback24(() => {
47974
48469
  saveCurrentSession();
47975
- setMessages(session.messages);
47976
- setCurrentSessionId(session.id);
48470
+ const newSessionId = genId();
48471
+ setMessages([]);
48472
+ setCurrentSessionId(newSessionId);
48473
+ setIsLoadingSession(false);
47977
48474
  setHistoryOpen(false);
47978
48475
  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) {
48476
+ setPrompt("");
48477
+ setFrameRef(null);
48478
+ setAttachedFiles([]);
48479
+ setStatusText("");
48480
+ }, [saveCurrentSession]);
48481
+ const handleSwitchSession = useCallback24(
48482
+ (session) => {
48483
+ saveCurrentSession();
48484
+ setCurrentSessionId(session.id);
48485
+ setHistoryOpen(false);
48486
+ setError(null);
48487
+ if (session.messages.length > 0) {
48488
+ setMessages(session.messages);
48489
+ setIsLoadingSession(false);
48490
+ } else {
48491
+ setMessages([]);
48492
+ setIsLoadingSession(true);
48493
+ if (onSessionSwitch) {
48494
+ onSessionSwitch(session.id);
48495
+ }
48496
+ }
48497
+ },
48498
+ [saveCurrentSession, onSessionSwitch]
48499
+ );
48500
+ const handlePromptChange = useCallback24(
48501
+ (value) => {
48502
+ const prevValue = prevPromptRef.current;
48503
+ setPrompt(value);
48504
+ prevPromptRef.current = value;
48505
+ if (excalidrawAPI && value.length > prevValue.length && value.endsWith("@")) {
48506
+ setAvailableFrames(listFrames(excalidrawAPI));
48507
+ setMentionMenuOpen(true);
48508
+ }
48509
+ },
48510
+ [excalidrawAPI]
48511
+ );
48512
+ const handlePickFrame = useCallback24(
48513
+ async (frame) => {
48514
+ if (!excalidrawAPI) {
48515
+ return;
48516
+ }
48517
+ setMentionMenuOpen(false);
48518
+ setPrompt((prev) => {
48519
+ const atIndex2 = prev.lastIndexOf("@");
48520
+ return atIndex2 >= 0 ? prev.slice(0, atIndex2) : prev;
48521
+ });
48522
+ const atIndex = prevPromptRef.current.lastIndexOf("@");
48523
+ prevPromptRef.current = atIndex >= 0 ? prevPromptRef.current.slice(0, atIndex) : prevPromptRef.current;
48524
+ const ctx = getFrameContext(excalidrawAPI, frame.id);
48525
+ if (!ctx) {
48526
+ return;
48527
+ }
48528
+ const screenshot = await captureFrameScreenshot(
48529
+ excalidrawAPI,
48530
+ frame.id
48531
+ );
48532
+ setFrameRef({
48533
+ frameId: frame.id,
48534
+ label: ctx.frameInfo.name,
48535
+ serialized: ctx.serialized,
48536
+ screenshot: screenshot ?? void 0
48537
+ });
48538
+ },
48539
+ [excalidrawAPI]
48540
+ );
48541
+ const handleRemoveRef = useCallback24(() => {
48542
+ setFrameRef(null);
48543
+ }, []);
48544
+ const handleFileUpload = useCallback24(
48545
+ (e) => {
48546
+ const files = e.target.files;
48547
+ if (!files) {
48548
+ return;
48549
+ }
48550
+ const remaining = MAX_ATTACHED_FILES - attachedFiles.length;
48551
+ if (remaining <= 0) {
48552
+ return;
48553
+ }
48554
+ const toAdd = Array.from(files).slice(0, remaining);
48555
+ for (const file2 of toAdd) {
48556
+ if (file2.size > MAX_FILE_SIZE_BYTES) {
48557
+ setError(`"${file2.name}" is too large (max 10 MB).`);
48558
+ continue;
48559
+ }
48560
+ const id = genId();
48561
+ const isImage = file2.type.startsWith("image/");
48562
+ const reader = new FileReader();
48563
+ if (isImage) {
48564
+ reader.onload = () => {
48565
+ setAttachedFiles(
48566
+ (prev) => prev.length < MAX_ATTACHED_FILES ? [
48567
+ ...prev,
48568
+ {
48569
+ id,
48570
+ file: file2,
48571
+ name: file2.name,
48572
+ type: "image",
48573
+ dataUrl: reader.result
48574
+ }
48575
+ ] : prev
48576
+ );
48577
+ };
48578
+ reader.readAsDataURL(file2);
48579
+ } else {
48580
+ reader.onload = () => {
48581
+ setAttachedFiles(
48582
+ (prev) => prev.length < MAX_ATTACHED_FILES ? [
48583
+ ...prev,
48584
+ {
48585
+ id,
48586
+ file: file2,
48587
+ name: file2.name,
48588
+ type: "text",
48589
+ textContent: reader.result
48590
+ }
48591
+ ] : prev
48592
+ );
48593
+ };
48594
+ reader.readAsText(file2);
48595
+ }
48596
+ }
48597
+ if (fileInputRef.current) {
48598
+ fileInputRef.current.value = "";
48599
+ }
48600
+ },
48601
+ [attachedFiles.length]
48602
+ );
48603
+ const handleRemoveFile = useCallback24((fileId) => {
48604
+ setAttachedFiles((prev) => prev.filter((f) => f.id !== fileId));
48605
+ }, []);
48606
+ const toggleWebSearch = useCallback24(() => {
48607
+ setWebSearchEnabled((prev) => !prev);
48608
+ }, []);
48609
+ const handleVoiceInput = useCallback24(async () => {
48610
+ if (isRecording) {
48611
+ mediaRecorderRef.current?.stop();
48612
+ setIsRecording(false);
48613
+ voiceUsedRef.current = true;
47992
48614
  return;
47993
48615
  }
47994
- setMentionMenuOpen(false);
47995
- const ctx = getFrameContext(excalidrawAPI, frame.id);
47996
- if (!ctx) {
47997
- return;
48616
+ let stream = null;
48617
+ try {
48618
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
48619
+ const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
48620
+ const mediaRecorder = new MediaRecorder(stream, { mimeType });
48621
+ mediaRecorderRef.current = mediaRecorder;
48622
+ audioChunksRef.current = [];
48623
+ mediaRecorder.ondataavailable = (e) => {
48624
+ if (e.data.size > 0) {
48625
+ audioChunksRef.current.push(e.data);
48626
+ }
48627
+ };
48628
+ mediaRecorder.onstop = async () => {
48629
+ stream?.getTracks().forEach((t2) => t2.stop());
48630
+ const audioBlob = new Blob(audioChunksRef.current, {
48631
+ type: mediaRecorder.mimeType
48632
+ });
48633
+ const normalizedKey = resolveOpenRouterApiKey(apiKey);
48634
+ if (!normalizedKey) {
48635
+ setError("No API key for voice transcription.");
48636
+ return;
48637
+ }
48638
+ setIsTranscribing(true);
48639
+ setStatusText("Transcribing\u2026");
48640
+ try {
48641
+ const wavBase64 = await blobToWavBase64(audioBlob);
48642
+ const transcription = await transcribeAudio(
48643
+ wavBase64,
48644
+ "wav",
48645
+ normalizedKey
48646
+ );
48647
+ const updated = (prevPromptRef.current ? `${prevPromptRef.current} ` : "") + transcription;
48648
+ prevPromptRef.current = updated;
48649
+ setPrompt(updated);
48650
+ } catch (err) {
48651
+ voiceUsedRef.current = false;
48652
+ setError(
48653
+ err instanceof Error ? err.message : "Transcription failed"
48654
+ );
48655
+ } finally {
48656
+ setIsTranscribing(false);
48657
+ setStatusText("");
48658
+ }
48659
+ };
48660
+ mediaRecorder.start();
48661
+ setIsRecording(true);
48662
+ } catch {
48663
+ stream?.getTracks().forEach((t2) => t2.stop());
48664
+ setError("Could not access microphone. Check browser permissions.");
47998
48665
  }
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."
48666
+ }, [isRecording, apiKey]);
48667
+ const handleCopy = useCallback24((msgId, content) => {
48668
+ navigator.clipboard.writeText(content).then(
48669
+ () => {
48670
+ setCopiedMsgId(msgId);
48671
+ setTimeout(() => setCopiedMsgId(null), 2e3);
48672
+ },
48673
+ () => {
48674
+ setError("Copy failed \u2014 please grant clipboard permission.");
48675
+ }
48024
48676
  );
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
- {
48677
+ }, []);
48678
+ const handleSend = useCallback24(
48679
+ async (textOverride) => {
48680
+ const text = (textOverride ?? prompt).trim();
48681
+ const normalizedKey = resolveOpenRouterApiKey(apiKey);
48682
+ if (!text || isLoading || isSendPending) {
48683
+ voiceUsedRef.current = false;
48684
+ return;
48685
+ }
48686
+ if (!normalizedKey) {
48687
+ voiceUsedRef.current = false;
48688
+ setError(
48689
+ "No OpenRouter API key. Set VITE_APP_OPENROUTER_API_KEY in .env or pass the key via the `apiKey` prop."
48690
+ );
48691
+ return;
48692
+ }
48693
+ const capturedFiles = [...attachedFiles];
48694
+ const capturedWebSearch = webSearchEnabled;
48695
+ const capturedFrameRef = frameRef;
48696
+ const userMsg = {
48697
+ id: genId(),
48698
+ role: "user",
48699
+ content: text,
48700
+ timestamp: /* @__PURE__ */ new Date(),
48701
+ attachments: capturedFiles.length > 0 ? capturedFiles.map((f) => ({
48702
+ name: f.name,
48703
+ type: f.type,
48704
+ dataUrl: f.dataUrl
48705
+ })) : void 0
48706
+ };
48707
+ const isFirstMessage = messages.length === 0;
48708
+ const prevMessages = messages;
48709
+ const nextMessages = [...messages, userMsg];
48710
+ setMessages(nextMessages);
48711
+ setPrompt("");
48712
+ prevPromptRef.current = "";
48713
+ setAttachedFiles([]);
48714
+ setFrameRef(null);
48715
+ setIsSendPending(true);
48716
+ setError(null);
48717
+ const hadVoice = voiceUsedRef.current;
48718
+ voiceUsedRef.current = false;
48719
+ if (onBeforeSend) {
48720
+ const { allowed, error: creditError } = await onBeforeSend({
48721
+ hasVoice: hadVoice
48722
+ });
48723
+ if (!allowed) {
48724
+ setMessages(prevMessages);
48725
+ setError(creditError || "Insufficient credits");
48726
+ setIsSendPending(false);
48727
+ return;
48728
+ }
48729
+ }
48730
+ setIsSendPending(false);
48731
+ const controller = new AbortController();
48732
+ abortControllerRef.current = controller;
48733
+ setIsLoading(true);
48734
+ setStatusText("Thinking\u2026");
48735
+ if (!excalidrawAPI) {
48736
+ try {
48737
+ const reply = await callPlainChatAPI(
48738
+ nextMessages,
48739
+ normalizedKey,
48740
+ controller.signal,
48741
+ {
48742
+ webSearchEnabled: capturedWebSearch,
48743
+ attachments: capturedFiles
48744
+ }
48745
+ );
48746
+ const assistantMsg = {
48747
+ id: genId(),
48748
+ role: "assistant",
48749
+ content: reply,
48750
+ timestamp: /* @__PURE__ */ new Date()
48751
+ };
48752
+ const updatedMessages = [...nextMessages, assistantMsg];
48753
+ setMessages(updatedMessages);
48754
+ if (onAfterSend) {
48755
+ onAfterSend({ hasVoice: hadVoice });
48756
+ }
48757
+ if (isFirstMessage && onSessionCreate) {
48758
+ onSessionCreate({
48759
+ id: currentSessionId,
48760
+ title: text.slice(0, 40)
48761
+ });
48762
+ }
48763
+ if (onMessagesChange) {
48764
+ onMessagesChange(updatedMessages, currentSessionId);
48765
+ }
48766
+ } catch (err) {
48767
+ if (err instanceof Error && err.name !== "AbortError") {
48768
+ setError(err.message);
48769
+ }
48770
+ } finally {
48771
+ abortControllerRef.current = null;
48772
+ setIsLoading(false);
48773
+ setStatusText("");
48774
+ }
48775
+ return;
48776
+ }
48777
+ try {
48778
+ const result = await runAgentLoop({
48779
+ userMessages: nextMessages.map((m) => ({
48780
+ role: m.role,
48781
+ content: m.content
48782
+ })),
48783
+ elementContext: capturedFrameRef?.serialized,
48784
+ frameScreenshot: capturedFrameRef?.screenshot,
48785
+ fileAttachments: capturedFiles.length > 0 ? capturedFiles.map((f) => ({
48786
+ name: f.name,
48787
+ type: f.type,
48788
+ dataUrl: f.dataUrl,
48789
+ textContent: f.textContent
48790
+ })) : void 0,
48791
+ webSearchEnabled: capturedWebSearch,
48792
+ apiKey: normalizedKey,
48793
+ toolCtx: {
48794
+ excalidrawAPI,
48795
+ geminiApiKey: geminiApiKey || "",
48796
+ signal: controller.signal
48797
+ },
48798
+ onUpdate: (update) => {
48799
+ if (update.type !== "final") {
48800
+ setStatusText(update.message);
48801
+ }
48802
+ },
48803
+ signal: controller.signal,
48804
+ onBeforeImageGen,
48805
+ onAfterImageGen
48806
+ });
48807
+ const assistantMsgAgent = {
48053
48808
  id: genId(),
48054
48809
  role: "assistant",
48055
- content: reply,
48056
- timestamp: /* @__PURE__ */ new Date()
48810
+ content: result.reply,
48811
+ timestamp: /* @__PURE__ */ new Date(),
48812
+ toolActions: result.toolActions.length > 0 ? result.toolActions : void 0
48813
+ };
48814
+ const updatedMessagesAgent = [...nextMessages, assistantMsgAgent];
48815
+ setMessages(updatedMessagesAgent);
48816
+ if (onAfterSend) {
48817
+ onAfterSend({ hasVoice: hadVoice });
48057
48818
  }
48058
- ]);
48059
- } catch (err) {
48060
- if (err instanceof Error && err.name !== "AbortError") {
48061
- setError(err.message);
48819
+ if (isFirstMessage && onSessionCreate) {
48820
+ onSessionCreate({ id: currentSessionId, title: text.slice(0, 40) });
48821
+ }
48822
+ if (onMessagesChange) {
48823
+ onMessagesChange(updatedMessagesAgent, currentSessionId);
48824
+ }
48825
+ } catch (err) {
48826
+ if (err instanceof Error && err.name !== "AbortError") {
48827
+ setError(err.message);
48828
+ }
48829
+ } finally {
48830
+ abortControllerRef.current = null;
48831
+ setIsLoading(false);
48832
+ setStatusText("");
48062
48833
  }
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);
48834
+ },
48835
+ [
48836
+ prompt,
48837
+ isLoading,
48838
+ isSendPending,
48839
+ messages,
48840
+ apiKey,
48841
+ excalidrawAPI,
48842
+ geminiApiKey,
48843
+ frameRef,
48844
+ attachedFiles,
48845
+ webSearchEnabled,
48846
+ currentSessionId,
48847
+ onBeforeSend,
48848
+ onAfterSend,
48849
+ onMessagesChange,
48850
+ onSessionCreate,
48851
+ onBeforeImageGen,
48852
+ onAfterImageGen
48853
+ ]
48854
+ );
48855
+ useImperativeHandle4(
48856
+ ref,
48857
+ () => ({
48858
+ setMessages: (msgs, sessionId) => {
48859
+ const targetSessionId = sessionId ?? currentSessionIdRef.current;
48860
+ setSessions((prev) => {
48861
+ const idx = prev.findIndex((s) => s.id === targetSessionId);
48862
+ if (idx >= 0) {
48863
+ const next = [...prev];
48864
+ next[idx] = { ...next[idx], messages: msgs };
48865
+ return next;
48866
+ }
48867
+ return prev;
48868
+ });
48869
+ if (targetSessionId === currentSessionIdRef.current) {
48870
+ setMessages(msgs);
48871
+ setIsLoadingSession(false);
48087
48872
  }
48088
48873
  },
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("");
48874
+ send: (text) => handleSend(text)
48875
+ }),
48876
+ [handleSend]
48877
+ );
48878
+ const handleStop = useCallback24(() => {
48879
+ abortControllerRef.current?.abort();
48880
+ }, []);
48881
+ const handleChip = useCallback24((chip) => {
48882
+ setPrompt(chip);
48883
+ }, []);
48884
+ const filteredSessions = sessions.filter(
48885
+ (s) => historySearch ? s.title.toLowerCase().includes(historySearch.toLowerCase()) : true
48886
+ );
48887
+ if (!isOpen) {
48888
+ return null;
48109
48889
  }
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(
48890
+ return /* @__PURE__ */ jsx184(
48891
+ "div",
48892
+ {
48893
+ className: "acp",
48894
+ onPointerDown: (e) => e.stopPropagation(),
48895
+ onClick: (e) => e.stopPropagation(),
48896
+ children: /* @__PURE__ */ jsxs100("div", { className: "acp-panel", children: [
48897
+ /* @__PURE__ */ jsxs100("div", { className: "acp-header", ref: historyRef, children: [
48898
+ /* @__PURE__ */ jsxs100(
48153
48899
  "button",
48154
48900
  {
48155
- className: "acp-icon-btn",
48156
- onClick: handleNewChat,
48157
- title: "New chat",
48158
- children: /* @__PURE__ */ jsx184(Plus, { size: 15 })
48901
+ className: "acp-title-btn",
48902
+ onClick: () => setHistoryOpen((v) => !v),
48903
+ title: "Chat history",
48904
+ children: [
48905
+ /* @__PURE__ */ jsx184("span", { children: currentTitle }),
48906
+ /* @__PURE__ */ jsx184(ChevronDown2, { size: 13 })
48907
+ ]
48159
48908
  }
48160
48909
  ),
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 }),
48910
+ /* @__PURE__ */ jsxs100("div", { className: "acp-header-right", children: [
48166
48911
  /* @__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
48912
  "button",
48180
48913
  {
48181
- className: "acp-history-item acp-history-item--new",
48914
+ className: "acp-icon-btn",
48182
48915
  onClick: handleNewChat,
48183
- children: [
48184
- /* @__PURE__ */ jsx184(Plus, { size: 13 }),
48185
- /* @__PURE__ */ jsx184("span", { children: "New chat" })
48186
- ]
48916
+ title: "New chat",
48917
+ children: /* @__PURE__ */ jsx184(Plus, { size: 15 })
48187
48918
  }
48188
48919
  ),
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" })
48920
+ /* @__PURE__ */ jsx184("button", { className: "acp-icon-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsx184(X2, { size: 15 }) })
48921
+ ] }),
48922
+ historyOpen && /* @__PURE__ */ jsxs100("div", { className: "acp-history-dropdown", children: [
48923
+ /* @__PURE__ */ jsxs100("div", { className: "acp-history-search", children: [
48924
+ /* @__PURE__ */ jsx184(Search, { size: 13 }),
48925
+ /* @__PURE__ */ jsx184(
48926
+ "input",
48927
+ {
48928
+ type: "search",
48929
+ placeholder: "Search chats\u2026",
48930
+ value: historySearch,
48931
+ onChange: (e) => setHistorySearch(e.target.value),
48932
+ autoFocus: true
48933
+ }
48934
+ )
48935
+ ] }),
48936
+ /* @__PURE__ */ jsxs100("div", { className: "acp-history-list", children: [
48937
+ /* @__PURE__ */ jsxs100(
48938
+ "button",
48939
+ {
48940
+ className: "acp-history-item acp-history-item--new",
48941
+ onClick: handleNewChat,
48942
+ children: [
48943
+ /* @__PURE__ */ jsx184(Plus, { size: 13 }),
48944
+ /* @__PURE__ */ jsx184("span", { children: "New chat" })
48945
+ ]
48946
+ }
48947
+ ),
48948
+ filteredSessions.map((session) => /* @__PURE__ */ jsxs100(
48949
+ "button",
48950
+ {
48951
+ className: "acp-history-item",
48952
+ onClick: () => handleSwitchSession(session),
48953
+ children: [
48954
+ /* @__PURE__ */ jsx184(MessageSquare, { size: 13 }),
48955
+ /* @__PURE__ */ jsx184("span", { children: session.title })
48956
+ ]
48957
+ },
48958
+ session.id
48959
+ )),
48960
+ filteredSessions.length === 0 && historySearch && /* @__PURE__ */ jsx184("div", { className: "acp-history-empty", children: "No matching chats" })
48961
+ ] })
48202
48962
  ] })
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
48963
  ] }),
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,
48964
+ /* @__PURE__ */ jsx184("div", { className: "acp-messages-wrap", children: isLoadingSession ? /* @__PURE__ */ jsx184("div", { className: "acp-empty", children: /* @__PURE__ */ jsxs100("div", { className: "acp-loading-session", children: [
48965
+ /* @__PURE__ */ jsxs100("div", { className: "acp-loading-dots", children: [
48966
+ /* @__PURE__ */ jsx184("span", {}),
48967
+ /* @__PURE__ */ jsx184("span", {}),
48968
+ /* @__PURE__ */ jsx184("span", {})
48969
+ ] }),
48970
+ /* @__PURE__ */ jsx184("span", { className: "acp-status-text", children: "Loading chat\u2026" })
48971
+ ] }) }) : messages.length === 0 && !isLoading && !isSendPending ? /* @__PURE__ */ jsxs100("div", { className: "acp-empty", children: [
48972
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-icon", children: /* @__PURE__ */ jsx184(MessageSquare, { size: 22 }) }),
48973
+ /* @__PURE__ */ jsxs100("div", { children: [
48974
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-title", children: "AI Ad Designer" }),
48975
+ /* @__PURE__ */ jsx184("div", { className: "acp-empty-sub", children: "Describe an ad and I'll create it on the canvas. Type @ to reference frames." })
48976
+ ] }),
48977
+ /* @__PURE__ */ jsx184("div", { className: "acp-chips", children: SUGGESTIONS.map((chip) => /* @__PURE__ */ jsx184(
48978
+ "button",
48979
+ {
48980
+ className: "acp-chip",
48981
+ onClick: () => handleChip(chip),
48982
+ children: chip
48983
+ },
48984
+ chip
48985
+ )) })
48986
+ ] }) : /* @__PURE__ */ jsxs100(ChatContainerRoot, { className: "acp-chat-root", children: [
48987
+ /* @__PURE__ */ jsxs100(ChatContainerContent, { className: "acp-chat-content", children: [
48988
+ messages.map((msg, index) => {
48989
+ const isAssistant = msg.role === "assistant";
48990
+ const isLast = index === messages.length - 1;
48991
+ return /* @__PURE__ */ jsx184(
48992
+ Message,
48993
+ {
48994
+ className: `acp-msg ${isAssistant ? "acp-msg--assistant" : "acp-msg--user"}`,
48995
+ children: isAssistant ? /* @__PURE__ */ jsxs100("div", { className: "acp-msg-inner", children: [
48996
+ msg.toolActions && msg.toolActions.length > 0 && /* @__PURE__ */ jsx184(ToolActionsDisplay, { actions: msg.toolActions }),
48997
+ /* @__PURE__ */ jsx184(
48998
+ MessageContent,
48999
+ {
49000
+ markdown: true,
49001
+ className: "acp-content-assistant",
49002
+ children: msg.content
49003
+ }
49004
+ ),
49005
+ /* @__PURE__ */ jsx184(
49006
+ MessageActions,
49007
+ {
49008
+ className: `acp-msg-actions${isLast ? " acp-msg-actions--visible" : ""}`,
49009
+ children: /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Copy", delayDuration: 100, children: /* @__PURE__ */ jsx184(
49010
+ "button",
48248
49011
  {
48249
- tooltip: "Downvote",
48250
- delayDuration: 100,
48251
- children: /* @__PURE__ */ jsx184("button", { className: "acp-action-btn", children: /* @__PURE__ */ jsx184(ThumbsDown, { size: 14 }) })
49012
+ className: "acp-action-btn",
49013
+ onClick: () => handleCopy(msg.id, msg.content),
49014
+ children: copiedMsgId === msg.id ? /* @__PURE__ */ jsx184(Check, { size: 14 }) : /* @__PURE__ */ jsx184(Copy, { size: 14 })
48252
49015
  }
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 }) }) })
49016
+ ) })
49017
+ }
49018
+ )
49019
+ ] }) : /* @__PURE__ */ jsxs100("div", { className: "acp-msg-inner", children: [
49020
+ msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ jsx184("div", { className: "acp-msg-attachments", children: msg.attachments.map(
49021
+ (att, i) => att.type === "image" && att.dataUrl ? /* @__PURE__ */ jsx184(
49022
+ "img",
49023
+ {
49024
+ src: att.dataUrl,
49025
+ alt: att.name,
49026
+ className: "acp-msg-att-thumb",
49027
+ title: att.name
49028
+ },
49029
+ i
49030
+ ) : /* @__PURE__ */ jsxs100("span", { className: "acp-msg-att-pill", children: [
49031
+ /* @__PURE__ */ jsx184(Paperclip, { size: 11 }),
49032
+ att.name
49033
+ ] }, i)
49034
+ ) }),
49035
+ /* @__PURE__ */ jsx184(MessageContent, { className: "acp-content-user", children: msg.content }),
49036
+ /* @__PURE__ */ jsx184(MessageActions, { className: "acp-msg-actions", children: /* @__PURE__ */ jsx184(MessageAction, { tooltip: "Copy", delayDuration: 100, children: /* @__PURE__ */ jsx184(
49037
+ "button",
49038
+ {
49039
+ className: "acp-action-btn",
49040
+ onClick: () => handleCopy(msg.id, msg.content),
49041
+ children: copiedMsgId === msg.id ? /* @__PURE__ */ jsx184(Check, { size: 14 }) : /* @__PURE__ */ jsx184(Copy, { size: 14 })
49042
+ }
49043
+ ) }) })
49044
+ ] })
49045
+ },
49046
+ msg.id
49047
+ );
49048
+ }),
49049
+ 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: [
49050
+ /* @__PURE__ */ jsxs100("div", { className: "acp-loading-dots", children: [
49051
+ /* @__PURE__ */ jsx184("span", {}),
49052
+ /* @__PURE__ */ jsx184("span", {}),
49053
+ /* @__PURE__ */ jsx184("span", {})
49054
+ ] }),
49055
+ statusText && /* @__PURE__ */ jsx184("span", { className: "acp-status-text", children: statusText })
49056
+ ] }) }) })
49057
+ ] }),
49058
+ /* @__PURE__ */ jsx184("div", { className: "acp-scroll-anchor", children: /* @__PURE__ */ jsx184(ScrollButton, { className: "acp-scroll-btn" }) })
49059
+ ] }) }),
49060
+ error && /* @__PURE__ */ jsx184("div", { className: "acp-error", children: error }),
49061
+ frameRef && /* @__PURE__ */ jsx184("div", { className: "acp-ref-area", children: /* @__PURE__ */ jsxs100("div", { className: "acp-ref-pill", children: [
49062
+ /* @__PURE__ */ jsx184(AtSign, { size: 12 }),
49063
+ /* @__PURE__ */ jsx184("span", { children: frameRef.label }),
49064
+ /* @__PURE__ */ jsx184(
49065
+ "button",
49066
+ {
49067
+ className: "acp-ref-remove",
49068
+ onClick: handleRemoveRef,
49069
+ title: "Remove reference",
49070
+ children: /* @__PURE__ */ jsx184(X2, { size: 10 })
49071
+ }
49072
+ )
49073
+ ] }) }),
49074
+ attachedFiles.length > 0 && /* @__PURE__ */ jsx184("div", { className: "acp-attachments-area", children: attachedFiles.map((f) => /* @__PURE__ */ jsxs100("div", { className: "acp-attachment-pill", children: [
49075
+ f.type === "image" && f.dataUrl ? /* @__PURE__ */ jsx184(
49076
+ "img",
49077
+ {
49078
+ src: f.dataUrl,
49079
+ alt: f.name,
49080
+ className: "acp-attachment-thumb"
49081
+ }
49082
+ ) : /* @__PURE__ */ jsx184(Paperclip, { size: 12 }),
49083
+ /* @__PURE__ */ jsx184("span", { children: f.name }),
49084
+ /* @__PURE__ */ jsx184(
49085
+ "button",
49086
+ {
49087
+ className: "acp-attachment-remove",
49088
+ onClick: () => handleRemoveFile(f.id),
49089
+ title: "Remove file",
49090
+ children: /* @__PURE__ */ jsx184(X2, { size: 10 })
49091
+ }
49092
+ )
49093
+ ] }, f.id)) }),
49094
+ /* @__PURE__ */ jsx184("div", { className: "acp-input-area", children: /* @__PURE__ */ jsxs100("div", { className: "acp-input-wrapper", ref: mentionMenuRef, children: [
49095
+ mentionMenuOpen && /* @__PURE__ */ jsx184("div", { className: "acp-mention-menu", children: availableFrames.length > 0 ? availableFrames.map((f) => /* @__PURE__ */ jsxs100(
49096
+ "button",
49097
+ {
49098
+ className: "acp-mention-item",
49099
+ onClick: () => handlePickFrame(f),
49100
+ children: [
49101
+ /* @__PURE__ */ jsx184(AtSign, { size: 14 }),
49102
+ /* @__PURE__ */ jsxs100("span", { children: [
49103
+ f.name,
49104
+ " ",
49105
+ /* @__PURE__ */ jsxs100("span", { className: "acp-mention-dim", children: [
49106
+ "(",
49107
+ f.width,
49108
+ "x",
49109
+ f.height,
49110
+ ", ",
49111
+ f.childCount,
49112
+ " items)"
48263
49113
  ] })
48264
49114
  ] })
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
- )
49115
+ ]
49116
+ },
49117
+ f.id
49118
+ )) : /* @__PURE__ */ jsx184("div", { className: "acp-mention-empty", children: "No frames on the canvas. Create a frame first, then type @ to reference it." }) }),
49119
+ /* @__PURE__ */ jsx184(
49120
+ "input",
49121
+ {
49122
+ ref: fileInputRef,
49123
+ type: "file",
49124
+ multiple: true,
49125
+ accept: "image/*,.txt,.json,.csv,.md,.html,.css,.js,.ts,.tsx,.jsx,.py,.xml",
49126
+ onChange: handleFileUpload,
49127
+ style: { display: "none" }
49128
+ }
49129
+ ),
49130
+ /* @__PURE__ */ jsxs100(
49131
+ PromptInput,
49132
+ {
49133
+ isLoading,
49134
+ value: prompt,
49135
+ onValueChange: handlePromptChange,
49136
+ onSubmit: () => handleSend(),
49137
+ className: "acp-prompt-input",
49138
+ children: [
49139
+ /* @__PURE__ */ jsx184(
49140
+ PromptInputTextarea,
49141
+ {
49142
+ placeholder: excalidrawAPI ? "Describe an ad to create\u2026 (@ for frames)" : "Ask anything",
49143
+ className: "acp-textarea"
49144
+ }
49145
+ ),
49146
+ /* @__PURE__ */ jsxs100(PromptInputActions, { className: "acp-actions-bar", children: [
49147
+ /* @__PURE__ */ jsxs100("div", { className: "acp-actions-left", children: [
49148
+ /* @__PURE__ */ jsx184(PromptInputAction, { tooltip: "Attach files (max 3)", children: /* @__PURE__ */ jsx184(
49149
+ "button",
49150
+ {
49151
+ className: "acp-pill-btn acp-pill-btn--icon",
49152
+ onClick: () => fileInputRef.current?.click(),
49153
+ disabled: attachedFiles.length >= MAX_ATTACHED_FILES,
49154
+ children: /* @__PURE__ */ jsx184(Paperclip, { size: 16 })
49155
+ }
49156
+ ) }),
49157
+ /* @__PURE__ */ jsx184(
49158
+ PromptInputAction,
49159
+ {
49160
+ tooltip: webSearchEnabled ? "Web search enabled" : "Enable web search",
49161
+ children: /* @__PURE__ */ jsxs100(
49162
+ "button",
49163
+ {
49164
+ className: `acp-pill-btn${webSearchEnabled ? " acp-pill-btn--active" : ""}`,
49165
+ onClick: toggleWebSearch,
49166
+ children: [
49167
+ /* @__PURE__ */ jsx184(Globe, { size: 15 }),
49168
+ /* @__PURE__ */ jsx184("span", { children: "Search" })
49169
+ ]
49170
+ }
49171
+ )
49172
+ }
49173
+ )
49174
+ ] }),
49175
+ /* @__PURE__ */ jsxs100("div", { className: "acp-actions-right", children: [
49176
+ /* @__PURE__ */ jsx184(
49177
+ PromptInputAction,
49178
+ {
49179
+ tooltip: isTranscribing ? "Transcribing\u2026" : isRecording ? "Stop recording" : "Voice input",
49180
+ children: /* @__PURE__ */ jsx184(
49181
+ "button",
49182
+ {
49183
+ className: `acp-pill-btn acp-pill-btn--icon${isRecording ? " acp-pill-btn--recording" : ""}${isTranscribing ? " acp-pill-btn--transcribing" : ""}`,
49184
+ onClick: handleVoiceInput,
49185
+ disabled: isTranscribing,
49186
+ children: isTranscribing ? /* @__PURE__ */ jsx184(
49187
+ "svg",
49188
+ {
49189
+ width: "16",
49190
+ height: "16",
49191
+ viewBox: "0 0 24 24",
49192
+ fill: "none",
49193
+ stroke: "currentColor",
49194
+ strokeWidth: "2",
49195
+ strokeLinecap: "round",
49196
+ strokeLinejoin: "round",
49197
+ className: "acp-mic-spinner",
49198
+ children: /* @__PURE__ */ jsx184("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
49199
+ }
49200
+ ) : isRecording ? /* @__PURE__ */ jsx184(MicOff, { size: 16 }) : /* @__PURE__ */ jsx184(Mic, { size: 16 })
49201
+ }
49202
+ )
49203
+ }
49204
+ ),
49205
+ /* @__PURE__ */ jsx184(
49206
+ "button",
49207
+ {
49208
+ className: "acp-send-btn",
49209
+ onClick: isLoading ? handleStop : () => handleSend(),
49210
+ disabled: !isLoading && (!prompt.trim() || isSendPending),
49211
+ title: isLoading ? "Stop" : "Send",
49212
+ children: isLoading ? /* @__PURE__ */ jsx184("span", { className: "acp-stop-square" }) : /* @__PURE__ */ jsx184(ArrowUp3, { size: 16 })
49213
+ }
49214
+ )
49215
+ ] })
48363
49216
  ] })
48364
- ] })
48365
- ]
48366
- }
48367
- )
48368
- ] }) })
48369
- ] })
48370
- }
48371
- );
48372
- };
49217
+ ]
49218
+ }
49219
+ )
49220
+ ] }) })
49221
+ ] })
49222
+ }
49223
+ );
49224
+ }
49225
+ );
49226
+ AIChatPanel.displayName = "AIChatPanel";
48373
49227
  function ToolActionsDisplay({ actions: actions2 }) {
48374
49228
  const [expanded, setExpanded] = useState49(false);
48375
49229
  return /* @__PURE__ */ jsxs100("div", { className: "acp-tool-actions", children: [
@@ -48409,7 +49263,37 @@ function ToolActionsDisplay({ actions: actions2 }) {
48409
49263
  )) })
48410
49264
  ] });
48411
49265
  }
48412
- async function callPlainChatAPI(messages, apiKey, signal) {
49266
+ async function callPlainChatAPI(messages, apiKey, signal, opts) {
49267
+ const model = opts?.webSearchEnabled ? "openai/gpt-4.1-mini:online" : "openai/gpt-4.1-mini";
49268
+ const apiMessages = messages.map((m, i) => {
49269
+ const isLast = i === messages.length - 1;
49270
+ if (isLast && m.role === "user" && opts?.attachments?.length) {
49271
+ const content2 = [];
49272
+ for (const att of opts.attachments) {
49273
+ if (att.type === "image" && att.dataUrl) {
49274
+ content2.push({
49275
+ type: "image_url",
49276
+ image_url: { url: att.dataUrl }
49277
+ });
49278
+ }
49279
+ }
49280
+ let text = m.content;
49281
+ const textFiles = opts.attachments.filter((a) => a.type === "text");
49282
+ if (textFiles.length > 0) {
49283
+ text += `
49284
+
49285
+ ${textFiles.map((f) => `[File: ${f.name}]
49286
+ ${f.textContent}`).join("\n\n")}`;
49287
+ }
49288
+ content2.push({ type: "text", text });
49289
+ return { role: m.role, content: content2 };
49290
+ }
49291
+ return { role: m.role, content: m.content };
49292
+ });
49293
+ const body = { model, messages: apiMessages };
49294
+ if (opts?.webSearchEnabled) {
49295
+ body.plugins = [{ id: "web", max_results: 5 }];
49296
+ }
48413
49297
  const response = await fetch(
48414
49298
  "https://openrouter.ai/api/v1/chat/completions",
48415
49299
  {
@@ -48419,10 +49303,7 @@ async function callPlainChatAPI(messages, apiKey, signal) {
48419
49303
  Authorization: `Bearer ${apiKey}`,
48420
49304
  "Content-Type": "application/json"
48421
49305
  },
48422
- body: JSON.stringify({
48423
- model: "openai/gpt-4.1-mini",
48424
- messages: messages.map((m) => ({ role: m.role, content: m.content }))
48425
- })
49306
+ body: JSON.stringify(body)
48426
49307
  }
48427
49308
  );
48428
49309
  if (!response.ok) {
@@ -48482,7 +49363,10 @@ var ExcalidrawBase = (props) => {
48482
49363
  renderEmbeddable,
48483
49364
  aiEnabled,
48484
49365
  showDeprecatedFonts,
48485
- renderScrollbars
49366
+ geminiApiKey,
49367
+ renderScrollbars,
49368
+ onBeforeImageGen,
49369
+ onAfterImageGen
48486
49370
  } = props;
48487
49371
  const canvasActions = props.UIOptions?.canvasActions;
48488
49372
  const UIOptions = {
@@ -48554,7 +49438,10 @@ var ExcalidrawBase = (props) => {
48554
49438
  renderEmbeddable,
48555
49439
  aiEnabled: aiEnabled !== false,
48556
49440
  showDeprecatedFonts,
49441
+ geminiApiKey,
48557
49442
  renderScrollbars,
49443
+ onBeforeImageGen,
49444
+ onAfterImageGen,
48558
49445
  children
48559
49446
  }
48560
49447
  ) }) });
@@ -48668,7 +49555,7 @@ export {
48668
49555
  useHandleLibrary,
48669
49556
  useI18n,
48670
49557
  useStylesPanelMode,
48671
- viewportCoordsToSceneCoords4 as viewportCoordsToSceneCoords,
49558
+ viewportCoordsToSceneCoords5 as viewportCoordsToSceneCoords,
48672
49559
  zoomToFitBounds
48673
49560
  };
48674
49561
  //# sourceMappingURL=index.js.map