best-email-builder 0.5.1 → 0.7.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 (43) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +74 -8
  3. package/dist/ai-server.js +116 -1
  4. package/dist/ai-server.js.map +1 -1
  5. package/dist/ai.js +639 -176
  6. package/dist/ai.js.map +1 -1
  7. package/dist/builder.js +4 -4
  8. package/dist/{chunk-2Y7CDXZM.js → chunk-3RPWH5PC.js} +3 -3
  9. package/dist/{chunk-H4W65GBX.js → chunk-C6WHIBGH.js} +2 -2
  10. package/dist/{chunk-H4W65GBX.js.map → chunk-C6WHIBGH.js.map} +1 -1
  11. package/dist/{chunk-BAA5XMWY.js → chunk-CTZ56YJG.js} +252 -28
  12. package/dist/chunk-CTZ56YJG.js.map +1 -0
  13. package/dist/{chunk-7QQH6MTV.js → chunk-QH5OI43L.js} +4 -4
  14. package/dist/{chunk-N7DEVAIP.js → chunk-QRUMXCGW.js} +2 -2
  15. package/dist/{chunk-2RKVJQSD.js → chunk-UMVBT7NW.js} +3 -3
  16. package/dist/index.js +5 -5
  17. package/dist/library.js +4 -4
  18. package/dist/server.js +4 -4
  19. package/dist/server.js.map +1 -1
  20. package/dist/styles.css +1 -1
  21. package/dist/types/ai/ConnectAi.d.ts +5 -0
  22. package/dist/types/ai/ModelPicker.d.ts +34 -0
  23. package/dist/types/ai/agent.d.ts +31 -0
  24. package/dist/types/ai/agentEndpoint.d.ts +69 -0
  25. package/dist/types/ai/claude.d.ts +31 -5
  26. package/dist/types/ai/createAi.d.ts +5 -3
  27. package/dist/types/ai/draft.d.ts +10 -4
  28. package/dist/types/ai/generator.d.ts +10 -4
  29. package/dist/types/ai/index.d.ts +3 -2
  30. package/dist/types/ai/keys.d.ts +28 -26
  31. package/dist/types/ai/providers.d.ts +58 -1
  32. package/dist/types/ai/rewrite.d.ts +2 -1
  33. package/dist/types/ai/server.d.ts +3 -1
  34. package/dist/types/ai/subjects.d.ts +2 -1
  35. package/dist/types/core/constants.d.ts +1 -1
  36. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  37. package/dist/types/server/handler.d.ts +3 -0
  38. package/package.json +1 -1
  39. package/dist/chunk-BAA5XMWY.js.map +0 -1
  40. /package/dist/{chunk-2Y7CDXZM.js.map → chunk-3RPWH5PC.js.map} +0 -0
  41. /package/dist/{chunk-7QQH6MTV.js.map → chunk-QH5OI43L.js.map} +0 -0
  42. /package/dist/{chunk-N7DEVAIP.js.map → chunk-QRUMXCGW.js.map} +0 -0
  43. /package/dist/{chunk-2RKVJQSD.js.map → chunk-UMVBT7NW.js.map} +0 -0
package/dist/ai.js CHANGED
@@ -23,9 +23,11 @@ import {
23
23
  createHttpCaller,
24
24
  useSearchShortcut,
25
25
  useUi
26
- } from "./chunk-N7DEVAIP.js";
27
- import "./chunk-H4W65GBX.js";
26
+ } from "./chunk-QRUMXCGW.js";
27
+ import "./chunk-C6WHIBGH.js";
28
28
  import {
29
+ AGENT_ERROR_CODES,
30
+ AGENT_PROTOCOL,
29
31
  AI_MESSAGES,
30
32
  AI_PROVIDERS,
31
33
  DEFAULT_CLAUDE_MODEL,
@@ -37,13 +39,17 @@ import {
37
39
  PURPOSE_CATEGORIES,
38
40
  REWRITE_ACTIONS,
39
41
  REWRITE_LANGUAGES,
42
+ agentEmailModel,
40
43
  claudeEmailModel,
44
+ claudeProvider,
41
45
  createEmailGenerator,
42
46
  emailWords,
47
+ failureCode,
48
+ listProviderModels,
43
49
  providerEmailModel,
44
50
  purposesByCategory,
45
51
  resolveProvider
46
- } from "./chunk-BAA5XMWY.js";
52
+ } from "./chunk-CTZ56YJG.js";
47
53
  import "./chunk-OWVXKMLI.js";
48
54
  import "./chunk-W424KPCR.js";
