@mjml-agent-editor/editor 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -118,6 +118,7 @@ var DEFAULT_LABELS = {
118
118
  stop: "Stop",
119
119
  send: "Send",
120
120
  chatError: "Agent chat error.",
121
+ agentUnavailable: "The agent is unavailable right now. You can still edit the email and leave comments.",
121
122
  toolLabels: {
122
123
  get_document: "Reading document",
123
124
  get_section: "Reading section",
@@ -1077,6 +1078,11 @@ function saveToastId(docId) {
1077
1078
  return `document-save-${docId}`;
1078
1079
  }
1079
1080
 
1081
+ // src/lib/turn-gate.ts
1082
+ function canStartTurn(text, gate) {
1083
+ return !gate.unavailable && !gate.busy && !gate.flushing && !gate.locked && text.trim() !== "";
1084
+ }
1085
+
1080
1086
  // src/components/ui/textarea.tsx
1081
1087
  var React11 = require("react");
1082
1088
  var import_jsx_runtime14 = require("react/jsx-runtime");
@@ -1606,6 +1612,7 @@ function ChatPanel({
1606
1612
  initialMessages,
1607
1613
  transport,
1608
1614
  resume,
1615
+ agentUnavailable = false,
1609
1616
  onBeforeSend,
1610
1617
  onAgentFinish,
1611
1618
  onLiveUpdate,
@@ -1613,6 +1620,9 @@ function ChatPanel({
1613
1620
  onSectionEditEnd
1614
1621
  }) {
1615
1622
  const labels = useLabels();
1623
+ const unavailableId = (0, import_react5.useId)();
1624
+ const footerRef = (0, import_react5.useRef)(null);
1625
+ const statusRef = (0, import_react5.useRef)(null);
1616
1626
  const [input, setInput] = (0, import_react5.useState)("");
1617
1627
  const taRef = (0, import_react5.useRef)(null);
1618
1628
  const sending = (0, import_react5.useRef)(false);
@@ -1681,6 +1691,7 @@ function ChatPanel({
1681
1691
  }, [messages, onLiveUpdate, onSectionEditStart, onSectionEditEnd]);
1682
1692
  const busy = status === "submitted" || status === "streaming";
1683
1693
  const { setRunning, locked } = useAgentTurn();
1694
+ const gate = { unavailable: agentUnavailable, busy, flushing, locked };
1684
1695
  (0, import_react5.useEffect)(() => setRunning(busy || flushing), [busy, flushing, setRunning]);
1685
1696
  (0, import_react5.useEffect)(() => () => setRunning(false), [setRunning]);
1686
1697
  const lastMsg = messages[messages.length - 1];
@@ -1695,24 +1706,40 @@ function ChatPanel({
1695
1706
  el.style.height = "auto";
1696
1707
  el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
1697
1708
  };
1709
+ (0, import_react5.useLayoutEffect)(() => {
1710
+ const active = document.activeElement;
1711
+ if (agentUnavailable && active?.matches(":disabled") && footerRef.current?.contains(active)) {
1712
+ statusRef.current?.focus();
1713
+ } else if (!agentUnavailable && active === statusRef.current) {
1714
+ taRef.current?.focus();
1715
+ }
1716
+ }, [agentUnavailable]);
1717
+ const [pending, setPending] = (0, import_react5.useState)(null);
1718
+ (0, import_react5.useEffect)(() => {
1719
+ if (pending === null) return;
1720
+ setPending(null);
1721
+ sending.current = false;
1722
+ setFlushing(false);
1723
+ if (agentUnavailable || pending.docId !== docId) return;
1724
+ void sendMessage({ text: pending.text });
1725
+ setInput("");
1726
+ if (taRef.current) taRef.current.style.height = "auto";
1727
+ }, [pending, agentUnavailable, docId, sendMessage]);
1698
1728
  const send = async (text) => {
1729
+ if (!canStartTurn(text, gate) || sending.current) return;
1699
1730
  const trimmed = text.trim();
1700
- if (!trimmed || busy || locked || sending.current) return;
1701
1731
  sending.current = true;
1702
1732
  setFlushing(true);
1703
1733
  try {
1704
1734
  await onBeforeSend();
1705
1735
  } catch (error) {
1736
+ sending.current = false;
1737
+ setFlushing(false);
1706
1738
  console.error(error);
1707
1739
  import_sonner2.toast.error(refusal(error) ?? labels.documentSaveFailed, { id: saveToastId(docId) });
1708
1740
  return;
1709
- } finally {
1710
- sending.current = false;
1711
- setFlushing(false);
1712
1741
  }
1713
- void sendMessage({ text: trimmed });
1714
- setInput("");
1715
- if (taRef.current) taRef.current.style.height = "auto";
1742
+ setPending({ text: trimmed, docId });
1716
1743
  };
1717
1744
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex h-full flex-col", children: [
1718
1745
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(MessageScroller, { className: "flex-1", children: [
@@ -1791,12 +1818,17 @@ function ChatPanel({
1791
1818
  ] }) }),
1792
1819
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(MessageScrollerButton, { direction: "end" })
1793
1820
  ] }) }),
1794
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "border-t border-panel-border p-3", children: [
1821
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { ref: footerRef, className: "border-t border-panel-border p-3", children: [
1822
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { ref: statusRef, id: unavailableId, role: "status", tabIndex: -1, className: "rounded-lg", children: agentUnavailable && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { className: "mb-2 flex items-start gap-2 rounded-lg border border-panel-border bg-panel-elevated/60 px-3 py-2 text-xs text-panel-fg", children: [
1823
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react5.CirclePause, { className: "mt-px size-3.5 shrink-0" }),
1824
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: labels.agentUnavailable })
1825
+ ] }) }),
1795
1826
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
1796
1827
  Button,
