best-email-builder 0.9.0 → 0.10.0

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 (67) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +4 -2
  3. package/dist/ai-server.js +21 -32
  4. package/dist/ai-server.js.map +1 -1
  5. package/dist/ai.js +309 -254
  6. package/dist/ai.js.map +1 -1
  7. package/dist/builder.js +8 -9
  8. package/dist/{chunk-VJKF6SR4.js → chunk-24E4GOHU.js} +464 -1505
  9. package/dist/chunk-24E4GOHU.js.map +1 -0
  10. package/dist/chunk-7G7C6G3A.js +837 -0
  11. package/dist/chunk-7G7C6G3A.js.map +1 -0
  12. package/dist/{chunk-G4U6PSF6.js → chunk-A6VTFUSW.js} +2 -2
  13. package/dist/{chunk-P7ABBLX6.js → chunk-KTTVBNOU.js} +2 -2
  14. package/dist/{chunk-AS2EPMA6.js → chunk-M4R53NYT.js} +158 -36
  15. package/dist/chunk-M4R53NYT.js.map +1 -0
  16. package/dist/{chunk-62HBUBBI.js → chunk-MBZI6NVP.js} +31 -732
  17. package/dist/chunk-MBZI6NVP.js.map +1 -0
  18. package/dist/{chunk-LUM7J5FF.js → chunk-OLTD3S3N.js} +2 -2
  19. package/dist/{chunk-FFSSAMHH.js → chunk-SCQKWAXR.js} +7 -7
  20. package/dist/{chunk-GWXY3MWQ.js → chunk-TUA6E3BR.js} +76 -39
  21. package/dist/chunk-TUA6E3BR.js.map +1 -0
  22. package/dist/{chunk-AXIM6E7T.js → chunk-VQRVBQIB.js} +2 -2
  23. package/dist/{chunk-D4JBPAJ5.js → chunk-XDCGYHLF.js} +4 -41
  24. package/dist/chunk-XDCGYHLF.js.map +1 -0
  25. package/dist/index.js +17 -18
  26. package/dist/index.js.map +1 -1
  27. package/dist/library.js +5 -5
  28. package/dist/server.js +13 -19
  29. package/dist/server.js.map +1 -1
  30. package/dist/styles.css +1 -1
  31. package/dist/types/ai/ProofreadButton.d.ts +4 -0
  32. package/dist/types/ai/SubjectIdeas.d.ts +1 -1
  33. package/dist/types/ai/agent.d.ts +1 -1
  34. package/dist/types/ai/agentEndpoint.d.ts +3 -11
  35. package/dist/types/ai/claude.d.ts +3 -3
  36. package/dist/types/ai/createAi.d.ts +9 -14
  37. package/dist/types/ai/diff.d.ts +24 -0
  38. package/dist/types/ai/{draft.d.ts → errors.d.ts} +30 -81
  39. package/dist/types/ai/generator.d.ts +25 -91
  40. package/dist/types/ai/index.d.ts +2 -2
  41. package/dist/types/ai/proofread.d.ts +99 -0
  42. package/dist/types/ai/rewrite.d.ts +27 -8
  43. package/dist/types/ai/server.d.ts +4 -4
  44. package/dist/types/ai/subjects.d.ts +6 -0
  45. package/dist/types/core/EmailEditor.d.ts +10 -0
  46. package/dist/types/core/constants.d.ts +1 -1
  47. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  48. package/dist/types/server/handler.d.ts +5 -3
  49. package/dist/types/ui/primitives/searchShortcut.d.ts +2 -2
  50. package/package.json +1 -1
  51. package/dist/chunk-4IXP5YVN.js +0 -119
  52. package/dist/chunk-4IXP5YVN.js.map +0 -1
  53. package/dist/chunk-62HBUBBI.js.map +0 -1
  54. package/dist/chunk-AS2EPMA6.js.map +0 -1
  55. package/dist/chunk-D4JBPAJ5.js.map +0 -1
  56. package/dist/chunk-GWXY3MWQ.js.map +0 -1
  57. package/dist/chunk-OWVXKMLI.js +0 -121
  58. package/dist/chunk-OWVXKMLI.js.map +0 -1
  59. package/dist/chunk-VJKF6SR4.js.map +0 -1
  60. package/dist/types/ai/GenerateButton.d.ts +0 -4
  61. package/dist/types/ai/prompt.d.ts +0 -76
  62. package/dist/types/ai/purposes.d.ts +0 -48
  63. /package/dist/{chunk-G4U6PSF6.js.map → chunk-A6VTFUSW.js.map} +0 -0
  64. /package/dist/{chunk-P7ABBLX6.js.map → chunk-KTTVBNOU.js.map} +0 -0
  65. /package/dist/{chunk-LUM7J5FF.js.map → chunk-OLTD3S3N.js.map} +0 -0
  66. /package/dist/{chunk-FFSSAMHH.js.map → chunk-SCQKWAXR.js.map} +0 -0
  67. /package/dist/{chunk-AXIM6E7T.js.map → chunk-VQRVBQIB.js.map} +0 -0