49
55
  import {
@@ -53,6 +59,8 @@ import {
53
59
 
54
60
  // src/ai/keys.js
55
61
  var STORAGE_PREFIX = "best-email-builder:ai-key:";
62
+ var CHOICE_SLOT = "best-email-builder:ai-provider";
63
+ var MODEL_PREFIX = "best-email-builder:ai-model:";
56
64
  function browserStorage() {
57
65
  try {
58
66
  const storage = globalThis.localStorage;
@@ -61,21 +69,33 @@ function browserStorage() {
61
69
  return null;
62
70
  }
63
71
  }
64
- function createKeyStore(provider) {
65
- const slot = STORAGE_PREFIX + provider.id;
66
- let memory = "";
72
+ var choiceOf = (provider) => Object.freeze({
73
+ id: provider.id,
74
+ name: provider.name,
75
+ model: provider.model ?? null,
76
+ keyUrl: provider.keyUrl,
77
+ keyHint: provider.keyHint,
78
+ free: provider.free,
79
+ publicModels: Boolean(provider.publicModels)
80
+ });
81
+ function createKeyStore(providerOrList, { fetch: fetchImpl } = {}) {
82
+ const originals = Array.isArray(providerOrList) ? providerOrList : [providerOrList];
83
+ const providers = Object.freeze(originals.map(choiceOf));
84
+ if (!providers.length) throw new TypeError("createKeyStore: at least one provider");
85
+ const known = (id) => providers.some((provider) => provider.id === id);
86
+ const memory = /* @__PURE__ */ new Map();
67
87
  let refused = false;
68
88
  let pending = null;
69
89
  const listeners = /* @__PURE__ */ new Set();
70
90
  let snapshot;
71
- const read = () => {
91
+ const read = (slot) => {
72
92
  try {
73
93
  return browserStorage()?.getItem(slot) || "";
74
94
  } catch {
75
95
  return "";
76
96
  }
77
97
  };
78
- const write = (value) => {
98
+ const write = (slot, value) => {
79
99
  try {
80
100
  const storage = browserStorage();
81
101
  if (value) storage?.setItem(slot, value);
@@ -83,7 +103,20 @@ function createKeyStore(provider) {
83
103
  } catch {
84
104
  }
85
105
  };
86
- const get = () => memory || read();
106
+ const keyOf = (id) => memory.has(id) ? memory.get(id) : read(STORAGE_PREFIX + id);
107
+ const keep = (id, value) => {
108
+ memory.set(id, value);
109
+ write(STORAGE_PREFIX + id, value);
110
+ };
111
+ const kept = read(CHOICE_SLOT);
112
+ let current = known(kept) ? kept : (providers.find((provider) => keyOf(provider.id)) || providers[0]).id;
113
+ const get = (id) => keyOf(id === void 0 ? current : String(id));
114
+ const providerId = (id) => {
115
+ if (id === void 0) return current;
116
+ if (!known(id)) throw new TypeError(`ai.key: no provider "${id}" \u2014 one of ${providers.map((provider) => provider.id).join(", ")}`);
117
+ return id;
118
+ };
119
+ const lists = /* @__PURE__ */ new Map();
87
120
  const changed = () => {
88
121
  snapshot = Object.freeze({ asking: Boolean(pending), refused, connected: Boolean(get()) });
89
122
  for (const listener of [...listeners]) listener();
@@ -92,9 +125,8 @@ function createKeyStore(provider) {
92
125
  const answer = (key) => {
93
126
  const value = String(key || "").trim();
94
127
  if (value) {
95
- memory = value;
128
+ keep(current, value);
96
129
  refused = false;
97
- write(value);
98
130
  }
99
131
  const waiting = pending;
100
132
  pending = null;
@@ -102,17 +134,57 @@ function createKeyStore(provider) {
102
134
  waiting?.resolve(value);
103
135
  };
104
136
  return Object.freeze({
105
- provider: Object.freeze({ id: provider.id, name: provider.name, keyUrl: provider.keyUrl, keyHint: provider.keyHint, free: provider.free }),
137
+ get provider() {
138
+ return (
139
+ /** @type {ProviderChoice} */
140
+ providers.find((provider) => provider.id === current)
141
+ );
142
+ },
143
+ providers,
106
144
  get,
107
145
  set: (key) => {
108
146
  if (typeof key !== "string" || !key.trim()) throw new TypeError("ai.key.set: a non-empty key");
109
147
  answer(key);
110
148
  },
111
149
  forget: () => {
112
- memory = "";
113
- write("");
150
+ keep(current, "");
114
151
  changed();
115
152
  },
153
+ choose: (id) => {
154
+ if (!known(id)) throw new TypeError(`ai.key.choose: one of ${providers.map((provider) => provider.id).join(", ")}`);
155
+ if (id === current) return;
156
+ current = id;
157
+ refused = false;
158
+ write(CHOICE_SLOT, id);
159
+ changed();
160
+ },
161
+ model: (id) => {
162
+ const choice = (
163
+ /** @type {ProviderChoice} */
164
+ providers.find((provider) => provider.id === providerId(id))
165
+ );
166
+ const kept2 = memory.get(MODEL_PREFIX + choice.id) ?? read(MODEL_PREFIX + choice.id);
167
+ return kept2 || choice.model || "";
168
+ },
169
+ chooseModel: (model, id) => {
170
+ if (typeof model !== "string" || !model.trim()) throw new TypeError("ai.key.chooseModel: a model id");
171
+ const choice = (
172
+ /** @type {ProviderChoice} */
173
+ providers.find((provider) => provider.id === providerId(id))
174
+ );
175
+ const value = model.trim() === choice.model ? "" : model.trim();
176
+ memory.set(MODEL_PREFIX + choice.id, value);
177
+ write(MODEL_PREFIX + choice.id, value);
178
+ changed();
179
+ },
180
+ listModels: async (id, { apiKey, signal } = {}) => {
181
+ const at = providers.findIndex((provider) => provider.id === providerId(id));
182
+ const key = String(apiKey ?? keyOf(providers[at].id)).trim();
183
+ const slot = `${providers[at].id}
184
+ ${key}`;
185
+ if (!lists.has(slot)) lists.set(slot, await listProviderModels({ provider: originals[at], apiKey: key, fetch: fetchImpl, signal }));
186
+ return lists.get(slot);
187
+ },
116
188
  request: () => {
117
189
  if (!pending) {
118
190
  let resolve = () => {
@@ -130,8 +202,7 @@ function createKeyStore(provider) {
130
202
  state: () => snapshot,
131
203
  answer: (key) => answer(key || ""),
132
204
  refuse: () => {
133
- memory = "";
134
- write("");
205
+ keep(current, "");
135
206
  refused = true;
136
207
  changed();
137
208
  }
@@ -139,68 +210,406 @@ function createKeyStore(provider) {
139
210
  }
140
211
 
141
212
  // src/ai/ConnectAi.jsx
142
- import { useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
143
- import { ArrowUpRight, Eye, EyeOff, Gift, KeyRound, ShieldCheck, TriangleAlert } from "lucide-react";
144
- import { jsx, jsxs } from "react/jsx-runtime";
213
+ import { useEffect as useEffect2, useId as useId2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
214
+ import { ArrowUpRight, Eye, EyeOff, Gift as Gift2, KeyRound, ShieldCheck, TriangleAlert } from "lucide-react";
215
+
216
+ // src/ai/ModelPicker.jsx
217
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
218
+ import { Check, ChevronDown, Gift, LoaderCircle, Search } from "lucide-react";
219
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
220
+ function ModelPicker({ aiKey, provider, apiKey, delay = 0, value, onChange, open, onOpenChange }) {
221
+ const { t } = useUi();
222
+ const [list, setList] = useState(
223
+ /** @type {{status: 'loading'|'ready'|'needs-key'|'refused'|'failed', models: import('./providers.js').ModelChoice[]}} */
224
+ { status: "loading", models: [] }
225
+ );
226
+ const [query, setQuery] = useState("");
227
+ const [freeOnly, setFreeOnly] = useState(false);
228
+ const [active, setActive] = useState(0);
229
+ const triggerRef = useRef(
230
+ /** @type {HTMLButtonElement|null} */
231
+ null
232
+ );
233
+ const listRef = useRef(
234
+ /** @type {HTMLDivElement|null} */
235
+ null
236
+ );
237
+ const labelId = useId();
238
+ const valueId = useId();
239
+ const listId = useId();
240
+ const hintId = useId();
241
+ useEffect(() => {
242
+ if (!apiKey && !provider.publicModels) {
243
+ setList({ status: "needs-key", models: [] });
244
+ return void 0;
245
+ }
246
+ const controller = new AbortController();
247
+ setList({ status: "loading", models: [] });
248
+ const timer = setTimeout(() => {
249
+ aiKey.listModels(provider.id, { apiKey, signal: controller.signal }).then(
250
+ (models2) => !controller.signal.aborted && setList({ status: "ready", models: models2 }),
251
+ (error) => !controller.signal.aborted && setList({ status: error?.code === "KEY_REFUSED" ? "refused" : "failed", models: [] })
252
+ );
253
+ }, delay);
254
+ return () => {
255
+ clearTimeout(timer);
256
+ controller.abort();
257
+ };
258
+ }, [aiKey, provider.id, provider.publicModels, apiKey, delay]);
259
+ useEffect(() => {
260
+ if (!open) return;
261
+ setQuery("");
262
+ setFreeOnly(false);
263
+ }, [open]);
264
+ const models = useMemo(() => {
265
+ const listed = new Map(list.models.map((model) => [model.id, model]));
266
+ const seen = /* @__PURE__ */ new Set();
267
+ const all = [];
268
+ const add = (model) => {
269
+ if (model.id && !seen.has(model.id)) seen.add(model.id) && all.push(model);
270
+ };
271
+ for (const id of [provider.model, value]) if (id) add(listed.get(id) || { id, name: id, free: false });
272
+ list.models.forEach(add);
273
+ return all;
274
+ }, [list.models, provider.model, value]);
275
+ const hasFree = list.models.some((model) => model.free);
276
+ const typed = query.trim();
277
+ const matches = useMemo(() => {
278
+ const words = typed.toLowerCase().split(/\s+/).filter(Boolean);
279
+ return models.filter((model) => (!freeOnly || model.free) && words.every((word) => `${model.id} ${model.name}`.toLowerCase().includes(word)));
280
+ }, [models, typed, freeOnly]);
281
+ const custom = typed && !/\s/.test(typed) && !models.some((model) => model.id === typed) ? typed : "";
282
+ const count = matches.length + (custom ? 1 : 0);
283
+ const current = models.find((model) => model.id === value);
284
+ useEffect(() => setActive(Math.max(0, matches.findIndex((model) => model.id === value))), [matches, value]);
285
+ useEffect(() => {
286
+ if (open) listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
287
+ }, [active, open]);
288
+ const close = () => {
289
+ triggerRef.current?.focus();
290
+ onOpenChange(false);
291
+ };
292
+ const pick = (id) => {
293
+ onChange(id);
294
+ close();
295
+ };
296
+ const onKeyDown = (event) => {
297
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
298
+ event.preventDefault();
299
+ setActive((at) => count ? (at + (event.key === "ArrowDown" ? 1 : -1) + count) % count : 0);
300
+ } else if (event.key === "Enter") {
301
+ event.preventDefault();
302
+ const id = active < matches.length ? matches[active]?.id : custom;
303
+ if (id) pick(id);
304
+ }
305
+ };
306
+ const optionId = (at) => `${listId}-${at}`;
307
+ const badges = (model) => /* @__PURE__ */ jsxs(Fragment, { children: [
308
+ model.free && /* @__PURE__ */ jsxs("span", { className: "eb:inline-flex eb:shrink-0 eb:items-center eb:gap-1 eb:rounded-full eb:bg-success/10 eb:px-1.5 eb:py-px eb:text-[10.5px] eb:font-medium eb:text-success", children: [
309
+ /* @__PURE__ */ jsx(Gift, { className: "eb:size-3", "aria-hidden": "true" }),
310
+ t("Free")
311
+ ] }),
312
+ model.id === provider.model && /* @__PURE__ */ jsx("span", { className: "eb:shrink-0 eb:rounded-full eb:bg-muted eb:px-1.5 eb:py-px eb:text-[10.5px] eb:font-medium eb:text-foreground-lighter", children: t("Default") })
313
+ ] });
314
+ const status = {
315
+ loading: /* @__PURE__ */ jsxs(Fragment, { children: [
316
+ /* @__PURE__ */ jsx(LoaderCircle, { className: "eb:size-3 eb:shrink-0 eb:animate-spin", "aria-hidden": "true" }),
317
+ t("Loading the models of {provider}\u2026", { provider: provider.name })
318
+ ] }),
319
+ "needs-key": t("Paste the key to see all the models of {provider}.", { provider: provider.name }),
320
+ refused: /* @__PURE__ */ jsx("span", { className: "eb:text-warning", children: t("{provider} refused this key: its models could not be listed.", { provider: provider.name }) }),
321
+ failed: t("The list of models could not be loaded. The default model works, or type the id of another."),
322
+ ready: list.models.length ? hasFree ? t("{count} models, the free ones first.", { count: list.models.length }) : t("{count} models.", { count: list.models.length }) : t("{provider} lists no models here: type the id of the one you want.", { provider: provider.name })
323
+ }[list.status];
324
+ return /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", "data-testid": "eb-ai-connect-model-field", children: [
325
+ /* @__PURE__ */ jsx("span", { id: labelId, className: "eb:text-[12.5px] eb:leading-none eb:font-medium eb:text-foreground", children: t("Model") }),
326
+ /* @__PURE__ */ jsxs(
327
+ "button",
328
+ {
329
+ ref: triggerRef,
330
+ type: "button",
331
+ "aria-haspopup": "listbox",
332
+ "aria-expanded": open,
333
+ "aria-labelledby": `${labelId} ${valueId}`,
334
+ "aria-describedby": hintId,
335
+ onClick: () => onOpenChange(!open),
336
+ className: cn(
337
+ "eb:flex eb:h-9 eb:w-full eb:min-w-0 eb:items-center eb:gap-2 eb:rounded-md eb:border eb:border-input eb:bg-transparent eb:px-3 eb:text-left eb:shadow-xs eb:transition-[color,box-shadow] eb:outline-none eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
338
+ open && "eb:border-ring"
339
+ ),
340
+ "data-testid": "eb-ai-connect-model",
341
+ children: [
342
+ /* @__PURE__ */ jsx("span", { id: valueId, className: "eb:min-w-0 eb:truncate eb:font-mono eb:text-[12.5px] eb:text-foreground", children: value || t("Choose a model") }),
343
+ current && badges(current),
344
+ /* @__PURE__ */ jsx(ChevronDown, { className: cn("eb:ml-auto eb:size-4 eb:shrink-0 eb:text-foreground-muted eb:transition-transform", open && "eb:rotate-180"), "aria-hidden": "true" })
345
+ ]
346
+ }
347
+ ),
348
+ open && /* @__PURE__ */ jsxs(
349
+ "div",
350
+ {
351
+ className: "eb:flex eb:flex-col eb:overflow-hidden eb:rounded-md eb:border eb:border-border eb:bg-background",
352
+ onKeyDown: (event) => event.key === "Escape" && close(),
353
+ "data-eb-models": "",
354
+ "data-testid": "eb-ai-connect-models",
355
+ children: [
356
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:items-center eb:gap-2 eb:border-b eb:border-border eb:p-2", children: [
357
+ /* @__PURE__ */ jsxs("div", { className: "eb:relative eb:min-w-0 eb:flex-1", children: [
358
+ /* @__PURE__ */ jsx(Search, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted", "aria-hidden": "true" }),
359
+ /* @__PURE__ */ jsx(
360
+ "input",
361
+ {
362
+ autoFocus: true,
363
+ role: "combobox",
364
+ "aria-expanded": "true",
365
+ "aria-controls": listId,
366
+ "aria-autocomplete": "list",
367
+ "aria-activedescendant": count ? optionId(active) : void 0,
368
+ "aria-label": t("Search the models"),
369
+ value: query,
370
+ onChange: (event) => setQuery(event.target.value),
371
+ onKeyDown,
372
+ placeholder: t("Search the models\u2026"),
373
+ autoComplete: "off",
374
+ autoCapitalize: "off",
375
+ autoCorrect: "off",
376
+ spellCheck: false,
377
+ className: "eb:h-8 eb:w-full eb:rounded-md eb:bg-surface-75 eb:pr-2 eb:pl-7 eb:text-[13px] eb:text-foreground eb:outline-none eb:placeholder:text-foreground-muted eb:focus-visible:ring-2 eb:focus-visible:ring-ring/40",
378
+ "data-testid": "eb-ai-connect-model-search"
379
+ }
380
+ )
381
+ ] }),
382
+ hasFree && /* @__PURE__ */ jsxs(
383
+ "button",
384
+ {
385
+ type: "button",
386
+ "aria-pressed": freeOnly,
387
+ onClick: () => setFreeOnly((on) => !on),
388
+ className: cn(
389
+ "eb:inline-flex eb:h-8 eb:shrink-0 eb:items-center eb:gap-1.5 eb:rounded-md eb:border eb:px-2.5 eb:text-[12px] eb:font-medium eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
390
+ freeOnly ? "eb:border-success/40 eb:bg-success/10 eb:text-success" : "eb:border-border eb:text-foreground-lighter eb:hover:bg-muted"
391
+ ),
392
+ children: [
393
+ /* @__PURE__ */ jsx(Gift, { className: "eb:size-3.5", "aria-hidden": "true" }),
394
+ t("Free only")
395
+ ]
396
+ }
397
+ )
398
+ ] }),
399
+ /* @__PURE__ */ jsxs("div", { ref: listRef, id: listId, role: "listbox", "aria-labelledby": labelId, className: "eb:max-h-56 eb:overflow-y-auto eb:p-1", children: [
400
+ matches.map((model, at) => /* @__PURE__ */ jsxs(
401
+ "button",
402
+ {
403
+ id: optionId(at),
404
+ type: "button",
405
+ role: "option",
406
+ tabIndex: -1,
407
+ "aria-selected": model.id === value,
408
+ "data-active": at === active,
409
+ "data-model": model.id,
410
+ onClick: () => pick(model.id),
411
+ onMouseMove: () => at !== active && setActive(at),
412
+ className: cn(
413
+ "eb:flex eb:w-full eb:min-w-0 eb:items-start eb:gap-2 eb:rounded-md eb:px-2 eb:py-1.5 eb:text-left",
414
+ at === active ? "eb:bg-accent eb:text-accent-foreground" : ""
415
+ ),
416
+ children: [
417
+ /* @__PURE__ */ jsx(Check, { className: cn("eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-primary", model.id !== value && "eb:invisible"), "aria-hidden": "true" }),
418
+ /* @__PURE__ */ jsxs("span", { className: "eb:flex eb:min-w-0 eb:flex-1 eb:flex-col eb:gap-0.5", children: [
419
+ /* @__PURE__ */ jsxs("span", { className: "eb:flex eb:min-w-0 eb:items-center eb:gap-1.5", children: [
420
+ /* @__PURE__ */ jsx("span", { className: "eb:truncate eb:text-[13px] eb:text-foreground", children: model.name }),
421
+ badges(model)
422
+ ] }),
423
+ model.name !== model.id && /* @__PURE__ */ jsx("span", { className: "eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", children: model.id })
424
+ ] })
425
+ ]
426
+ },
427
+ model.id
428
+ )),
429
+ custom && /* @__PURE__ */ jsxs(
430
+ "button",
431
+ {
432
+ id: optionId(matches.length),
433
+ type: "button",
434
+ role: "option",
435
+ tabIndex: -1,
436
+ "aria-selected": false,
437
+ "data-active": active === matches.length,
438
+ onClick: () => pick(custom),
439
+ onMouseMove: () => active !== matches.length && setActive(matches.length),
440
+ className: cn(
441
+ "eb:flex eb:w-full eb:min-w-0 eb:items-center eb:gap-2 eb:rounded-md eb:px-2 eb:py-1.5 eb:text-left eb:text-[13px] eb:text-primary",
442
+ active === matches.length ? "eb:bg-accent" : ""
443
+ ),
444
+ "data-testid": "eb-ai-connect-model-custom",
445
+ children: [
446
+ /* @__PURE__ */ jsx("span", { className: "eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
447
+ /* @__PURE__ */ jsx("span", { className: "eb:min-w-0 eb:truncate", children: t("Use \u201C{model}\u201D", { model: custom }) })
448
+ ]
449
+ }
450
+ ),
451
+ !count && /* @__PURE__ */ jsx("p", { className: "eb:px-2 eb:py-3 eb:text-center eb:text-[12px] eb:text-foreground-muted", children: t("No model matches.") })
452
+ ] })
453
+ ]
454
+ }
455
+ ),
456
+ /* @__PURE__ */ jsx("p", { id: hintId, role: "status", className: "eb:flex eb:min-h-4 eb:items-center eb:gap-1.5 eb:text-[11.5px] eb:text-foreground-muted", "data-testid": "eb-ai-connect-models-status", children: status })
457
+ ] });
458
+ }
459
+
460
+ // src/ai/ConnectAi.jsx
461
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
462
+ var LISTABLE_KEY = 20;
145
463
  function ConnectAi({ aiKey }) {
146
464
  const { t } = useUi();
147
465
  const state = useSyncExternalStore(aiKey.subscribe, aiKey.state, aiKey.state);
148
- const [value, setValue] = useState("");
149
- const [shown, setShown] = useState(false);
150
- const inputRef = useRef(
466
+ const [value, setValue] = useState2("");
467
+ const [shown, setShown] = useState2(false);
468
+ const [chosenId, setChosenId] = useState2(aiKey.provider.id);
469
+ const [picks, setPicks] = useState2(
470
+ /** @type {Record<string, string>} */
471
+ {}
472
+ );
473
+ const [modelsOpen, setModelsOpen] = useState2(false);
474
+ const inputRef = useRef2(
151
475
  /** @type {HTMLInputElement|null} */
152
476
  null
153
477
  );
154
- const inputId = useId();
155
- const hintId = useId();
156
- const { name, keyUrl, keyHint, free } = aiKey.provider;
157
- useEffect(() => {
478
+ const choicesRef = useRef2(
479
+ /** @type {HTMLDivElement|null} */
480
+ null
481
+ );
482
+ const inputId = useId2();
483
+ const hintId = useId2();
484
+ const choices = aiKey.providers;
485
+ const chosen = choices.find((provider) => provider.id === chosenId) || aiKey.provider;
486
+ const { name, keyUrl, keyHint, free } = chosen;
487
+ useEffect2(() => {
158
488
  if (!state.asking) return;
489
+ setChosenId(aiKey.provider.id);
159
490
  setValue("");
160
491
  setShown(false);
161
- }, [state.asking]);
492
+ setPicks({});
493
+ setModelsOpen(false);
494
+ }, [state.asking, aiKey]);
162
495
  const key = value.trim();
496
+ const saved = aiKey.get(chosen.id);
163
497
  const unusual = Boolean(key && keyHint && !key.startsWith(keyHint));
498
+ const refused = state.refused && chosen.id === aiKey.provider.id;
499
+ const modelFor = (id) => picks[id] ?? aiKey.model(id);
164
500
  const submit = (event) => {
165
501
  event.preventDefault();
166
- if (key) aiKey.answer(key);
502
+ const answer = key || saved;
503
+ if (!answer) return;
504
+ if (chosen.id !== aiKey.provider.id) aiKey.choose(chosen.id);
505
+ if (picks[chosen.id] && picks[chosen.id] !== aiKey.model(chosen.id)) aiKey.chooseModel(picks[chosen.id], chosen.id);
506
+ aiKey.answer(answer);
167
507
  };
168
- return /* @__PURE__ */ jsx(Dialog, { open: state.asking, onOpenChange: (open) => !open && aiKey.answer(null), children: /* @__PURE__ */ jsx(
508
+ const choose = (id) => {
509
+ setChosenId(id);
510
+ setValue("");
511
+ setModelsOpen(false);
512
+ };
513
+ const onChoicesKeyDown = (event) => {
514
+ const at = choices.findIndex((provider) => provider.id === chosen.id);
515
+ const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
516
+ const to = step ? (at + step + choices.length) % choices.length : event.key === "Home" ? 0 : event.key === "End" ? choices.length - 1 : -1;
517
+ if (to === -1) return;
518
+ event.preventDefault();
519
+ choose(choices[to].id);
520
+ choicesRef.current?.querySelectorAll('[role="radio"]')[to]?.focus();
521
+ };
522
+ return /* @__PURE__ */ jsx2(Dialog, { open: state.asking, onOpenChange: (open) => !open && aiKey.answer(null), children: /* @__PURE__ */ jsx2(
169
523
  DialogContent,
170
524
  {
171
- className: "eb:gap-0 eb:overflow-hidden eb:p-0 eb:sm:max-w-md",
525
+ className: cn("eb:max-h-[calc(100dvh-2rem)] eb:gap-0 eb:overflow-y-auto eb:p-0", choices.length > 1 ? "eb:sm:max-w-xl" : "eb:sm:max-w-md"),
172
526
  "data-testid": "eb-ai-connect",
173
527
  onOpenAutoFocus: (event) => {
174
528
  event.preventDefault();
175
529
  inputRef.current?.focus();
176
530
  },
177
- children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, children: [
178
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-5 eb:px-6 eb:pt-6 eb:pb-5", children: [
179
- /* @__PURE__ */ jsxs(DialogHeader, { className: "eb:flex-row eb:items-start eb:gap-3.5 eb:pr-6", children: [
180
- /* @__PURE__ */ jsx("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-full eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx(KeyRound, { className: "eb:size-5", "aria-hidden": "true" }) }),
181
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
182
- /* @__PURE__ */ jsx(DialogTitle, { children: t("Connect {provider}", { provider: name }) }),
183
- /* @__PURE__ */ jsx(DialogDescription, { className: "eb:text-[13px] eb:leading-relaxed eb:text-foreground-lighter", children: t("The AI writes with your own {provider} key. Connect it once: this browser remembers it.", { provider: name }) })
531
+ onEscapeKeyDown: (event) => {
532
+ if (!modelsOpen) return;
533
+ event.preventDefault();
534
+ if (!(event.target instanceof Element && event.target.closest("[data-eb-models]"))) setModelsOpen(false);
535
+ },
536
+ children: /* @__PURE__ */ jsxs2("form", { onSubmit: submit, className: "eb:min-w-0", children: [
537
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-5 eb:px-6 eb:pt-6 eb:pb-5", children: [
538
+ /* @__PURE__ */ jsxs2(DialogHeader, { className: "eb:flex-row eb:items-start eb:gap-3.5 eb:pr-6", children: [
539
+ /* @__PURE__ */ jsx2("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-full eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx2(KeyRound, { className: "eb:size-5", "aria-hidden": "true" }) }),
540
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
541
+ /* @__PURE__ */ jsx2(DialogTitle, { children: t("Connect {provider}", { provider: name }) }),
542
+ /* @__PURE__ */ jsx2(DialogDescription, { className: "eb:text-[13px] eb:leading-relaxed eb:text-foreground-lighter", children: t("The AI writes with your own {provider} key. Connect it once: this browser remembers it.", { provider: name }) })
184
543
  ] })
185
544
  ] }),
186
- state.refused && /* @__PURE__ */ jsxs("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-2 eb:rounded-md eb:border eb:border-destructive/30 eb:bg-destructive/10 eb:px-3 eb:py-2 eb:text-[12.5px] eb:text-destructive", children: [
187
- /* @__PURE__ */ jsx(TriangleAlert, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
545
+ choices.length > 1 && /* @__PURE__ */ jsx2(
546
+ "div",
547
+ {
548
+ ref: choicesRef,
549
+ role: "radiogroup",
550
+ "aria-label": t("AI provider"),
551
+ className: "eb:grid eb:grid-cols-2 eb:gap-1.5 eb:sm:grid-cols-3",
552
+ onKeyDown: onChoicesKeyDown,
553
+ "data-testid": "eb-ai-connect-providers",
554
+ children: choices.map((provider) => {
555
+ const checked = provider.id === chosen.id;
556
+ return /* @__PURE__ */ jsxs2(
557
+ "button",
558
+ {
559
+ type: "button",
560
+ role: "radio",
561
+ "aria-checked": checked,
562
+ tabIndex: checked ? 0 : -1,
563
+ onClick: () => choose(provider.id),
564
+ className: cn(
565
+ "eb:flex eb:min-w-0 eb:flex-col eb:gap-0.5 eb:rounded-md eb:border eb:px-2.5 eb:py-2 eb:text-left eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
566
+ checked ? "eb:border-primary eb:bg-primary/10" : "eb:border-border eb:hover:bg-muted"
567
+ ),
568
+ children: [
569
+ /* @__PURE__ */ jsxs2("span", { className: "eb:flex eb:items-center eb:gap-1.5 eb:text-[13px] eb:leading-tight eb:font-medium eb:text-foreground", children: [
570
+ /* @__PURE__ */ jsx2("span", { className: "eb:truncate", children: provider.name }),
571
+ provider.free && /* @__PURE__ */ jsx2(Gift2, { className: "eb:size-3 eb:shrink-0 eb:text-success", "aria-label": t(provider.free) }),
572
+ aiKey.get(provider.id) && /* @__PURE__ */ jsx2("span", { className: "eb:ml-auto eb:size-1.5 eb:shrink-0 eb:rounded-full eb:bg-success", title: t("Key saved"), "aria-label": t("Key saved") })
573
+ ] }),
574
+ modelFor(provider.id) && /* @__PURE__ */ jsx2("span", { className: "eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", title: modelFor(provider.id), children: modelFor(provider.id) })
575
+ ]
576
+ },
577
+ provider.id
578
+ );
579
+ })
580
+ }
581
+ ),
582
+ refused && /* @__PURE__ */ jsxs2("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-2 eb:rounded-md eb:border eb:border-destructive/30 eb:bg-destructive/10 eb:px-3 eb:py-2 eb:text-[12.5px] eb:text-destructive", children: [
583
+ /* @__PURE__ */ jsx2(TriangleAlert, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
188
584
  t("{provider} refused the last key. Paste another one.", { provider: name })
189
585
  ] }),
190
- (keyUrl || free) && /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-wrap eb:items-center eb:gap-x-3 eb:gap-y-2", children: [
191
- keyUrl && /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", size: "sm-compact", children: /* @__PURE__ */ jsxs("a", { href: keyUrl, target: "_blank", rel: "noopener noreferrer", "data-testid": "eb-ai-connect-get-key", children: [
192
- t("Get a key"),
193
- /* @__PURE__ */ jsx(ArrowUpRight, { "aria-hidden": "true" })
194
- ] }) }),
195
- free && /* @__PURE__ */ jsxs("span", { className: "eb:inline-flex eb:items-center eb:gap-1.5 eb:rounded-full eb:bg-success/10 eb:px-2.5 eb:py-1 eb:text-[11.5px] eb:font-medium eb:text-success", children: [
196
- /* @__PURE__ */ jsx(Gift, { className: "eb:size-3.5", "aria-hidden": "true" }),
586
+ (keyUrl || free) && /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-wrap eb:items-center eb:gap-x-3 eb:gap-y-2", children: [
587
+ keyUrl && /* @__PURE__ */ jsx2(Button, { asChild: true, variant: "outline", size: "sm-compact", children: /* @__PURE__ */ jsxs2(
588
+ "a",
589
+ {
590
+ href: keyUrl,
591
+ target: "_blank",
592
+ rel: "noopener noreferrer",
593
+ onClick: (event) => {
594
+ event.preventDefault();
595
+ window.open(keyUrl, "_blank", "noopener,noreferrer");
596
+ },
597
+ "data-testid": "eb-ai-connect-get-key",
598
+ children: [
599
+ t("Get a key"),
600
+ /* @__PURE__ */ jsx2(ArrowUpRight, { "aria-hidden": "true" })
601
+ ]
602
+ }
603
+ ) }),
604
+ free && /* @__PURE__ */ jsxs2("span", { className: "eb:inline-flex eb:items-center eb:gap-1.5 eb:rounded-full eb:bg-success/10 eb:px-2.5 eb:py-1 eb:text-[11.5px] eb:font-medium eb:text-success", children: [
605
+ /* @__PURE__ */ jsx2(Gift2, { className: "eb:size-3.5", "aria-hidden": "true" }),
197
606
  t(free)
198
607
  ] })
199
608
  ] }),
200
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
201
- /* @__PURE__ */ jsx(Label, { htmlFor: inputId, children: t("API key") }),
202
- /* @__PURE__ */ jsxs("div", { className: "eb:relative", children: [
203
- /* @__PURE__ */ jsx(
609
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
610
+ /* @__PURE__ */ jsx2(Label, { htmlFor: inputId, children: t("API key") }),
611
+ /* @__PURE__ */ jsxs2("div", { className: "eb:relative", children: [
612
+ /* @__PURE__ */ jsx2(
204
613
  Input,
205
614
  {
206
615
  ref: inputRef,
@@ -218,7 +627,7 @@ function ConnectAi({ aiKey }) {
218
627
  "data-testid": "eb-ai-connect-key"
219
628
  }
220
629
  ),
221
- /* @__PURE__ */ jsx(
630
+ /* @__PURE__ */ jsx2(
222
631
  "button",
223
632
  {
224
633
  type: "button",
@@ -226,20 +635,33 @@ function ConnectAi({ aiKey }) {
226
635
  onClick: () => setShown((current) => !current),
227
636
  "aria-label": shown ? t("Hide the key") : t("Show the key"),
228
637
  "aria-pressed": shown,
229
- children: shown ? /* @__PURE__ */ jsx(EyeOff, { className: "eb:size-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { className: "eb:size-4", "aria-hidden": "true" })
638
+ children: shown ? /* @__PURE__ */ jsx2(EyeOff, { className: "eb:size-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx2(Eye, { className: "eb:size-4", "aria-hidden": "true" })
230
639
  }
231
640
  )
232
641
  ] }),
233
- /* @__PURE__ */ jsx("p", { id: hintId, className: "eb:min-h-4 eb:text-[11.5px] eb:text-foreground-muted", children: unusual ? /* @__PURE__ */ jsx("span", { className: "eb:text-warning", children: t("{provider} keys usually start with {prefix}.", { provider: name, prefix: keyHint }) }) : keyHint ? t("{provider} keys start with {prefix}.", { provider: name, prefix: keyHint }) : null })
234
- ] })
642
+ /* @__PURE__ */ jsx2("p", { id: hintId, className: "eb:min-h-4 eb:text-[11.5px] eb:text-foreground-muted", children: unusual ? /* @__PURE__ */ jsx2("span", { className: "eb:text-warning", children: t("{provider} keys usually start with {prefix}.", { provider: name, prefix: keyHint }) }) : saved && !key ? t("A key for {provider} is saved in this browser: Connect uses it, or paste another one.", { provider: name }) : keyHint ? t("{provider} keys start with {prefix}.", { provider: name, prefix: keyHint }) : null })
643
+ ] }),
644
+ /* @__PURE__ */ jsx2(
645
+ ModelPicker,
646
+ {
647
+ aiKey,
648
+ provider: chosen,
649
+ apiKey: key.length >= LISTABLE_KEY ? key : saved,
650
+ delay: key.length >= LISTABLE_KEY ? 350 : 0,
651
+ value: modelFor(chosen.id),
652
+ onChange: (model) => setPicks((current) => ({ ...current, [chosen.id]: model })),
653
+ open: modelsOpen,
654
+ onOpenChange: setModelsOpen
655
+ }
656
+ )
235
657
  ] }),
236
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:items-start eb:gap-2 eb:border-t eb:border-border eb:bg-surface-75 eb:px-6 eb:py-3 eb:text-[11.5px] eb:leading-relaxed eb:text-foreground-lighter", children: [
237
- /* @__PURE__ */ jsx(ShieldCheck, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted", "aria-hidden": "true" }),
238
- /* @__PURE__ */ jsx("span", { children: t("The key stays in this browser and is sent only to {provider}.", { provider: name }) })
658
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:items-start eb:gap-2 eb:border-t eb:border-border eb:bg-surface-75 eb:px-6 eb:py-3 eb:text-[11.5px] eb:leading-relaxed eb:text-foreground-lighter", children: [
659
+ /* @__PURE__ */ jsx2(ShieldCheck, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted", "aria-hidden": "true" }),
660
+ /* @__PURE__ */ jsx2("span", { children: t("The key stays in this browser and is sent only to {provider}.", { provider: name }) })
239
661
  ] }),
240
- /* @__PURE__ */ jsxs(DialogFooter, { className: "eb:border-t eb:border-border eb:px-6 eb:py-4", children: [
241
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", onClick: () => aiKey.answer(null), children: t("Cancel") }),
242
- /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !key, "data-testid": "eb-ai-connect-submit", children: t("Connect") })
662
+ /* @__PURE__ */ jsxs2(DialogFooter, { className: "eb:border-t eb:border-border eb:px-6 eb:py-4", children: [
663
+ /* @__PURE__ */ jsx2(Button, { type: "button", variant: "ghost", onClick: () => aiKey.answer(null), children: t("Cancel") }),
664
+ /* @__PURE__ */ jsx2(Button, { type: "submit", disabled: !key && !saved, "data-testid": "eb-ai-connect-submit", children: t("Connect") })
243
665
  ] })
244
666
  ] })
245
667
  }
@@ -247,8 +669,8 @@ function ConnectAi({ aiKey }) {
247
669
  }
248
670
 
249
671
  // src/ai/GenerateButton.jsx
250
- import { useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
251
- import { Check, Loader2, Search, Sparkles, TriangleAlert as TriangleAlert2 } from "lucide-react";
672
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
673
+ import { Check as Check2, Loader2, Search as Search2, Sparkles, TriangleAlert as TriangleAlert2 } from "lucide-react";
252
674
 
253
675
  // src/ai/chip.js
254
676
  var chipClassName = (selected = false) => cn(
@@ -257,39 +679,39 @@ var chipClassName = (selected = false) => cn(
257
679
  );
258
680
 
259
681
  // src/ai/GenerateButton.jsx
260
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
682
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
261
683
  var fold = (text) => text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
262
684
  function GenerateButton({ context, generate: generateEmail }) {
263
685
  const { editor, ready, isDirty, markDirty, template, setMeta, variables, sender, brand } = context;
264
686
  const { t, locale, notify } = useUi();
265
- const [open, setOpen] = useState2(false);
266
- const [purpose, setPurpose] = useState2(
687
+ const [open, setOpen] = useState3(false);
688
+ const [purpose, setPurpose] = useState3(
267
689
  /** @type {string|null} */
268
690
  null
269
691
  );
270
- const [details, setDetails] = useState2("");
271
- const [query, setQuery] = useState2("");
272
- const [busy, setBusy] = useState2(false);
273
- const [error, setError] = useState2("");
274
- const [seconds, setSeconds] = useState2(0);
275
- const [dirty, setDirty] = useState2(false);
276
- const abortRef = useRef2(
692
+ const [details, setDetails] = useState3("");
693
+ const [query, setQuery] = useState3("");
694
+ const [busy, setBusy] = useState3(false);
695
+ const [error, setError] = useState3("");
696
+ const [seconds, setSeconds] = useState3(0);
697
+ const [dirty, setDirty] = useState3(false);
698
+ const abortRef = useRef3(
277
699
  /** @type {AbortController|null} */
278
700
  null
279
701
  );
280
- const searchRef = useRef2(
702
+ const searchRef = useRef3(
281
703
  /** @type {HTMLInputElement|null} */
282
704
  null
283
705
  );
284
706
  useSearchShortcut(searchRef);
285
- useEffect2(() => () => abortRef.current?.abort(), []);
286
- useEffect2(() => {
707
+ useEffect3(() => () => abortRef.current?.abort(), []);
708
+ useEffect3(() => {
287
709
  if (!busy) return void 0;
288
710
  setSeconds(0);
289
711
  const timer = setInterval(() => setSeconds((s) => s + 1), 1e3);
290
712
  return () => clearInterval(timer);
291
713
  }, [busy]);
292
- const groups = useMemo(() => {
714
+ const groups = useMemo2(() => {
293
715
  const words = fold(query.trim()).split(/\s+/).filter(Boolean);
294
716
  return purposesByCategory().map((group) => ({
295
717
  ...group,
@@ -350,12 +772,12 @@ function GenerateButton({ context, generate: generateEmail }) {
350
772
  setBusy(false);
351
773
  };
352
774
  const canGenerate = Boolean(purpose || details.trim());
353
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
354
- /* @__PURE__ */ jsxs2(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: openDialog, children: [
355
- /* @__PURE__ */ jsx2(Sparkles, {}),
775
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
776
+ /* @__PURE__ */ jsxs3(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: openDialog, children: [
777
+ /* @__PURE__ */ jsx3(Sparkles, {}),
356
778
  t("Generate")
357
779
  ] }),
358
- /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange: close, children: /* @__PURE__ */ jsxs2(
780
+ /* @__PURE__ */ jsx3(Dialog, { open, onOpenChange: close, children: /* @__PURE__ */ jsxs3(
359
781
  DialogContent,
360
782
  {
361
783
  className: "eb:flex eb:max-h-[calc(100dvh-2rem)] eb:flex-col eb:gap-0 eb:p-0 eb:sm:max-w-2xl",
@@ -365,13 +787,13 @@ function GenerateButton({ context, generate: generateEmail }) {
365
787
  searchRef.current?.focus();
366
788
  },
367
789
  children: [
368
- /* @__PURE__ */ jsxs2(DialogHeader, { className: "eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-3", children: [
369
- /* @__PURE__ */ jsx2(DialogTitle, { children: t("Generate an email") }),
370
- /* @__PURE__ */ jsx2(DialogDescription, { children: t("Pick what it is for. AI writes a complete email that opens here, ready to edit.") })
790
+ /* @__PURE__ */ jsxs3(DialogHeader, { className: "eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-3", children: [
791
+ /* @__PURE__ */ jsx3(DialogTitle, { children: t("Generate an email") }),
792
+ /* @__PURE__ */ jsx3(DialogDescription, { children: t("Pick what it is for. AI writes a complete email that opens here, ready to edit.") })
371
793
  ] }),
372
- /* @__PURE__ */ jsx2("div", { className: "eb:px-6 eb:pb-3", children: /* @__PURE__ */ jsxs2("div", { className: "eb:relative", children: [
373
- /* @__PURE__ */ jsx2(Search, { 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" }),
374
- /* @__PURE__ */ jsx2(
794
+ /* @__PURE__ */ jsx3("div", { className: "eb:px-6 eb:pb-3", children: /* @__PURE__ */ jsxs3("div", { className: "eb:relative", children: [
795
+ /* @__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" }),
796
+ /* @__PURE__ */ jsx3(
375
797
  Input,
376
798
  {
377
799
  ref: searchRef,
@@ -385,16 +807,16 @@ function GenerateButton({ context, generate: generateEmail }) {
385
807
  disabled: busy
386
808
  }
387
809
  ),
388
- /* @__PURE__ */ jsx2(SearchKeyHint, { className: "eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2" })
810
+ /* @__PURE__ */ jsx3(SearchKeyHint, { className: "eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2" })
389
811
  ] }) }),
390
- /* @__PURE__ */ jsx2("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__ */ jsxs2("fieldset", { disabled: busy, className: "eb:m-0 eb:min-w-0 eb:border-0 eb:p-0", children: [
391
- /* @__PURE__ */ jsx2("legend", { className: "eb:sr-only", children: t("What is the email for?") }),
392
- groups.length === 0 && /* @__PURE__ */ jsx2("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.") }),
393
- /* @__PURE__ */ jsx2("div", { className: "eb:flex eb:flex-col eb:gap-4", children: groups.map((group) => /* @__PURE__ */ jsxs2("section", { className: "eb:flex eb:flex-col eb:gap-2", "aria-label": t(group.label), children: [
394
- /* @__PURE__ */ jsx2(SectionLabel, { children: t(group.label) }),
395
- /* @__PURE__ */ jsx2("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: group.purposes.map((entry) => {
812
+ /* @__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: [
813
+ /* @__PURE__ */ jsx3("legend", { className: "eb:sr-only", children: t("What is the email for?") }),
814
+ 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.") }),
815
+ /* @__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: [
816
+ /* @__PURE__ */ jsx3(SectionLabel, { children: t(group.label) }),
817
+ /* @__PURE__ */ jsx3("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: group.purposes.map((entry) => {
396
818
  const selected = purpose === entry.id;
397
- return /* @__PURE__ */ jsxs2(
819
+ return /* @__PURE__ */ jsxs3(
398
820
  "button",
399
821
  {
400
822
  type: "button",
@@ -403,7 +825,7 @@ function GenerateButton({ context, generate: generateEmail }) {
403
825
  onClick: () => setPurpose(selected ? null : entry.id),
404
826
  className: chipClassName(selected),
405
827
  children: [
406
- selected && /* @__PURE__ */ jsx2(Check, { className: "eb:size-3", "aria-hidden": "true" }),
828
+ selected && /* @__PURE__ */ jsx3(Check2, { className: "eb:size-3", "aria-hidden": "true" }),
407
829
  t(entry.label)
408
830
  ]
409
831
  },
@@ -412,8 +834,8 @@ function GenerateButton({ context, generate: generateEmail }) {
412
834
  }) })
413
835
  ] }, group.id)) })
414
836
  ] }) }),
415
- /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-3 eb:px-6 eb:pt-3 eb:pb-6", children: [
416
- /* @__PURE__ */ jsx2(
837
+ /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:flex-col eb:gap-3 eb:px-6 eb:pt-3 eb:pb-6", children: [
838
+ /* @__PURE__ */ jsx3(
417
839
  Textarea,
418
840
  {
419
841
  value: details,
@@ -428,15 +850,15 @@ function GenerateButton({ context, generate: generateEmail }) {
428
850
  }
429
851
  }
430
852
  ),
431
- error ? /* @__PURE__ */ jsxs2("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
432
- /* @__PURE__ */ jsx2(TriangleAlert2, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
853
+ error ? /* @__PURE__ */ jsxs3("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
854
+ /* @__PURE__ */ jsx3(TriangleAlert2, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
433
855
  error
434
- ] }) : dirty && !busy && /* @__PURE__ */ jsx2("p", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("The email being edited will be replaced, unsaved changes included.") }),
435
- /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:items-center eb:justify-end eb:gap-2", children: [
436
- busy && /* @__PURE__ */ jsx2("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 }) }),
437
- busy && /* @__PURE__ */ jsx2(Button, { variant: "outline", size: "sm", onClick: cancel, children: t("Cancel") }),
438
- /* @__PURE__ */ jsxs2(Button, { size: "sm", onClick: generate, disabled: !canGenerate || busy, "data-testid": "eb-generate-submit", children: [
439
- busy ? /* @__PURE__ */ jsx2(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx2(Sparkles, {}),
856
+ ] }) : 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.") }),
857
+ /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:items-center eb:justify-end eb:gap-2", children: [
858
+ 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 }) }),
859
+ busy && /* @__PURE__ */ jsx3(Button, { variant: "outline", size: "sm", onClick: cancel, children: t("Cancel") }),
860
+ /* @__PURE__ */ jsxs3(Button, { size: "sm", onClick: generate, disabled: !canGenerate || busy, "data-testid": "eb-generate-submit", children: [
861
+ busy ? /* @__PURE__ */ jsx3(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx3(Sparkles, {}),
440
862
  t("Generate")
441
863
  ] })
442
864
  ] })
@@ -448,9 +870,9 @@ function GenerateButton({ context, generate: generateEmail }) {
448
870
  }
449
871
 
450
872
  // src/ai/RewriteText.jsx
451
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
452
- import { ChevronDown, Languages, Loader2 as Loader22, Sparkles as Sparkles2, TriangleAlert as TriangleAlert3 } from "lucide-react";
453
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
873
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
874
+ import { ChevronDown as ChevronDown2, Languages, Loader2 as Loader22, Sparkles as Sparkles2, TriangleAlert as TriangleAlert3 } from "lucide-react";
875
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
454
876
  function nativeName(code) {
455
877
  try {
456
878
  const name = new Intl.DisplayNames([code], { type: "language" }).of(code) || code;
@@ -461,18 +883,18 @@ function nativeName(code) {
461
883
  }
462
884
  function RewriteText({ editor, selection, rewrite }) {
463
885
  const { t, notify } = useUi();
464
- const [running, setRunning] = useState3(
886
+ const [running, setRunning] = useState4(
465
887
  /** @type {string|null} */
466
888
  null
467
889
  );
468
- const [error, setError] = useState3("");
469
- const [instruction, setInstruction] = useState3("");
470
- const abortRef = useRef3(
890
+ const [error, setError] = useState4("");
891
+ const [instruction, setInstruction] = useState4("");
892
+ const abortRef = useRef4(
471
893
  /** @type {AbortController|null} */
472
894
  null
473
895
  );
474
896
  const label = selection.tagName === "mj-button";
475
- useEffect3(() => () => abortRef.current?.abort(), []);
897
+ useEffect4(() => () => abortRef.current?.abort(), []);
476
898
  const currentText = () => {
477
899
  const element = editor.getElement(selection.id);
478
900
  return (label ? element?.content : element?.html) ?? "";
@@ -506,27 +928,27 @@ function RewriteText({ editor, selection, rewrite }) {
506
928
  setRunning(null);
507
929
  };
508
930
  const busy = Boolean(running);
509
- const spinner = /* @__PURE__ */ jsx3(Loader22, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
510
- return /* @__PURE__ */ jsxs3("section", { className: "eb:space-y-2.5", "aria-label": t("Rewrite with AI"), "data-testid": "eb-rewrite", children: [
511
- /* @__PURE__ */ jsxs3(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
512
- /* @__PURE__ */ jsx3(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
931
+ const spinner = /* @__PURE__ */ jsx4(Loader22, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
932
+ return /* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2.5", "aria-label": t("Rewrite with AI"), "data-testid": "eb-rewrite", children: [
933
+ /* @__PURE__ */ jsxs4(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
934
+ /* @__PURE__ */ jsx4(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
513
935
  t("Rewrite with AI")
514
936
  ] }),
515
- /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: [
516
- REWRITE_ACTIONS.map((action) => /* @__PURE__ */ jsxs3("button", { type: "button", className: chipClassName(), disabled: busy, onClick: () => run(action.id), children: [
937
+ /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: [
938
+ REWRITE_ACTIONS.map((action) => /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: busy, onClick: () => run(action.id), children: [
517
939
  running === action.id && spinner,
518
940
  t(action.label)
519
941
  ] }, action.id)),
520
- /* @__PURE__ */ jsxs3(DropdownMenu, { children: [
521
- /* @__PURE__ */ jsx3(DropdownMenuTrigger, { asChild: true, disabled: busy, children: /* @__PURE__ */ jsxs3("button", { type: "button", className: chipClassName(), children: [
522
- running === "translate" ? spinner : /* @__PURE__ */ jsx3(Languages, { className: "eb:size-3", "aria-hidden": "true" }),
942
+ /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
943
+ /* @__PURE__ */ jsx4(DropdownMenuTrigger, { asChild: true, disabled: busy, children: /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), children: [
944
+ running === "translate" ? spinner : /* @__PURE__ */ jsx4(Languages, { className: "eb:size-3", "aria-hidden": "true" }),
523
945
  t("Translate"),
524
- /* @__PURE__ */ jsx3(ChevronDown, { className: "eb:size-3", "aria-hidden": "true" })
946
+ /* @__PURE__ */ jsx4(ChevronDown2, { className: "eb:size-3", "aria-hidden": "true" })
525
947
  ] }) }),
526
- /* @__PURE__ */ jsx3(DropdownMenuContent, { align: "start", "aria-label": t("Translate into"), children: REWRITE_LANGUAGES.map((code) => /* @__PURE__ */ jsx3(DropdownMenuItem, { lang: code, onSelect: () => run("translate", { targetLanguage: code }), children: nativeName(code) }, code)) })
948
+ /* @__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)) })
527
949
  ] })
528
950
  ] }),
529
- /* @__PURE__ */ jsxs3(
951
+ /* @__PURE__ */ jsxs4(
530
952
  "form",
531
953
  {
532
954
  className: "eb:flex eb:gap-1.5",
@@ -535,7 +957,7 @@ function RewriteText({ editor, selection, rewrite }) {
535
957
  if (instruction.trim()) run("custom", { instruction: instruction.trim() });
536
958
  },
537
959
  children: [
538
- /* @__PURE__ */ jsx3(
960
+ /* @__PURE__ */ jsx4(
539
961
  Input,
540
962
  {
541
963
  value: instruction,
@@ -546,55 +968,55 @@ function RewriteText({ editor, selection, rewrite }) {
546
968
  disabled: busy
547
969
  }
548
970
  ),
549
- /* @__PURE__ */ jsx3(Button, { type: "submit", variant: "outline", size: "icon-sm", "aria-label": t("Rewrite"), disabled: busy || !instruction.trim(), children: running === "custom" ? /* @__PURE__ */ jsx3(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx3(Sparkles2, {}) })
971
+ /* @__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, {}) })
550
972
  ]
551
973
  }
552
974
  ),
553
- running && /* @__PURE__ */ jsxs3("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
975
+ 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: [
554
976
  t("Rewriting\u2026"),
555
- /* @__PURE__ */ jsx3(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
977
+ /* @__PURE__ */ jsx4(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
556
978
  ] }),
557
- error && /* @__PURE__ */ jsxs3("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
558
- /* @__PURE__ */ jsx3(TriangleAlert3, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
979
+ error && /* @__PURE__ */ jsxs4("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
980
+ /* @__PURE__ */ jsx4(TriangleAlert3, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
559
981
  error
560
982
  ] })
561
983
  ] });
562
984
  }
563
985
 
564
986
  // src/ai/SubjectIdeas.jsx
565
- import { useEffect as useEffect4, useLayoutEffect, useRef as useRef4, useState as useState4 } from "react";
566
- import { Check as Check2, Loader2 as Loader23, Sparkles as Sparkles3, TriangleAlert as TriangleAlert4 } from "lucide-react";
567
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
987
+ import { useEffect as useEffect5, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
988
+ import { Check as Check3, Loader2 as Loader23, Sparkles as Sparkles3, TriangleAlert as TriangleAlert4 } from "lucide-react";
989
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
568
990
  var MAX_AVOID = 12;
569
991
  function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
570
992
  const { t, locale } = useUi();
571
- const [running, setRunning] = useState4(false);
572
- const [error, setError] = useState4("");
573
- const [ideas, setIdeas] = useState4(
993
+ const [running, setRunning] = useState5(false);
994
+ const [error, setError] = useState5("");
995
+ const [ideas, setIdeas] = useState5(
574
996
  /** @type {string[]|null} */
575
997
  null
576
998
  );
577
- const [original, setOriginal] = useState4("");
578
- const shownRef = useRef4(
999
+ const [original, setOriginal] = useState5("");
1000
+ const shownRef = useRef5(
579
1001
  /** @type {string[]} */
580
1002
  []
581
1003
  );
582
- const abortRef = useRef4(
1004
+ const abortRef = useRef5(
583
1005
  /** @type {AbortController|null} */
584
1006
  null
585
1007
  );
586
- const subjectRef = useRef4(meta.subject);
1008
+ const subjectRef = useRef5(meta.subject);
587
1009
  subjectRef.current = meta.subject;
588
- const rootRef = useRef4(
1010
+ const rootRef = useRef5(
589
1011
  /** @type {HTMLDivElement|null} */
590
1012
  null
591
1013
  );
592
- const focusRef = useRef4(
1014
+ const focusRef = useRef5(
593
1015
  /** @type {'list'|'button'|null} */
594
1016
  null
595
1017
  );
596
- useEffect4(() => () => abortRef.current?.abort(), []);
597
- useEffect4(
1018
+ useEffect5(() => () => abortRef.current?.abort(), []);
1019
+ useEffect5(
598
1020
  () => editor.on("load", () => {
599
1021
  abortRef.current?.abort();
600
1022
  abortRef.current = null;
@@ -607,7 +1029,7 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
607
1029
  [editor]
608
1030
  );
609
1031
  const typed = meta.subject.trim();
610
- useEffect4(() => {
1032
+ useEffect5(() => {
611
1033
  if (ideas && typed && !ideas.includes(meta.subject) && typed !== original) setOriginal(typed);
612
1034
  }, [ideas, meta.subject, typed, original]);
613
1035
  useLayoutEffect(() => {
@@ -679,16 +1101,16 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
679
1101
  onSubject(options[to].text);
680
1102
  rootRef.current?.querySelectorAll('[role="radio"]')[to]?.focus();
681
1103
  };
682
- const spinner = /* @__PURE__ */ jsx4(Loader23, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
683
- return /* @__PURE__ */ jsxs4("div", { className: "eb:space-y-2", "data-testid": "eb-subject-ideas", ref: rootRef, children: [
684
- !ideas && /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
685
- running ? spinner : /* @__PURE__ */ jsx4(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
1104
+ const spinner = /* @__PURE__ */ jsx5(Loader23, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
1105
+ return /* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2", "data-testid": "eb-subject-ideas", ref: rootRef, children: [
1106
+ !ideas && /* @__PURE__ */ jsxs5("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
1107
+ running ? spinner : /* @__PURE__ */ jsx5(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
686
1108
  t("Suggest subjects")
687
1109
  ] }),
688
- ideas && /* @__PURE__ */ jsxs4("div", { className: "eb:space-y-2 eb:rounded-md eb:border eb:border-border eb:p-1.5", children: [
689
- /* @__PURE__ */ jsx4("div", { role: "radiogroup", "aria-label": t("Subject ideas"), className: "eb:space-y-0.5", onKeyDown, children: options.map((option, index) => {
1110
+ ideas && /* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2 eb:rounded-md eb:border eb:border-border eb:p-1.5", children: [
1111
+ /* @__PURE__ */ jsx5("div", { role: "radiogroup", "aria-label": t("Subject ideas"), className: "eb:space-y-0.5", onKeyDown, children: options.map((option, index) => {
690
1112
  const chosen = index === checked;
691
- return /* @__PURE__ */ jsxs4(
1113
+ return /* @__PURE__ */ jsxs5(
692
1114
  "button",
693
1115
  {
694
1116
  type: "button",
@@ -701,28 +1123,28 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
701
1123
  ),
702
1124
  onClick: () => onSubject(option.text),
703
1125
  children: [
704
- /* @__PURE__ */ jsx4(Check2, { className: cn("eb:mt-0.5 eb:size-3.5 eb:shrink-0", chosen ? "eb:text-primary" : "eb:invisible"), "aria-hidden": "true" }),
705
- /* @__PURE__ */ jsx4("span", { className: "eb:min-w-0 eb:flex-1 eb:break-words", children: option.text }),
706
- option.yours && /* @__PURE__ */ jsx4("span", { className: "eb:shrink-0 eb:text-[11px] eb:text-foreground-muted", children: t("yours") })
1126
+ /* @__PURE__ */ jsx5(Check3, { className: cn("eb:mt-0.5 eb:size-3.5 eb:shrink-0", chosen ? "eb:text-primary" : "eb:invisible"), "aria-hidden": "true" }),
1127
+ /* @__PURE__ */ jsx5("span", { className: "eb:min-w-0 eb:flex-1 eb:break-words", children: option.text }),
1128
+ option.yours && /* @__PURE__ */ jsx5("span", { className: "eb:shrink-0 eb:text-[11px] eb:text-foreground-muted", children: t("yours") })
707
1129
  ]
708
1130
  },
709
1131
  `${option.yours ? "yours" : "idea"}:${option.text}`
710
1132
  );
711
1133
  }) }),
712
- /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:items-center eb:gap-1.5 eb:px-1 eb:pb-0.5", children: [
713
- /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
714
- running ? spinner : /* @__PURE__ */ jsx4(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
1134
+ /* @__PURE__ */ jsxs5("div", { className: "eb:flex eb:items-center eb:gap-1.5 eb:px-1 eb:pb-0.5", children: [
1135
+ /* @__PURE__ */ jsxs5("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
1136
+ running ? spinner : /* @__PURE__ */ jsx5(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
715
1137
  t("More ideas")
716
1138
  ] }),
717
- /* @__PURE__ */ jsx4(Button, { type: "button", variant: "ghost", size: "xs", className: "eb:ml-auto", disabled: running, onClick: done, children: t("Done") })
1139
+ /* @__PURE__ */ jsx5(Button, { type: "button", variant: "ghost", size: "xs", className: "eb:ml-auto", disabled: running, onClick: done, children: t("Done") })
718
1140
  ] })
719
1141
  ] }),
720
- 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: [
1142
+ running && /* @__PURE__ */ jsxs5("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
721
1143
  t("Reading the email\u2026"),
722
- /* @__PURE__ */ jsx4(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
1144
+ /* @__PURE__ */ jsx5(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
723
1145
  ] }),
724
- error && /* @__PURE__ */ jsxs4("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
725
- /* @__PURE__ */ jsx4(TriangleAlert4, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
1146
+ error && /* @__PURE__ */ jsxs5("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
1147
+ /* @__PURE__ */ jsx5(TriangleAlert4, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
726
1148
  error
727
1149
  ] })
728
1150
  ] });
@@ -753,7 +1175,13 @@ function createAi(options = {}) {
753
1175
  }
754
1176
  if (typeof askKey !== "boolean") throw new TypeError("createAi: `askKey` must be true or false");
755
1177
  if (apiKey && askKey) throw new TypeError("createAi: `apiKey` (one key for all) or `askKey` (each user's own), not both");
756
- const provider = usesProvider ? resolveProvider(providerOption) : null;
1178
+ const choice = Array.isArray(providerOption);
1179
+ if (choice && (!providerOption.length || !askKey)) {
1180
+ throw new TypeError("createAi: a list of providers is for `askKey: true` \u2014 each user chooses one and connects their own key");
1181
+ }
1182
+ const providers = usesProvider ? (choice ? providerOption : [providerOption]).map(providerOf) : [];
1183
+ if (new Set(providers.map((provider2) => provider2.id)).size !== providers.length) throw new TypeError("createAi: each provider once");
1184
+ const [provider = null] = providers;
757
1185
  if (provider && !apiKey && !askKey && !provider.keyless) {
758
1186
  throw new TypeError(`createAi: ${provider.name} needs a key \u2014 \`apiKey\`, or \`askKey: true\` to let each user connect their own`);
759
1187
  }
@@ -765,18 +1193,34 @@ function createAi(options = {}) {
765
1193
  }
766
1194
  const ownOnly = !model && !http && PARTS.some((part) => typeof options[part] === "function");
767
1195
  let transport = {};
768
- const key = provider && askKey ? createKeyStore(provider) : null;
1196
+ const key = provider && askKey ? createKeyStore(providers, { fetch: fetchImpl }) : null;
769
1197
  if (provider) {
770
- const generator = createEmailGenerator({
771
- model: providerEmailModel({
772
- provider,
773
- fetch: fetchImpl,
774
- // The user's key, asked when there is none ('' when they cancel: KEY_MISSING).
775
- apiKey: key ? async () => key.get() || await key.request() : apiKey
776
- }),
777
- maxAttempts,
778
- compile
779
- });
1198
+ const models = /* @__PURE__ */ new Map();
1199
+ const keyFor = () => key ? key.get() : apiKey || "";
1200
+ const modelOf = (id) => {
1201
+ const chosen = (
1202
+ /** @type {import('./providers.js').AiProvider} */
1203
+ providers.find((item) => item.id === id)
1204
+ );
1205
+ const name = key ? key.model(id) : chosen.model;
1206
+ const slot = `${id}
1207
+ ${name}`;
1208
+ if (!models.has(slot)) {
1209
+ models.set(
1210
+ slot,
1211
+ chosen.createModel ? chosen.createModel({ apiKey: keyFor, fetch: fetchImpl, ...name ? { model: name } : {} }) : providerEmailModel({ provider: chosen, fetch: fetchImpl, apiKey: keyFor, ...name ? { model: name } : {} })
1212
+ );
1213
+ }
1214
+ return (
1215
+ /** @type {import('./generator.js').EmailModel} */
1216
+ models.get(slot)
1217
+ );
1218
+ };
1219
+ const model2 = async (request) => {
1220
+ if (key && !key.get() && !providers.find((item) => item.id === key.provider.id)?.keyless) await key.request();
1221
+ return modelOf(key ? key.provider.id : provider.id)(request);
1222
+ };
1223
+ const generator = createEmailGenerator({ model: model2, maxAttempts, compile });
780
1224
  for (const part of PARTS) transport[part] = key ? askedAgain(generator[part], key) : generator[part];
781
1225
  } else if (model) {
782
1226
  transport = createEmailGenerator({ model, maxAttempts, compile });
@@ -810,6 +1254,19 @@ function createAi(options = {}) {
810
1254
  components: Object.freeze({ Generate: GenerateButton, Rewrite: RewriteText, Subjects: SubjectIdeas, Connect: key ? ConnectAi : null })
811
1255
  });
812
1256
  }
1257
+ function providerOf(option) {
1258
+ if (option && typeof option === "object" && typeof option.createModel === "function") {
1259
+ if (!option.id || !option.name) throw new TypeError("createAi: a provider with `createModel` needs an `id` and a `name`");
1260
+ return (
1261
+ /** @type {import('./providers.js').AiProvider} */
1262
+ option
1263
+ );
1264
+ }
1265
+ return resolveProvider(
1266
+ /** @type {any} */
1267
+ option
1268
+ );
1269
+ }
813
1270
  function askedAgain(run, key) {
814
1271
  return async (input, options) => {
815
1272
  try {
@@ -833,6 +1290,8 @@ function askedAgain(run, key) {
833
1290
  };
834
1291
  }
835
1292
  export {
1293
+ AGENT_ERROR_CODES,
1294
+ AGENT_PROTOCOL,
836
1295
  AI_MESSAGES,
837
1296
  AI_PROVIDERS,
838
1297
  DEFAULT_CLAUDE_MODEL,
@@ -842,10 +1301,14 @@ export {
842
1301
  PURPOSE_CATEGORIES,
843
1302
  REWRITE_ACTIONS,
844
1303
  REWRITE_LANGUAGES,
1304
+ agentEmailModel,
845
1305
  claudeEmailModel,
1306
+ claudeProvider,
846
1307
  createAi,
847
1308
  createEmailGenerator,
848
1309
  emailWords,
1310
+ failureCode,
1311
+ listProviderModels,
849
1312
  providerEmailModel,
850
1313
  resolveProvider
851
1314
  };