@mjml-agent-editor/editor 0.1.12 → 0.1.15

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.
@@ -18,7 +18,7 @@ import {
18
18
  } from "./chunk-V6RR4VRD.js";
19
19
 
20
20
  // src/stores.tsx
21
- import { createContext, useContext, useMemo } from "react";
21
+ import { createContext, useContext, useMemo, useState } from "react";
22
22
 
23
23
  // src/labels.ts
24
24
  var DEFAULT_LABELS = {
@@ -49,6 +49,29 @@ var DEFAULT_LABELS = {
49
49
  rteBackgroundColor: "Background color",
50
50
  documentLoadFailed: "Failed to load the document.",
51
51
  documentSaveFailed: "Failed to save changes.",
52
+ retry: "Retry",
53
+ chooseImage: "Choose image",
54
+ imagePickerTitle: "Choose an image",
55
+ imagePickerDrop: "Drag images here, or",
56
+ imagePickerBrowse: "Browse files",
57
+ imagePickerUrlPlaceholder: "\u2026or paste an image URL",
58
+ imagePickerUseUrl: "Use URL",
59
+ imagePickerGallery: "Your images",
60
+ imagePickerEmpty: "No images yet \u2014 the ones you upload will be kept here.",
61
+ imagePickerQuota: (used, limit) => `${used} of ${limit} uploads used`,
62
+ imagePickerQuotaFull: "Upload limit reached \u2014 pick an image below or paste a URL.",
63
+ imagePickerDelete: "Delete image",
64
+ imagePickerConfirmDelete: "Click again to delete",
65
+ imagePickerLoadFailed: "Failed to load your images.",
66
+ imageUploadFailed: "Failed to upload the image.",
67
+ imageDeleteFailed: "Failed to delete the image.",
68
+ imagePickerUnsupported: (fileName) => `${fileName} is not a supported image type.`,
69
+ imageFieldRemove: "Remove image",
70
+ colorPickerOpen: "Pick a color",
71
+ colorPickerEyedropper: "Pick from the screen",
72
+ colorPickerDocument: "In this email",
73
+ colorPickerPresets: "Presets",
74
+ colorPickerTransparent: "Transparent",
52
75
  commentOnSection: "Comment on the whole section",
53
76
  closeComments: "Close",
54
77
  noCommentsYet: "No comments yet \u2014 add the first one.",
@@ -60,6 +83,7 @@ var DEFAULT_LABELS = {
60
83
  agentWorking: "Agent is working\u2026",
61
84
  reasoning: "Reasoning",
62
85
  completedSteps: (count, failed) => failed > 0 ? `Completed steps (${count}, ${failed} failed)` : `Completed steps (${count})`,
86
+ stepLimitReached: "The agent ran out of steps for this turn. Ask it to continue.",
63
87
  applyComments: "Apply changes from comments",
64
88
  chatPlaceholder: "Describe what you want to change\u2026 (Enter = send, Shift+Enter = new line)",
65
89
  stop: "Stop",
@@ -90,13 +114,27 @@ function mergeLabels(overrides) {
90
114
  import { jsx } from "react/jsx-runtime";
91
115
  var StoreContext = createContext(null);
92
116
  var LabelContext = createContext(null);
117
+ var TurnContext = createContext({
118
+ running: false,
119
+ setRunning: () => {
120
+ },
121
+ locked: false,
122
+ setLocked: () => {
123
+ }
124
+ });
93
125
  function EditorStoreProvider({
94
126
  stores,
95
127
  labels,
96
128
  children
97
129
  }) {
98
130
  const merged = useMemo(() => mergeLabels(labels), [labels]);
99
- return /* @__PURE__ */ jsx(StoreContext.Provider, { value: stores, children: /* @__PURE__ */ jsx(LabelContext.Provider, { value: merged, children }) });
131
+ const [running, setRunning] = useState(false);
132
+ const [locked, setLocked] = useState(false);
133
+ const turn = useMemo(() => ({ running, setRunning, locked, setLocked }), [running, locked]);
134
+ return /* @__PURE__ */ jsx(StoreContext.Provider, { value: stores, children: /* @__PURE__ */ jsx(LabelContext.Provider, { value: merged, children: /* @__PURE__ */ jsx(TurnContext.Provider, { value: turn, children }) }) });
135
+ }
136
+ function useAgentTurn() {
137
+ return useContext(TurnContext);
100
138
  }
101
139
  function useLabels() {
102
140
  return useContext(LabelContext) ?? mergeLabels();
@@ -116,12 +154,15 @@ function useDocumentStore() {
116
154
  function useCommentStore() {
117
155
  return useStores().comments;
118
156
  }
157
+ function useImageLibrary() {
158
+ return useStores().images;
159
+ }
119
160
 
120
161
  // src/index.ts
121
162
  import { STARTER_MJML } from "@mjml-agent-editor/core";
122
163
 
123
164
  // src/components/editor/EditorHeader.tsx
124
- import { useEffect, useState } from "react";
165
+ import { useEffect, useState as useState2 } from "react";
125
166
  import { toast } from "sonner";
126
167
  import {
127
168
  Undo2,
@@ -420,6 +461,22 @@ function DialogTitle({ className, ...props }) {
420
461
  }
421
462
  );
422
463
  }
464
+ function DialogDescription({
465
+ className,
466
+ ...props
467
+ }) {
468
+ return /* @__PURE__ */ jsx6(
469
+ DialogPrimitive.Description,
470
+ {
471
+ "data-slot": "dialog-description",
472
+ className: cn(
473
+ "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
474
+ className
475
+ ),
476
+ ...props
477
+ }
478
+ );
479
+ }
423
480
 
424
481
  // src/components/editor/EditorHeader.tsx
425
482
  import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -465,17 +522,18 @@ function EditorHeader({
465
522
  }) {
466
523
  const documents = useDocumentStore();
467
524
  const labels = useLabels();
468
- const [name, setName] = useState("");
469
- const [state, setState] = useState({
525
+ const [name, setName] = useState2("");
526
+ const [state, setState] = useState2({
470
527
  device: "Desktop",
471
528
  canUndo: false,
472
529
  canRedo: false,
473
530
  saveStatus: "idle",
474
531
  zoom: 100,
475
- contentWidth: "600px"
532
+ contentWidth: "600px",
533
+ readOnly: false
476
534
  });
477
- const [code, setCode] = useState({ mjml: "", html: "" });
478
- const [preview, setPreview] = useState("");
535
+ const [code, setCode] = useState2({ mjml: "", html: "" });
536
+ const [preview, setPreview] = useState2("");
479
537
  useEffect(() => {
480
538
  documents.get(docId).then((d) => setName(d.name)).catch(() => {
481
539
  });
@@ -569,6 +627,7 @@ function EditorHeader({
569
627
  {
570
628
  variant: "ghost",
571
629
  size: "sm",
630
+ disabled: state.readOnly,
572
631
  className: cn(darkGhost, "gap-1.5 rounded-md border border-panel-border max-xl:hidden"),
573
632
  children: [
574
633
  /* @__PURE__ */ jsx7("span", { className: "text-panel-muted-fg", children: labels.contentWidth }),
@@ -801,7 +860,7 @@ function EditorHeader({
801
860
  // src/components/chat/ChatPanel.tsx
802
861
  import { useChat } from "@ai-sdk/react";
803
862
  import { DefaultChatTransport } from "ai";
804
- import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
863
+ import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
805
864
  import { toast as toast2 } from "sonner";
806
865
  import {
807
866
  Brain,
@@ -815,6 +874,27 @@ import {
815
874
  ChevronRight
816
875
  } from "lucide-react";
817
876
 
877
+ // src/lib/refusal.ts
878
+ function refusal(error) {
879
+ if (!(error instanceof Error)) return null;
880
+ const status = error.status;
881
+ const deliberate = typeof status === "number" ? status >= 400 && status < 500 : null;
882
+ if (deliberate === false) return null;
883
+ if (error.message.startsWith("{")) {
884
+ try {
885
+ const parsed = JSON.parse(error.message);
886
+ return typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : null;
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ const message = error.message.trim();
892
+ return deliberate && message !== "" && !message.startsWith("<") ? error.message : null;
893
+ }
894
+ function saveToastId(docId) {
895
+ return `document-save-${docId}`;
896
+ }
897
+
818
898
  // src/components/ui/message.tsx
819
899
  import "react";
820
900
  import { jsx as jsx8 } from "react/jsx-runtime";
@@ -1199,16 +1279,6 @@ function toBlocks(parts) {
1199
1279
  }
1200
1280
  return blocks;
1201
1281
  }
1202
- function refusal(error) {
1203
- const body = error instanceof Error ? error.message : "";
1204
- if (!body.startsWith("{")) return null;
1205
- try {
1206
- const parsed = JSON.parse(body);
1207
- return typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : null;
1208
- } catch {
1209
- return null;
1210
- }
1211
- }
1212
1282
  function ToolMarker({ tool }) {
1213
1283
  const labels = useLabels();
1214
1284
  const name = tool.type.slice(5);
@@ -1270,8 +1340,10 @@ function ChatPanel({
1270
1340
  onSectionEditEnd
1271
1341
  }) {
1272
1342
  const labels = useLabels();
1273
- const [input, setInput] = useState2("");
1343
+ const [input, setInput] = useState3("");
1274
1344
  const taRef = useRef(null);
1345
+ const sending = useRef(false);
1346
+ const [flushing, setFlushing] = useState3(false);
1275
1347
  const times = useRef(/* @__PURE__ */ new Map());
1276
1348
  const timeFor = (id) => {
1277
1349
  if (!times.current.has(id)) {
@@ -1335,6 +1407,9 @@ function ChatPanel({
1335
1407
  }
1336
1408
  }, [messages, onLiveUpdate, onSectionEditStart, onSectionEditEnd]);
1337
1409
  const busy = status === "submitted" || status === "streaming";
1410
+ const { setRunning, locked } = useAgentTurn();
1411
+ useEffect2(() => setRunning(busy || flushing), [busy, flushing, setRunning]);
1412
+ useEffect2(() => () => setRunning(false), [setRunning]);
1338
1413
  const lastMsg = messages[messages.length - 1];
1339
1414
  const toolRunning = lastMsg?.role === "assistant" && lastMsg.parts.some((p) => {
1340
1415
  if (!p.type.startsWith("tool-")) return false;
@@ -1349,8 +1424,19 @@ function ChatPanel({
1349
1424
  };
1350
1425
  const send = async (text) => {
1351
1426
  const trimmed = text.trim();
1352
- if (!trimmed || busy) return;
1353
- await onBeforeSend().catch(console.error);
1427
+ if (!trimmed || busy || locked || sending.current) return;
1428
+ sending.current = true;
1429
+ setFlushing(true);
1430
+ try {
1431
+ await onBeforeSend();
1432
+ } catch (error) {
1433
+ console.error(error);
1434
+ toast2.error(refusal(error) ?? labels.documentSaveFailed, { id: saveToastId(docId) });
1435
+ return;
1436
+ } finally {
1437
+ sending.current = false;
1438
+ setFlushing(false);
1439
+ }
1354
1440
  void sendMessage({ text: trimmed });
1355
1441
  setInput("");
1356
1442
  if (taRef.current) taRef.current.style.height = "auto";
@@ -1367,6 +1453,7 @@ function ChatPanel({
1367
1453
  const isLast = mi === messages.length - 1;
1368
1454
  const blocks = toBlocks(message.parts);
1369
1455
  const hasText = blocks.some((block) => block.kind === "text");
1456
+ const metadata = message.metadata;
1370
1457
  return /* @__PURE__ */ jsx14(MessageScrollerItem, { messageId: message.id, children: /* @__PURE__ */ jsxs4("div", { className: cn("flex w-full gap-2", isUser && "flex-row-reverse"), children: [
1371
1458
  !isUser && /* @__PURE__ */ jsx14("span", { className: "mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-brand/15 text-brand", children: /* @__PURE__ */ jsx14(Sparkles, { className: "size-3.5" }) }),
1372
1459
  /* @__PURE__ */ jsxs4(
@@ -1415,7 +1502,11 @@ function ChatPanel({
1415
1502
  i
1416
1503
  );
1417
1504
  }),
1418
- isLast && !isUser && busy && !toolRunning && !hasText && /* @__PURE__ */ jsx14(BusyMarker, {})
1505
+ isLast && !isUser && busy && !toolRunning && !hasText && /* @__PURE__ */ jsx14(BusyMarker, {}),
1506
+ metadata?.stepLimitReached && /* @__PURE__ */ jsxs4(Marker, { className: "text-panel-muted-fg", children: [
1507
+ /* @__PURE__ */ jsx14(MarkerIcon, { children: /* @__PURE__ */ jsx14(TriangleAlert, {}) }),
1508
+ /* @__PURE__ */ jsx14(MarkerContent, { children: labels.stepLimitReached })
1509
+ ] })
1419
1510
  ] }) }),
1420
1511
  blocks.length > 0 && /* @__PURE__ */ jsx14("span", { className: "px-1 text-[10px] text-panel-muted-fg", children: timeFor(message.id) })
1421
1512
  ]
@@ -1432,7 +1523,7 @@ function ChatPanel({
1432
1523
  Button,
1433
1524
  {
1434
1525
  className: "mb-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
1435
- disabled: busy,
1526
+ disabled: busy || flushing || locked,
1436
1527
  onClick: () => void send(APPLY_COMMENTS_PROMPT),
1437
1528
  children: [
1438
1529
  /* @__PURE__ */ jsx14(Sparkles, {}),
@@ -1491,7 +1582,7 @@ function ChatPanel({
1491
1582
  {
1492
1583
  type: "submit",
1493
1584
  size: "sm",
1494
- disabled: busy || !input.trim(),
1585
+ disabled: busy || flushing || locked || !input.trim(),
1495
1586
  className: "bg-brand text-brand-fg hover:bg-brand/90",
1496
1587
  children: [
1497
1588
  /* @__PURE__ */ jsx14(Send, {}),
@@ -1510,7 +1601,7 @@ function ChatPanel({
1510
1601
 
1511
1602
  // src/components/comments/CanvasComments.tsx
1512
1603
  import { useEditorMaybe } from "@grapesjs/react";
1513
- import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
1604
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
1514
1605
  import { Check as Check2, MessageSquarePlus, X as X2 } from "lucide-react";
1515
1606
  import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs5 } from "react/jsx-runtime";
1516
1607
  var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
@@ -1526,11 +1617,11 @@ function CanvasComments({
1526
1617
  const labels = useLabels();
1527
1618
  const editor = useEditorMaybe();
1528
1619
  const containerRef = useRef2(null);
1529
- const [comments, setComments] = useState3([]);
1530
- const [positions, setPositions] = useState3({});
1531
- const [activeKey, setActiveKey] = useState3(null);
1532
- const [composeInfo, setComposeInfo] = useState3(null);
1533
- const [body, setBody] = useState3("");
1620
+ const [comments, setComments] = useState4([]);
1621
+ const [positions, setPositions] = useState4({});
1622
+ const [activeKey, setActiveKey] = useState4(null);
1623
+ const [composeInfo, setComposeInfo] = useState4(null);
1624
+ const [body, setBody] = useState4("");
1534
1625
  const reload = useCallback(() => {
1535
1626
  commentStore.list(docId).then(setComments).catch(console.error);
1536
1627
  }, [docId]);
@@ -1779,13 +1870,26 @@ export {
1779
1870
  DEFAULT_LABELS,
1780
1871
  mergeLabels,
1781
1872
  EditorStoreProvider,
1873
+ useAgentTurn,
1782
1874
  useLabels,
1783
1875
  useDocumentStore,
1784
1876
  useCommentStore,
1877
+ useImageLibrary,
1878
+ Popover,
1879
+ PopoverTrigger,
1880
+ PopoverAnchor,
1881
+ PopoverContent,
1882
+ Dialog,
1883
+ DialogContent,
1884
+ DialogHeader,
1885
+ DialogTitle,
1886
+ DialogDescription,
1785
1887
  EditorHeader,
1888
+ refusal,
1889
+ saveToastId,
1786
1890
  Markdown,
1787
1891
  ChatPanel,
1788
1892
  CanvasComments,
1789
1893
  STARTER_MJML
1790
1894
  };
1791
- //# sourceMappingURL=chunk-4N2OJT22.js.map
1895
+ //# sourceMappingURL=chunk-3RCKZYM4.js.map