package/dist/ai.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cleanRichText
3
- } from "./chunk-P7ABBLX6.js";
3
+ } from "./chunk-KTTVBNOU.js";
4
4
  import {
5
5
  Button,
6
6
  Dialog,
@@ -15,47 +15,37 @@ import {
15
15
  DropdownMenuTrigger,
16
16
  Input,
17
17
  Label,
18
- SEARCH_KEYS,
19
- SearchKeyHint,
20
18
  SectionLabel,
21
- Textarea,
22
19
  cn,
23
20
  createHttpCaller,
24
- useSearchShortcut,
25
21
  useUi
26
- } from "./chunk-D4JBPAJ5.js";
27
- import "./chunk-4IXP5YVN.js";
22
+ } from "./chunk-XDCGYHLF.js";
23
+ import "./chunk-7G7C6G3A.js";
28
24
  import {
29
25
  AGENT_ERROR_CODES,
30
26
  AGENT_PROTOCOL,
31
27
  AI_MESSAGES,
32
28
  AI_PROVIDERS,
33
29
  DEFAULT_CLAUDE_MODEL,
34
- EMAIL_PURPOSES,
35
30
  EmailGenerationError,
36
31
  GENERATION_ERRORS,
37
- MAX_DETAILS_LENGTH,
38
32
  MAX_REWRITE_INSTRUCTION,
39
- PURPOSE_CATEGORIES,
40
33
  REWRITE_ACTIONS,
41
34
  REWRITE_LANGUAGES,
42
35
  agentEmailModel,
43
36
  claudeEmailModel,
44
37
  claudeProvider,
45
38
  createEmailGenerator,
39
+ diffWords,
46
40
  emailWords,
47
41
  failureCode,
48
42
  listProviderModels,
49
43
  providerEmailModel,
50
- purposesByCategory,
51
- resolveProvider
52
- } from "./chunk-VJKF6SR4.js";
53
- import "./chunk-OWVXKMLI.js";
54
- import "./chunk-AXIM6E7T.js";
55
- import {
56
- brandTemplate,
57
- hasBrand
58
- } from "./chunk-62HBUBBI.js";
44
+ resolveProvider,
45
+ visibleText
46
+ } from "./chunk-24E4GOHU.js";
47
+ import "./chunk-VQRVBQIB.js";
48
+ import "./chunk-MBZI6NVP.js";
59
49
 
60
50
  // src/ai/keys.js
61
51
  var STORAGE_PREFIX = "best-email-builder:ai-key:";
@@ -733,42 +723,45 @@ function ConnectAi({ aiKey }) {
733
723
  ) });
734
724
  }
735
725
 