1797
1828
  {
1798
1829
  className: "mb-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
1799
- disabled: busy || flushing || locked,
1830
+ disabled: !canStartTurn(APPLY_COMMENTS_PROMPT, gate),
1831
+ "aria-describedby": agentUnavailable ? unavailableId : void 0,
1800
1832
  onClick: () => void send(APPLY_COMMENTS_PROMPT),
1801
1833
  children: [
1802
1834
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react5.Sparkles, {}),
@@ -1819,8 +1851,10 @@ function ChatPanel({
1819
1851
  {
1820
1852
  ref: taRef,
1821
1853
  rows: 2,
1822
- className: "max-h-40 min-h-14 resize-none border-0 bg-transparent px-3 pt-2.5 text-panel-fg shadow-none placeholder:text-panel-muted-fg focus-visible:ring-0",
1823
- placeholder: labels.chatPlaceholder,
1854
+ className: "max-h-40 min-h-14 resize-none border-0 bg-transparent px-3 pt-2.5 text-panel-fg shadow-none placeholder:text-panel-muted-fg focus-visible:ring-0 disabled:bg-transparent",
1855
+ placeholder: agentUnavailable ? void 0 : labels.chatPlaceholder,
1856
+ disabled: agentUnavailable,
1857
+ "aria-describedby": agentUnavailable ? unavailableId : void 0,
1824
1858
  value: input,
1825
1859
  onChange: (e) => {
1826
1860
  setInput(e.target.value);
@@ -1855,7 +1889,8 @@ function ChatPanel({
1855
1889
  {
1856
1890
  type: "submit",
1857
1891
  size: "sm",
1858
- disabled: busy || flushing || locked || !input.trim(),
1892
+ disabled: !canStartTurn(input, gate),
1893
+ "aria-describedby": agentUnavailable ? unavailableId : void 0,
1859
1894
  className: "bg-brand text-brand-fg hover:bg-brand/90",
1860
1895
  children: [
1861
1896
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react5.Send, {}),