736
- // src/ai/GenerateButton.jsx
737
- import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
738
- import { Check as Check2, Loader2, Search as Search2, Sparkles, TriangleAlert as TriangleAlert2 } from "lucide-react";
739
-
740
- // src/ai/chip.js
741
- var chipClassName = (selected = false) => cn(
742
- "eb:inline-flex eb:h-7 eb:items-center eb:gap-1 eb:rounded-full eb:border eb:px-3 eb:text-[12px] eb:font-medium eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:disabled:opacity-50",
743
- selected ? "eb:border-primary eb:bg-primary eb:text-primary-foreground" : "eb:border-border eb:bg-background eb:text-foreground eb:hover:border-primary/50 eb:hover:bg-primary/5"
744
- );
745
-
746
- // src/ai/GenerateButton.jsx
726
+ // src/ai/ProofreadButton.jsx
727
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
728
+ import { Check as Check2, CircleCheck, Loader2, SpellCheck, TriangleAlert as TriangleAlert2 } from "lucide-react";
747
729
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
748
- var fold = (text) => text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
749
- function GenerateButton({ context, generate: generateEmail }) {
750
- const { editor, ready, isDirty, markDirty, template, setMeta, variables, sender, brand } = context;
730
+ var LABEL_TAGS = { "mj-button": "button", "mj-navbar-link": "link" };
731
+ var KIND_LABELS = { subject: "Subject", preheader: "Preview text", text: "Text", button: "Button", link: "Menu link" };
732
+ function textsOf(editor, meta) {
733
+ const texts = [];
734
+ if (meta.subject.trim()) texts.push({ id: "subject", kind: "subject", format: "plain", text: meta.subject.trim() });
735
+ if (meta.preheader.trim()) texts.push({ id: "preheader", kind: "preheader", format: "plain", text: meta.preheader.trim() });
736
+ const walk = (node) => {
737
+ if (!node || typeof node !== "object") return;
738
+ const element = node.tag === "mj-text" || LABEL_TAGS[node.tag] ? editor.getElement(node.id) : null;
739
+ if (node.tag === "mj-text" && element?.html) texts.push({ id: node.id, kind: "text", format: "html", text: element.html });
740
+ else if (LABEL_TAGS[node.tag] && element?.content) texts.push({ id: node.id, kind: LABEL_TAGS[node.tag], format: "plain", text: element.content });
741
+ for (const child of node.children || []) walk(child);
742
+ };
743
+ walk(editor.getDocument().project?.root);
744
+ return texts;
745
+ }
746
+ function currentText(editor, meta, id) {
747
+ if (id === "subject" || id === "preheader") return meta[id].trim();
748
+ const element = editor.getElement(id);
749
+ return (element?.tagName === "mj-text" ? element?.html : element?.content) ?? null;
750
+ }
751
+ function ProofreadButton({ context, proofread }) {
752
+ const { editor, ready, meta, setMeta } = context;
751
753
  const { t, locale, notify } = useUi();
752
754
  const [open, setOpen] = useState3(false);
753
- const [purpose, setPurpose] = useState3(
754
- /** @type {string|null} */
755
- null
756
- );
757
- const [details, setDetails] = useState3("");
758
- const [query, setQuery] = useState3("");
759
755
  const [busy, setBusy] = useState3(false);
760
756
  const [error, setError] = useState3("");
761
757
  const [seconds, setSeconds] = useState3(0);
762
- const [dirty, setDirty] = useState3(false);
758
+ const [fixes, setFixes] = useState3(null);
763
759
  const abortRef = useRef3(
764
760
  /** @type {AbortController|null} */
765
761
  null
766
762
  );
767
- const searchRef = useRef3(
768
- /** @type {HTMLInputElement|null} */
769
- null
770
- );
771
- useSearchShortcut(searchRef);
763
+ const metaRef = useRef3(meta);
764
+ metaRef.current = meta;
772
765
  useEffect3(() => () => abortRef.current?.abort(), []);
773
766
  useEffect3(() => {
774
767
  if (!busy) return void 0;
@@ -776,168 +769,178 @@ function GenerateButton({ context, generate: generateEmail }) {
776
769
  const timer = setInterval(() => setSeconds((s) => s + 1), 1e3);
777
770
  return () => clearInterval(timer);
778
771
  }, [busy]);
779
- const groups = useMemo2(() => {
780
- const words = fold(query.trim()).split(/\s+/).filter(Boolean);
781
- return purposesByCategory().map((group) => ({
782
- ...group,
783
- purposes: group.purposes.filter((entry) => {
784
- const label = fold(`${t(entry.label)} ${entry.label} ${t(group.label)}`);
785
- return words.every((word) => label.includes(word));
786
- })
787
- })).filter((group) => group.purposes.length > 0);
788
- }, [query, t]);
789
- const openDialog = () => {
790
- setError("");
791
- setDirty(isDirty());
792
- setOpen(true);
793
- };
794
- const close = (next) => {
795
- if (next) return;
796
- abortRef.current?.abort();
797
- setOpen(false);
798
- };
799
- const generate = async () => {
800
- if (busy || !purpose && !details.trim()) return;
772
+ useEffect3(
773
+ () => editor.on("load", () => {
774
+ abortRef.current?.abort();
775
+ setOpen(false);
776
+ }),
777
+ [editor]
778
+ );
779
+ const run = async () => {
780
+ if (busy) return;
781
+ const texts = textsOf(editor, metaRef.current);
801
782
  setError("");
783
+ setFixes(null);
784
+ if (!texts.length) {
785
+ setError(t("The email has no text to check yet."));
786
+ return;
787
+ }
802
788
  setBusy(true);
803
789
  const controller = new AbortController();
804
790
  abortRef.current = controller;
805
791
  try {
806
- const email = await generateEmail(
807
- {
808
- purpose,
809
- details: details.trim(),
810
- language: String(locale || "en").slice(0, 2),
811
- sender: sender || {},
812
- ...hasBrand(brand) ? { brand: { primary: brand.primary, heading: brand.heading, font: brand.font } } : {},
813
- variables: (variables || []).map((variable) => ({ key: variable.key, label: variable.label }))
814
- },
792
+ const result = await proofread(
793
+ { texts: texts.map(({ id, kind, format, text }) => ({ id, kind, format, text })), language: String(locale || "en").slice(0, 2) },
815
794
  { signal: controller.signal }
816
795
  );
817
796
  if (controller.signal.aborted) return;
818
- editor.loadMjml(hasBrand(brand) ? brandTemplate(email.mjml, brand, { sender, t }) : email.mjml);
819
- setMeta((meta) => ({
820
- ...meta,
821
- name: meta.name || email.name,
822
- subject: email.subject || meta.subject,
823
- preheader: email.preheader || meta.preheader
824
- }));
825
- if (template.id) markDirty();
826
- setOpen(false);
827
- notify("success", t("Your email is ready. Edit anything, then save."));
797
+ const sent = new Map(texts.map((text) => [text.id, text]));
798
+ setFixes(
799
+ result.fixes.filter((fix) => sent.has(fix.id)).map((fix) => {
800
+ const text = (
801
+ /** @type {(typeof texts)[number]} */
802
+ sent.get(fix.id)
803
+ );
804
+ const stale = currentText(editor, metaRef.current, fix.id) !== text.text;
805
+ return { id: fix.id, kind: text.kind, format: text.format, before: text.text, after: fix.text, state: stale ? "stale" : "open" };
806
+ })
807
+ );
828
808
  } catch (err) {
829
- if (!controller.signal.aborted) setError(err?.message ? t(err.message) : t("The email could not be generated."));
809
+ if (!controller.signal.aborted) setError(err?.message ? t(err.message) : t("The email could not be checked."));
830
810
  } finally {
831
- if (abortRef.current === controller) abortRef.current = null;
832
- setBusy(false);
811
+ if (abortRef.current === controller) {
812
+ abortRef.current = null;
813
+ setBusy(false);
814
+ }
833
815
  }
834
816
  };
835
- const cancel = () => {
817
+ useEffect3(() => {
818
+ if (!open) return void 0;
819
+ run();
820
+ return () => abortRef.current?.abort();
821
+ }, [open]);
822
+ const openDialog = () => setOpen(true);
823
+ const close = (next) => {
824
+ if (next) return;
836
825
  abortRef.current?.abort();
826
+ abortRef.current = null;
837
827
  setBusy(false);
828
+ setOpen(false);
838
829
  };
839
- const canGenerate = Boolean(purpose || details.trim());
830
+ const apply = (ids) => {
831
+ if (!fixes) return;
832
+ const wanted = new Set(ids);
833
+ const contents = [];
834
+ const metaChanges = {};
835
+ const next = fixes.map((fix) => {
836
+ if (!wanted.has(fix.id) || fix.state !== "open") return fix;
837
+ if (currentText(editor, metaRef.current, fix.id) !== fix.before) return { ...fix, state: (
838
+ /** @type {const} */
839
+ "stale"
840
+ ) };
841
+ if (fix.id === "subject" || fix.id === "preheader") metaChanges[fix.id] = fix.after;
842
+ else contents.push({ id: fix.id, content: fix.format === "html" ? cleanRichText(fix.after, document) : fix.after });
843
+ return { ...fix, state: (
844
+ /** @type {const} */
845
+ "applied"
846
+ ) };
847
+ });
848
+ if (contents.length) editor.setContents(contents);
849
+ if (Object.keys(metaChanges).length) setMeta((current) => ({ ...current, ...metaChanges }));
850
+ setFixes(next);
851
+ const applied = next.filter((fix, index) => fix.state === "applied" && fixes[index].state !== "applied").length;
852
+ if (applied && next.every((fix) => fix.state !== "open")) {
853
+ setOpen(false);
854
+ notify("success", t("Corrections applied: {count}.", { count: next.filter((fix) => fix.state === "applied").length }));
855
+ }
856
+ };
857
+ const remaining = (fixes || []).filter((fix) => fix.state === "open");
840
858
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
841
859
  /* @__PURE__ */ jsxs3(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: openDialog, children: [
842
- /* @__PURE__ */ jsx3(Sparkles, {}),
843
- t("Generate")
860
+ /* @__PURE__ */ jsx3(SpellCheck, {}),
861
+ t("Proofread")
844
862
  ] }),
845
- /* @__PURE__ */ jsx3(Dialog, { open, onOpenChange: close, children: /* @__PURE__ */ jsxs3(
846
- DialogContent,
847
- {
848
- className: "eb:flex eb:max-h-[calc(100dvh-2rem)] eb:flex-col eb:gap-0 eb:p-0 eb:sm:max-w-2xl",
849
- "data-testid": "eb-generate",
850
- onOpenAutoFocus: (event) => {
851
- event.preventDefault();
852
- searchRef.current?.focus();
853
- },
854
- children: [
855
- /* @__PURE__ */ jsxs3(DialogHeader, { className: "eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-3", children: [
856
- /* @__PURE__ */ jsx3(DialogTitle, { children: t("Generate an email") }),
857
- /* @__PURE__ */ jsx3(DialogDescription, { children: t("Pick what it is for. AI writes a complete email that opens here, ready to edit.") })
858
- ] }),
859
- /* @__PURE__ */ jsx3("div", { className: "eb:px-6 eb:pb-3", children: /* @__PURE__ */ jsxs3("div", { className: "eb:relative", children: [
860
- /* @__PURE__ */ jsx3(Search2, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2.5 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted", "aria-hidden": "true" }),
861
- /* @__PURE__ */ jsx3(
862
- Input,
863
- {
864
- ref: searchRef,
865
- type: "search",
866
- value: query,
867
- onChange: (event) => setQuery(event.target.value),
868
- placeholder: t("Search: sale, password, birthday\u2026"),
869
- "aria-label": t("Search"),
870
- "aria-keyshortcuts": SEARCH_KEYS,
871
- className: "eb:pr-16 eb:pl-8 eb:[&::-webkit-search-cancel-button]:hidden",
872
- disabled: busy
873
- }
874
- ),
875
- /* @__PURE__ */ jsx3(SearchKeyHint, { className: "eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2" })
876
- ] }) }),
877
- /* @__PURE__ */ jsx3("div", { className: "eb:min-h-0 eb:flex-1 eb:overflow-y-auto eb:border-y eb:border-border eb:px-6 eb:py-4", "data-testid": "eb-generate-purposes", children: /* @__PURE__ */ jsxs3("fieldset", { disabled: busy, className: "eb:m-0 eb:min-w-0 eb:border-0 eb:p-0", children: [
878
- /* @__PURE__ */ jsx3("legend", { className: "eb:sr-only", children: t("What is the email for?") }),
879
- groups.length === 0 && /* @__PURE__ */ jsx3("p", { className: "eb:py-6 eb:text-center eb:text-[13px] eb:text-foreground-muted", children: t("Nothing matches. Describe your email in the details below.") }),
880
- /* @__PURE__ */ jsx3("div", { className: "eb:flex eb:flex-col eb:gap-4", children: groups.map((group) => /* @__PURE__ */ jsxs3("section", { className: "eb:flex eb:flex-col eb:gap-2", "aria-label": t(group.label), children: [
881
- /* @__PURE__ */ jsx3(SectionLabel, { children: t(group.label) }),
882
- /* @__PURE__ */ jsx3("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: group.purposes.map((entry) => {
883
- const selected = purpose === entry.id;
884
- return /* @__PURE__ */ jsxs3(
885
- "button",
886
- {
887
- type: "button",
888
- "aria-pressed": selected,
889
- "data-purpose": entry.id,
890
- onClick: () => setPurpose(selected ? null : entry.id),
891
- className: chipClassName(selected),
892
- children: [
893
- selected && /* @__PURE__ */ jsx3(Check2, { className: "eb:size-3", "aria-hidden": "true" }),
894
- t(entry.label)
895
- ]
896
- },
897
- entry.id
898
- );
899
- }) })
900
- ] }, group.id)) })
901
- ] }) }),
902
- /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:flex-col eb:gap-3 eb:px-6 eb:pt-3 eb:pb-6", children: [
903
- /* @__PURE__ */ jsx3(
904
- Textarea,
905
- {
906
- value: details,
907
- onChange: (event) => setDetails(event.target.value.slice(0, MAX_DETAILS_LENGTH)),
908
- placeholder: t("Details (optional): product, dates, offer, tone, language\u2026"),
909
- "aria-label": t("Details (optional)"),
910
- rows: 2,
911
- className: "eb:min-h-14",
912
- disabled: busy,
913
- onKeyDown: (event) => {
914
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) generate();
915
- }
916
- }
917
- ),
918
- error ? /* @__PURE__ */ jsxs3("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
919
- /* @__PURE__ */ jsx3(TriangleAlert2, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
920
- error
921
- ] }) : dirty && !busy && /* @__PURE__ */ jsx3("p", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("The email being edited will be replaced, unsaved changes included.") }),
922
- /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:items-center eb:justify-end eb:gap-2", children: [
923
- busy && /* @__PURE__ */ jsx3("p", { className: "eb:mr-auto eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: t("Writing your email\u2026 {seconds} s", { seconds }) }),
924
- busy && /* @__PURE__ */ jsx3(Button, { variant: "outline", size: "sm", onClick: cancel, children: t("Cancel") }),
925
- /* @__PURE__ */ jsxs3(Button, { size: "sm", onClick: generate, disabled: !canGenerate || busy, "data-testid": "eb-generate-submit", children: [
926
- busy ? /* @__PURE__ */ jsx3(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx3(Sparkles, {}),
927
- t("Generate")
928
- ] })
929
- ] })
930
- ] })
931
- ]
932
- }
933
- ) })
863
+ /* @__PURE__ */ jsx3(Dialog, { open, onOpenChange: close, children: /* @__PURE__ */ jsxs3(DialogContent, { className: "eb:flex eb:max-h-[calc(100dvh-2rem)] eb:flex-col eb:gap-0 eb:p-0 eb:sm:max-w-xl", "data-testid": "eb-proofread", children: [
864
+ /* @__PURE__ */ jsxs3(DialogHeader, { className: "eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-4", children: [
865
+ /* @__PURE__ */ jsx3(DialogTitle, { children: t("Proofread the email") }),
866
+ /* @__PURE__ */ jsx3(DialogDescription, { children: t("AI looks for spelling, grammar and punctuation mistakes in every text. Nothing changes until you apply it.") })
867
+ ] }),
868
+ /* @__PURE__ */ jsxs3("div", { className: "eb:min-h-0 eb:flex-1 eb:overflow-y-auto eb:border-t eb:border-border eb:px-6 eb:py-4", children: [
869
+ busy && /* @__PURE__ */ jsxs3("p", { className: "eb:flex eb:items-center eb:gap-2 eb:py-6 eb:text-[13px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
870
+ /* @__PURE__ */ jsx3(Loader2, { className: "eb:size-4 eb:animate-spin", "aria-hidden": "true" }),
871
+ t("Reading the email\u2026 {seconds} s", { seconds })
872
+ ] }),
873
+ error && /* @__PURE__ */ jsxs3("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:py-2 eb:text-[13px] eb:text-destructive", children: [
874
+ /* @__PURE__ */ jsx3(TriangleAlert2, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
875
+ error
876
+ ] }),
877
+ fixes && !fixes.length && /* @__PURE__ */ jsxs3("p", { className: "eb:flex eb:items-center eb:gap-2 eb:py-6 eb:text-[13px] eb:text-foreground", role: "status", children: [
878
+ /* @__PURE__ */ jsx3(CircleCheck, { className: "eb:size-4 eb:text-success", "aria-hidden": "true" }),
879
+ t("No mistake found.")
880
+ ] }),
881
+ fixes && fixes.length > 0 && /* @__PURE__ */ jsx3("ul", { className: "eb:flex eb:flex-col eb:gap-2.5", "aria-label": t("Corrections"), children: fixes.map((fix) => /* @__PURE__ */ jsxs3(
882
+ "li",
883
+ {
884
+ className: cn("eb:rounded-md eb:border eb:border-border eb:p-3", fix.state !== "open" && "eb:bg-muted/40"),
885
+ "data-testid": "eb-proofread-fix",
886
+ "data-state": fix.state,
887
+ children: [
888
+ /* @__PURE__ */ jsxs3("div", { className: "eb:mb-1.5 eb:flex eb:items-center eb:gap-2", children: [
889
+ /* @__PURE__ */ jsx3("span", { className: "eb:text-[11px] eb:font-medium eb:tracking-wide eb:text-foreground-muted eb:uppercase", children: t(KIND_LABELS[fix.kind]) }),
890
+ /* @__PURE__ */ jsxs3("span", { className: "eb:ml-auto", children: [
891
+ fix.state === "open" && /* @__PURE__ */ jsx3(Button, { variant: "outline", size: "xs", onClick: () => apply([fix.id]), children: t("Apply") }),
892
+ fix.state === "applied" && /* @__PURE__ */ jsxs3("span", { className: "eb:flex eb:items-center eb:gap-1 eb:text-[12px] eb:text-success", children: [
893
+ /* @__PURE__ */ jsx3(Check2, { className: "eb:size-3.5", "aria-hidden": "true" }),
894
+ t("Applied")
895
+ ] }),
896
+ fix.state === "stale" && /* @__PURE__ */ jsx3("span", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("Text changed since \u2014 left as it is") })
897
+ ] })
898
+ ] }),
899
+ /* @__PURE__ */ jsx3(Changes, { before: fix.before, after: fix.after, html: fix.format === "html" })
900
+ ]
901
+ },
902
+ fix.id
903
+ )) })
904
+ ] }),
905
+ /* @__PURE__ */ jsx3("div", { className: "eb:flex eb:items-center eb:justify-end eb:gap-2 eb:border-t eb:border-border eb:px-6 eb:py-4", children: busy ? /* @__PURE__ */ jsx3(Button, { variant: "outline", size: "sm", onClick: () => close(false), children: t("Cancel") }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
906
+ /* @__PURE__ */ jsx3(Button, { variant: "ghost", size: "sm", className: "eb:mr-auto", onClick: run, children: t("Check again") }),
907
+ remaining.length > 0 ? /* @__PURE__ */ jsxs3(Button, { size: "sm", onClick: () => apply(remaining.map((fix) => fix.id)), "data-testid": "eb-proofread-apply-all", children: [
908
+ /* @__PURE__ */ jsx3(Check2, {}),
909
+ t("Apply all ({count})", { count: remaining.length })
910
+ ] }) : /* @__PURE__ */ jsx3(Button, { variant: "outline", size: "sm", onClick: () => close(false), children: t("Close") })
911
+ ] }) })
912
+ ] }) })
934
913
  ] });
935
914
  }
915
+ function Changes({ before, after, html }) {
916
+ const parts = diffWords(html ? visibleText(before) : before, html ? visibleText(after) : after);
917
+ return /* @__PURE__ */ jsx3("p", { className: "eb:text-[13px] eb:leading-relaxed eb:break-words eb:whitespace-pre-line eb:text-foreground", children: parts.map(
918
+ (part, index) => part.type === "same" ? /* @__PURE__ */ jsx3("span", { children: around(part.text, index === 0, index === parts.length - 1) }, index) : part.type === "removed" ? /* @__PURE__ */ jsx3("del", { className: "eb:rounded-sm eb:bg-destructive/10 eb:text-destructive eb:line-through", children: part.text }, index) : /* @__PURE__ */ jsx3("ins", { className: "eb:rounded-sm eb:bg-success/15 eb:text-success eb:no-underline", children: part.text }, index)
919
+ ) });
920
+ }
921
+ var CONTEXT = 48;
922
+ function around(text, first, last) {
923
+ if (text.length <= CONTEXT * 2 + 8) return text;
924
+ const head = (value) => value.slice(0, CONTEXT).replace(/\s\S*$/, "");
925
+ const tail = (value) => value.slice(-CONTEXT).replace(/^\S*\s/, "");
926
+ if (first && last) return text;
927
+ if (first) return `\u2026${tail(text)}`;
928
+ if (last) return `${head(text)}\u2026`;
929
+ return `${head(text)} \u2026 ${tail(text)}`;
930
+ }
936
931
 
937
932
  // src/ai/RewriteText.jsx
938
933
  import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
939
- import { ChevronDown as ChevronDown2, Languages, Loader2 as Loader22, Sparkles as Sparkles2, TriangleAlert as TriangleAlert3 } from "lucide-react";
940
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
934
+ import { ChevronDown as ChevronDown2, Languages, Loader2 as Loader22, PenLine, Sparkles, TriangleAlert as TriangleAlert3 } from "lucide-react";
935
+
936
+ // src/ai/chip.js
937
+ var chipClassName = (selected = false) => cn(
938
+ "eb:inline-flex eb:h-7 eb:items-center eb:gap-1 eb:rounded-full eb:border eb:px-3 eb:text-[12px] eb:font-medium eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:disabled:opacity-50",
939
+ selected ? "eb:border-primary eb:bg-primary eb:text-primary-foreground" : "eb:border-border eb:bg-background eb:text-foreground eb:hover:border-primary/50 eb:hover:bg-primary/5"
940
+ );
941
+
942
+ // src/ai/RewriteText.jsx
943
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
941
944
  function nativeName(code) {
942
945
  try {
943
946
  const name = new Intl.DisplayNames([code], { type: "language" }).of(code) || code;
@@ -947,26 +950,27 @@ function nativeName(code) {
947
950
  }
948
951
  }
949
952
  function RewriteText({ editor, selection, rewrite }) {
950
- const { t, notify } = useUi();
953
+ const { t, locale, notify } = useUi();
951
954
  const [running, setRunning] = useState4(
952
955
  /** @type {string|null} */
953
956
  null
954
957
  );
955
958
  const [error, setError] = useState4("");
956
959
  const [instruction, setInstruction] = useState4("");
960
+ const [topic, setTopic] = useState4("");
957
961
  const abortRef = useRef4(
958
962
  /** @type {AbortController|null} */
959
963
  null
960
964
  );
961
965
  const label = selection.tagName === "mj-button";
962
966
  useEffect4(() => () => abortRef.current?.abort(), []);
963
- const currentText = () => {
967
+ const currentText2 = () => {
964
968
  const element = editor.getElement(selection.id);
965
969
  return (label ? element?.content : element?.html) ?? "";
966
970
  };
967
971
  const run = async (action, extra = {}) => {
968
972
  if (running) return;
969
- const text = currentText();
973
+ const text = currentText2();
970
974
  setError("");
971
975
  setRunning(action);
972
976
  const controller = new AbortController();
@@ -974,15 +978,18 @@ function RewriteText({ editor, selection, rewrite }) {
974
978
  try {
975
979
  const result = await rewrite({ action, text, format: label ? "plain" : "html", ...extra }, { signal: controller.signal });
976
980
  if (controller.signal.aborted) return;
977
- if (currentText() !== text) {
981
+ if (currentText2() !== text) {
978
982
  setError(t("The text changed in the meantime, so it was left as it is."));
979
983
  return;
980
984
  }
981
985
  editor.setContent(label ? result.text : cleanRichText(result.text, document), selection.id);
982
986
  if (action === "custom") setInstruction("");
983
- notify("success", t("Text rewritten. Ctrl+Z brings back the previous one."));
987
+ if (action === "write") setTopic("");
988
+ notify("success", action === "write" ? t("Text written. Ctrl+Z brings back the previous one.") : t("Text rewritten. Ctrl+Z brings back the previous one."));
984
989
  } catch (err) {
985
- if (!controller.signal.aborted) setError(err?.message ? t(err.message) : t("The text could not be rewritten."));
990
+ if (!controller.signal.aborted) {
991
+ setError(err?.message ? t(err.message) : action === "write" ? t("The text could not be written.") : t("The text could not be rewritten."));
992
+ }
986
993
  } finally {
987
994
  if (abortRef.current === controller) abortRef.current = null;
988
995
  setRunning(null);
@@ -992,65 +999,110 @@ function RewriteText({ editor, selection, rewrite }) {
992
999
  abortRef.current?.abort();
993
1000
  setRunning(null);
994
1001
  };
1002
+ const write = () => {
1003
+ const subject = topic.trim();
1004
+ if (!subject) return;
1005
+ const size = Number.parseFloat(String(editor.getElement(selection.id)?.attributes?.["font-size"] || ""));
1006
+ const own = visibleText(currentText2());
1007
+ const words = emailWords(editor.exportMjml());
1008
+ run("write", {
1009
+ instruction: subject,
1010
+ context: own ? words.replace(own, "").trim() : words,
1011
+ role: size >= 20 ? "heading" : "text",
1012
+ language: String(locale || "en").slice(0, 2)
1013
+ });
1014
+ };
995
1015
  const busy = Boolean(running);
996
1016
  const spinner = /* @__PURE__ */ jsx4(Loader22, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
997
- return /* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2.5", "aria-label": t("Rewrite with AI"), "data-testid": "eb-rewrite", children: [
998
- /* @__PURE__ */ jsxs4(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
999
- /* @__PURE__ */ jsx4(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
1000
- t("Rewrite with AI")
1017
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
1018
+ !label && /* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2.5", "aria-label": t("Write with AI"), "data-testid": "eb-write", children: [
1019
+ /* @__PURE__ */ jsxs4(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
1020
+ /* @__PURE__ */ jsx4(PenLine, { className: "eb:size-3", "aria-hidden": "true" }),
1021
+ t("Write with AI")
1022
+ ] }),
1023
+ /* @__PURE__ */ jsxs4(
1024
+ "form",
1025
+ {
1026
+ className: "eb:flex eb:gap-1.5",
1027
+ onSubmit: (event) => {
1028
+ event.preventDefault();
1029
+ write();
1030
+ },
1031
+ children: [
1032
+ /* @__PURE__ */ jsx4(
1033
+ Input,
1034
+ {
1035
+ value: topic,
1036
+ onChange: (event) => setTopic(event.target.value),
1037
+ maxLength: MAX_REWRITE_INSTRUCTION,
1038
+ placeholder: t("What should it say? e.g. the sale ends Sunday"),
1039
+ "aria-label": t("What the text is about"),
1040
+ disabled: busy
1041
+ }
1042
+ ),
1043
+ /* @__PURE__ */ jsx4(Button, { type: "submit", variant: "outline", size: "icon-sm", "aria-label": t("Write"), disabled: busy || !topic.trim(), children: running === "write" ? /* @__PURE__ */ jsx4(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx4(PenLine, {}) })
1044
+ ]
1045
+ }
1046
+ )
1001
1047
  ] }),
1002
- /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: [
1003
- REWRITE_ACTIONS.map((action) => /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: busy, onClick: () => run(action.id), children: [
1004
- running === action.id && spinner,
1005
- t(action.label)
1006
- ] }, action.id)),
1007
- /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
1008
- /* @__PURE__ */ jsx4(DropdownMenuTrigger, { asChild: true, disabled: busy, children: /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), children: [
1009
- running === "translate" ? spinner : /* @__PURE__ */ jsx4(Languages, { className: "eb:size-3", "aria-hidden": "true" }),
1010
- t("Translate"),
1011
- /* @__PURE__ */ jsx4(ChevronDown2, { className: "eb:size-3", "aria-hidden": "true" })
1012
- ] }) }),
1013
- /* @__PURE__ */ jsx4(DropdownMenuContent, { align: "start", "aria-label": t("Translate into"), children: REWRITE_LANGUAGES.map((code) => /* @__PURE__ */ jsx4(DropdownMenuItem, { lang: code, onSelect: () => run("translate", { targetLanguage: code }), children: nativeName(code) }, code)) })
1048
+ /* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2.5", "aria-label": t("Rewrite with AI"), "data-testid": "eb-rewrite", children: [
1049
+ /* @__PURE__ */ jsxs4(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
1050
+ /* @__PURE__ */ jsx4(Sparkles, { className: "eb:size-3", "aria-hidden": "true" }),
1051
+ t("Rewrite with AI")
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: [
1054
+ REWRITE_ACTIONS.map((action) => /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: busy, onClick: () => run(action.id), children: [
1055
+ running === action.id && spinner,
1056
+ t(action.label)
1057
+ ] }, action.id)),
1058
+ /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
1059
+ /* @__PURE__ */ jsx4(DropdownMenuTrigger, { asChild: true, disabled: busy, children: /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), children: [
1060
+ running === "translate" ? spinner : /* @__PURE__ */ jsx4(Languages, { className: "eb:size-3", "aria-hidden": "true" }),
1061
+ t("Translate"),
1062
+ /* @__PURE__ */ jsx4(ChevronDown2, { className: "eb:size-3", "aria-hidden": "true" })
1063
+ ] }) }),
1064
+ /* @__PURE__ */ jsx4(DropdownMenuContent, { align: "start", "aria-label": t("Translate into"), children: REWRITE_LANGUAGES.map((code) => /* @__PURE__ */ jsx4(DropdownMenuItem, { lang: code, onSelect: () => run("translate", { targetLanguage: code }), children: nativeName(code) }, code)) })
1065
+ ] })
1066
+ ] }),
1067
+ /* @__PURE__ */ jsxs4(
1068
+ "form",
1069
+ {
1070
+ className: "eb:flex eb:gap-1.5",
1071
+ onSubmit: (event) => {
1072
+ event.preventDefault();
1073
+ if (instruction.trim()) run("custom", { instruction: instruction.trim() });
1074
+ },
1075
+ children: [
1076
+ /* @__PURE__ */ jsx4(
1077
+ Input,
1078
+ {
1079
+ value: instruction,
1080
+ onChange: (event) => setInstruction(event.target.value),
1081
+ maxLength: MAX_REWRITE_INSTRUCTION,
1082
+ placeholder: t("Or say what to change\u2026"),
1083
+ "aria-label": t("What to change"),
1084
+ disabled: busy
1085
+ }
1086
+ ),
1087
+ /* @__PURE__ */ jsx4(Button, { type: "submit", variant: "outline", size: "icon-sm", "aria-label": t("Rewrite"), disabled: busy || !instruction.trim(), children: running === "custom" ? /* @__PURE__ */ jsx4(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx4(Sparkles, {}) })
1088
+ ]
1089
+ }
1090
+ ),
1091
+ running && /* @__PURE__ */ jsxs4("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
1092
+ running === "write" ? t("Writing\u2026") : t("Rewriting\u2026"),
1093
+ /* @__PURE__ */ jsx4(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
1094
+ ] }),
1095
+ error && /* @__PURE__ */ jsxs4("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
1096
+ /* @__PURE__ */ jsx4(TriangleAlert3, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
1097
+ error
1014
1098
  ] })
1015
- ] }),
1016
- /* @__PURE__ */ jsxs4(
1017
- "form",
1018
- {
1019
- className: "eb:flex eb:gap-1.5",
1020
- onSubmit: (event) => {
1021
- event.preventDefault();
1022
- if (instruction.trim()) run("custom", { instruction: instruction.trim() });
1023
- },
1024
- children: [
1025
- /* @__PURE__ */ jsx4(
1026
- Input,
1027
- {
1028
- value: instruction,
1029
- onChange: (event) => setInstruction(event.target.value),
1030
- maxLength: MAX_REWRITE_INSTRUCTION,
1031
- placeholder: t("Or say what to change\u2026"),
1032
- "aria-label": t("What to change"),
1033
- disabled: busy
1034
- }
1035
- ),
1036
- /* @__PURE__ */ jsx4(Button, { type: "submit", variant: "outline", size: "icon-sm", "aria-label": t("Rewrite"), disabled: busy || !instruction.trim(), children: running === "custom" ? /* @__PURE__ */ jsx4(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx4(Sparkles2, {}) })
1037
- ]
1038
- }
1039
- ),
1040
- running && /* @__PURE__ */ jsxs4("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
1041
- t("Rewriting\u2026"),
1042
- /* @__PURE__ */ jsx4(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
1043
- ] }),
1044
- error && /* @__PURE__ */ jsxs4("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
1045
- /* @__PURE__ */ jsx4(TriangleAlert3, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
1046
- error
1047
1099
  ] })
1048
1100
  ] });
1049
1101
  }
1050
1102
 
1051
1103
  // src/ai/SubjectIdeas.jsx
1052
1104
  import { useEffect as useEffect5, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
1053
- import { Check as Check3, Loader2 as Loader23, Sparkles as Sparkles3, TriangleAlert as TriangleAlert4 } from "lucide-react";
1105
+ import { Check as Check3, Loader2 as Loader23, Sparkles as Sparkles2, TriangleAlert as TriangleAlert4 } from "lucide-react";
1054
1106
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1055
1107
  var MAX_AVOID = 12;
1056
1108
  function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
@@ -1169,7 +1221,7 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
1169
1221
  const spinner = /* @__PURE__ */ jsx5(Loader23, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
1170
1222
  return /* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2", "data-testid": "eb-subject-ideas", ref: rootRef, children: [
1171
1223
  !ideas && /* @__PURE__ */ jsxs5("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
1172
- running ? spinner : /* @__PURE__ */ jsx5(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
1224
+ running ? spinner : /* @__PURE__ */ jsx5(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
1173
1225
  t("Suggest subjects")
1174
1226
  ] }),
1175
1227
  ideas && /* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2 eb:rounded-md eb:border eb:border-border eb:p-1.5", children: [
@@ -1198,7 +1250,7 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
1198
1250
  }) }),
1199
1251
  /* @__PURE__ */ jsxs5("div", { className: "eb:flex eb:items-center eb:gap-1.5 eb:px-1 eb:pb-0.5", children: [
1200
1252
  /* @__PURE__ */ jsxs5("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
1201
- running ? spinner : /* @__PURE__ */ jsx5(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
1253
+ running ? spinner : /* @__PURE__ */ jsx5(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
1202
1254
  t("More ideas")
1203
1255
  ] }),
1204
1256
  /* @__PURE__ */ jsx5(Button, { type: "button", variant: "ghost", size: "xs", className: "eb:ml-auto", disabled: running, onClick: done, children: t("Done") })
@@ -1218,12 +1270,12 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
1218
1270
  // src/ai/createAi.js
1219
1271
  var PARTS = (
1220
1272
  /** @type {const} */
1221
- ["generate", "rewrite", "suggestSubjects"]
1273
+ ["proofread", "rewrite", "suggestSubjects"]
1222
1274
  );
1223
- var ROUTES = { generate: "/generate", rewrite: "/rewrite", suggestSubjects: "/subjects" };
1275
+ var ROUTES = { proofread: "/proofread", rewrite: "/rewrite", suggestSubjects: "/subjects" };
1224
1276
  function createAi(options = {}) {
1225
1277
  if (!options || typeof options !== "object") throw new TypeError("createAi: options must be an object");
1226
- const { model, maxAttempts, compile, baseUrl, headers, fetch: fetchImpl, provider: providerOption, apiKey, askKey = false } = options;
1278
+ const { model, maxAttempts, baseUrl, headers, fetch: fetchImpl, provider: providerOption, apiKey, askKey = false } = options;
1227
1279
  const usesProvider = providerOption !== void 0;
1228
1280
  const http = baseUrl !== void 0 || headers !== void 0 || fetchImpl !== void 0 && !usesProvider;
1229
1281
  if (model !== void 0 && typeof model !== "function") throw new TypeError("createAi: `model` must be a function");
@@ -1235,8 +1287,12 @@ function createAi(options = {}) {
1235
1287
  throw new TypeError('createAi: `apiKey` and `askKey` go with a `provider` (e.g. provider: "openrouter")');
1236
1288
  }
1237
1289
  if (apiKey !== void 0 && (typeof apiKey !== "string" || !apiKey)) throw new TypeError("createAi: `apiKey` must be a non-empty string");
1238
- if (compile !== void 0 && (typeof compile !== "function" || !model && !usesProvider)) {
1239
- throw new TypeError("createAi: `compile` must be a function, and goes with `provider` or `model` (the AI in the page)");
1290
+ const { generate } = (
1291
+ /** @type {any} */
1292
+ options
1293
+ );
1294
+ if (generate !== void 0 && generate !== false) {
1295
+ throw new TypeError("createAi: `generate` was removed in 0.10 \u2014 the AI checks and writes the texts of an email (`proofread`, `rewrite`), not whole emails");
1240
1296
  }
1241
1297
  if (typeof askKey !== "boolean") throw new TypeError("createAi: `askKey` must be true or false");
1242
1298
  if (apiKey && askKey) throw new TypeError("createAi: `apiKey` (one key for all) or `askKey` (each user's own), not both");
@@ -1290,10 +1346,10 @@ ${address}`;
1290
1346
  }
1291
1347
  return modelOf(key ? key.provider.id : provider.id)(request);
1292
1348
  };
1293
- const generator = createEmailGenerator({ model: model2, maxAttempts, compile });
1349
+ const generator = createEmailGenerator({ model: model2, maxAttempts });
1294
1350
  for (const part of PARTS) transport[part] = key ? askedAgain(generator[part], key) : generator[part];
1295
1351
  } else if (model) {
1296
- transport = createEmailGenerator({ model, maxAttempts, compile });
1352
+ transport = createEmailGenerator({ model, maxAttempts });
1297
1353
  } else if (!ownOnly) {
1298
1354
  const { call } = createHttpCaller({ baseUrl, headers, fetch: fetchImpl });
1299
1355
  for (const part of PARTS) {
@@ -1307,9 +1363,9 @@ ${address}`;
1307
1363
  return transport[part] || null;
1308
1364
  };
1309
1365
  return Object.freeze({
1310
- generate: (
1311
- /** @type {GenerateFn|null} */
1312
- resolve("generate")
1366
+ proofread: (
1367
+ /** @type {ProofreadFn|null} */
1368
+ resolve("proofread")
1313
1369
  ),
1314
1370
  rewrite: (
1315
1371
  /** @type {RewriteFn|null} */
@@ -1321,7 +1377,7 @@ ${address}`;
1321
1377
  ),
1322
1378
  key,
1323
1379
  messages: AI_MESSAGES,
1324
- components: Object.freeze({ Generate: GenerateButton, Rewrite: RewriteText, Subjects: SubjectIdeas, Connect: key ? ConnectAi : null })
1380
+ components: Object.freeze({ Proofread: ProofreadButton, Rewrite: RewriteText, Subjects: SubjectIdeas, Connect: key ? ConnectAi : null })
1325
1381
  });
1326
1382
  }
1327
1383
  function providerOf(option) {
@@ -1365,10 +1421,8 @@ export {
1365
1421
  AI_MESSAGES,
1366
1422
  AI_PROVIDERS,
1367
1423
  DEFAULT_CLAUDE_MODEL,
1368
- EMAIL_PURPOSES,
1369
1424
  EmailGenerationError,
1370
1425
  GENERATION_ERRORS,
1371
- PURPOSE_CATEGORIES,
1372
1426
  REWRITE_ACTIONS,
1373
1427
  REWRITE_LANGUAGES,
1374
1428
  agentEmailModel,
@@ -1376,6 +1430,7 @@ export {
1376
1430
  claudeProvider,
1377
1431
  createAi,
1378
1432
  createEmailGenerator,
1433
+ diffWords,
1379
1434
  emailWords,
1380
1435
  failureCode,
1381
1436
  listProviderModels,