best-email-builder 0.5.1 → 0.6.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.
- package/CHANGELOG.md +26 -0
- package/README.md +36 -7
- package/dist/ai-server.js +3 -1
- package/dist/ai.js +163 -35
- package/dist/ai.js.map +1 -1
- package/dist/builder.js +4 -4
- package/dist/{chunk-2Y7CDXZM.js → chunk-5W4TUUPD.js} +3 -3
- package/dist/{chunk-N7DEVAIP.js → chunk-JU3OUO6X.js} +2 -2
- package/dist/{chunk-7QQH6MTV.js → chunk-NC2YDZSP.js} +4 -4
- package/dist/{chunk-BAA5XMWY.js → chunk-NZIQF7IW.js} +77 -22
- package/dist/{chunk-BAA5XMWY.js.map → chunk-NZIQF7IW.js.map} +1 -1
- package/dist/{chunk-H4W65GBX.js → chunk-QPAZLCPO.js} +2 -2
- package/dist/{chunk-H4W65GBX.js.map → chunk-QPAZLCPO.js.map} +1 -1
- package/dist/{chunk-2RKVJQSD.js → chunk-XEFBPP5Y.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/library.js +4 -4
- package/dist/server.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +4 -0
- package/dist/types/ai/claude.d.ts +30 -5
- package/dist/types/ai/createAi.d.ts +5 -3
- package/dist/types/ai/index.d.ts +2 -2
- package/dist/types/ai/keys.d.ts +15 -26
- package/dist/types/ai/providers.d.ts +15 -1
- package/dist/types/ai/server.d.ts +1 -1
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/engine/NativeAdapter.d.ts +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-2Y7CDXZM.js.map → chunk-5W4TUUPD.js.map} +0 -0
- /package/dist/{chunk-N7DEVAIP.js.map → chunk-JU3OUO6X.js.map} +0 -0
- /package/dist/{chunk-7QQH6MTV.js.map → chunk-NC2YDZSP.js.map} +0 -0
- /package/dist/{chunk-2RKVJQSD.js.map → chunk-XEFBPP5Y.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ All notable changes are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] — 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **The user chooses the provider.** `createAi({ provider: [...], askKey: true })`
|
|
13
|
+
takes a list: the Connect dialog shows the providers as cards (name, model,
|
|
14
|
+
free or not, a dot on those whose key this browser keeps; arrows move
|
|
15
|
+
through them). The choice and one key per provider are remembered, and a
|
|
16
|
+
provider whose key is kept needs nothing pasted. `ai.key` gains `provider`,
|
|
17
|
+
`providers`, `choose(id)` and `get(id)`; `ai.key.request()` opens the dialog
|
|
18
|
+
to change provider.
|
|
19
|
+
- **`claudeProvider({ client })`** — Claude as one of those providers, with
|
|
20
|
+
the user's own Anthropic key; the application's SDK is loaded when Claude
|
|
21
|
+
is chosen.
|
|
22
|
+
- **`failureCode(status, message)`** — the rule that turns a provider's
|
|
23
|
+
failure into the interface's error, exported for a model of your own.
|
|
24
|
+
- The live demo offers OpenAI, Claude, Mistral, Gemini and OpenRouter, says
|
|
25
|
+
which one is connected, and has a *Change provider* link.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **An exhausted quota said "busy, try again".** OpenAI answers a quota
|
|
29
|
+
spent with a 429 (`insufficient_quota`), Anthropic an empty credit balance
|
|
30
|
+
with a 400: both are now `MODEL_CREDITS`.
|
|
31
|
+
- **Claude's failures were all "could not answer".** The SDK's errors are now
|
|
32
|
+
read like every provider's: a refused key is `KEY_REFUSED` (asked again with
|
|
33
|
+
`askKey`), no credits `MODEL_CREDITS`, overloaded (529) `MODEL_BUSY`.
|
|
34
|
+
|
|
9
35
|
## [0.5.1] — 2026-10-15
|
|
10
36
|
|
|
11
37
|
### Fixed
|
package/README.md
CHANGED
|
@@ -61,7 +61,8 @@ Best Email Builder is the second option done once, properly, and given away:
|
|
|
61
61
|
model's MJML is cleaned and checked like any template before it gets there.
|
|
62
62
|
OpenRouter, Gemini, Groq, Mistral, OpenAI, DeepSeek, Cerebras, Ollama or any
|
|
63
63
|
OpenAI-compatible API by its key — free tiers included — and Claude through
|
|
64
|
-
its SDK; on your server, or in the page with each user's own key
|
|
64
|
+
its SDK; on your server, or in the page with each user's own key — and the
|
|
65
|
+
provider they choose.
|
|
65
66
|
- **Any text rewritten in one click** — select a text or a button: *Shorter*,
|
|
66
67
|
*Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate* into 13
|
|
67
68
|
languages, or say what to change in your own words. The text changes in
|
|
@@ -363,9 +364,19 @@ the provider refuses is forgotten and asked again. Off by default:
|
|
|
363
364
|
const ai = createAi({ provider: 'openrouter', askKey: true });
|
|
364
365
|
```
|
|
365
366
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
Give a list and each user chooses: the dialog shows the providers as cards
|
|
368
|
+
(name, model, free or not, a dot on those whose key this browser keeps). The
|
|
369
|
+
choice and one key per provider are remembered; going back to a provider whose
|
|
370
|
+
key is kept needs nothing pasted.
|
|
371
|
+
|
|
372
|
+
```jsx
|
|
373
|
+
const ai = createAi({ provider: ['openai', 'mistral', 'gemini', 'openrouter'], askKey: true });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`ai.key` reads and changes it: `provider` (the one in use), `providers`,
|
|
377
|
+
`choose(id)`, `get(id?)`, `set(key)`, `forget()`, `request()` (opens the
|
|
378
|
+
dialog — to change provider, for one), `subscribe(fn)`, `state()`
|
|
379
|
+
(`{ asking, refused, connected }`). This is what the live demo does.
|
|
369
380
|
|
|
370
381
|
**In the page, with one key for everyone** — prototypes and internal tools
|
|
371
382
|
only, the key is in the page's code: `createAi({ provider: 'groq', apiKey })`.
|
|
@@ -411,7 +422,10 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
|
|
|
411
422
|
quotes of HTML) can be told to skip it: `provider: { …, format: 'text' }`.
|
|
412
423
|
- The provider's failures become errors the interface explains, in its
|
|
413
424
|
language: `KEY_MISSING`, `KEY_REFUSED`, `MODEL_CREDITS`, `MODEL_BUSY`,
|
|
414
|
-
`MODEL_ERROR` (with the provider's own text in `detail`).
|
|
425
|
+
`MODEL_ERROR` (with the provider's own text in `detail`). An exhausted
|
|
426
|
+
quota is `MODEL_CREDITS`, not `MODEL_BUSY`, even when it comes as a 429
|
|
427
|
+
(OpenAI's `insufficient_quota`) or a 400 (Anthropic's credit balance);
|
|
428
|
+
`failureCode(status, message)` is that rule, for a model of your own. None is a 401:
|
|
415
429
|
the key is the AI's, not the user's session. A 403 about the model (not
|
|
416
430
|
available here, in this region) or the content is not `KEY_REFUSED`: with
|
|
417
431
|
`askKey`, the user's key is not thrown away for it.
|
|
@@ -419,7 +433,21 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
|
|
|
419
433
|
Anthropic SDK (`npm install @anthropic-ai/sdk`) — `claude-opus-5-5` by
|
|
420
434
|
default, `effort: 'medium'`, structured output, streamed so a long email
|
|
421
435
|
does not hit a request timeout — or through OpenRouter with an
|
|
422
|
-
`anthropic/…` model and one key for every model.
|
|
436
|
+
`anthropic/…` model and one key for every model. With `askKey`, Claude is
|
|
437
|
+
one more provider to choose, with the user's own Anthropic key and the SDK
|
|
438
|
+
loaded only when it is chosen:
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
import { claudeProvider, createAi } from 'best-email-builder/ai';
|
|
442
|
+
|
|
443
|
+
const claude = claudeProvider({
|
|
444
|
+
client: async (apiKey) => {
|
|
445
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
446
|
+
return new Anthropic({ apiKey, dangerouslyAllowBrowser: true });
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
createAi({ provider: ['openai', claude, 'gemini'], askKey: true });
|
|
450
|
+
```
|
|
423
451
|
- **Anything else** is a function `({ system, messages, schema, signal, task })
|
|
424
452
|
=> Promise<string>` returning the JSON text — `createEmailGenerator({ model })`
|
|
425
453
|
on the server, `createAi({ model })` in the page.
|
|
@@ -474,7 +502,8 @@ another click another one, the first row brings the user's subject back.
|
|
|
474
502
|
are exported.
|
|
475
503
|
|
|
476
504
|
The live demo has no server: there, the AI runs in the page with the
|
|
477
|
-
visitor's own
|
|
505
|
+
visitor's own key, for the provider they choose — OpenAI, Claude, Mistral,
|
|
506
|
+
Gemini or OpenRouter (`createAi({ provider: ['openai', claude, 'mistral', 'gemini', 'openrouter'], askKey: true })`).
|
|
478
507
|
|
|
479
508
|
### 5. Your brand (optional)
|
|
480
509
|
|
package/dist/ai-server.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createRewriter,
|
|
21
21
|
createSubjectSuggester,
|
|
22
22
|
emailWords,
|
|
23
|
+
failureCode,
|
|
23
24
|
normalizeGeneratedMjml,
|
|
24
25
|
parseDraft,
|
|
25
26
|
providerEmailModel,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
reviewDraft,
|
|
30
31
|
reviewRewrite,
|
|
31
32
|
reviewSubjects
|
|
32
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-NZIQF7IW.js";
|
|
33
34
|
import "./chunk-OWVXKMLI.js";
|
|
34
35
|
import "./chunk-W424KPCR.js";
|
|
35
36
|
import "./chunk-JI36FNWE.js";
|
|
@@ -55,6 +56,7 @@ export {
|
|
|
55
56
|
createRewriter,
|
|
56
57
|
createSubjectSuggester,
|
|
57
58
|
emailWords,
|
|
59
|
+
failureCode,
|
|
58
60
|
normalizeGeneratedMjml,
|
|
59
61
|
parseDraft,
|
|
60
62
|
providerEmailModel,
|
package/dist/ai.js
CHANGED
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
createHttpCaller,
|
|
24
24
|
useSearchShortcut,
|
|
25
25
|
useUi
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
import "./chunk-
|
|
26
|
+
} from "./chunk-JU3OUO6X.js";
|
|
27
|
+
import "./chunk-QPAZLCPO.js";
|
|
28
28
|
import {
|
|
29
29
|
AI_MESSAGES,
|
|
30
30
|
AI_PROVIDERS,
|
|
@@ -38,12 +38,14 @@ import {
|
|
|
38
38
|
REWRITE_ACTIONS,
|
|
39
39
|
REWRITE_LANGUAGES,
|
|
40
40
|
claudeEmailModel,
|
|
41
|
+
claudeProvider,
|
|
41
42
|
createEmailGenerator,
|
|
42
43
|
emailWords,
|
|
44
|
+
failureCode,
|
|
43
45
|
providerEmailModel,
|
|
44
46
|
purposesByCategory,
|
|
45
47
|
resolveProvider
|
|
46
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-NZIQF7IW.js";
|
|
47
49
|
import "./chunk-OWVXKMLI.js";
|
|
48
50
|
import "./chunk-W424KPCR.js";
|
|
49
51
|
import {
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
|
|
54
56
|
// src/ai/keys.js
|
|
55
57
|
var STORAGE_PREFIX = "best-email-builder:ai-key:";
|
|
58
|
+
var CHOICE_SLOT = "best-email-builder:ai-provider";
|
|
56
59
|
function browserStorage() {
|
|
57
60
|
try {
|
|
58
61
|
const storage = globalThis.localStorage;
|
|
@@ -61,21 +64,31 @@ function browserStorage() {
|
|
|
61
64
|
return null;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
var choiceOf = (provider) => Object.freeze({
|
|
68
|
+
id: provider.id,
|
|
69
|
+
name: provider.name,
|
|
70
|
+
model: provider.model ?? null,
|
|
71
|
+
keyUrl: provider.keyUrl,
|
|
72
|
+
keyHint: provider.keyHint,
|
|
73
|
+
free: provider.free
|
|
74
|
+
});
|
|
75
|
+
function createKeyStore(providerOrList) {
|
|
76
|
+
const providers = Object.freeze((Array.isArray(providerOrList) ? providerOrList : [providerOrList]).map(choiceOf));
|
|
77
|
+
if (!providers.length) throw new TypeError("createKeyStore: at least one provider");
|
|
78
|
+
const known = (id) => providers.some((provider) => provider.id === id);
|
|
79
|
+
const memory = /* @__PURE__ */ new Map();
|
|
67
80
|
let refused = false;
|
|
68
81
|
let pending = null;
|
|
69
82
|
const listeners = /* @__PURE__ */ new Set();
|
|
70
83
|
let snapshot;
|
|
71
|
-
const read = () => {
|
|
84
|
+
const read = (slot) => {
|
|
72
85
|
try {
|
|
73
86
|
return browserStorage()?.getItem(slot) || "";
|
|
74
87
|
} catch {
|
|
75
88
|
return "";
|
|
76
89
|
}
|
|
77
90
|
};
|
|
78
|
-
const write = (value) => {
|
|
91
|
+
const write = (slot, value) => {
|
|
79
92
|
try {
|
|
80
93
|
const storage = browserStorage();
|
|
81
94
|
if (value) storage?.setItem(slot, value);
|
|
@@ -83,7 +96,14 @@ function createKeyStore(provider) {
|
|
|
83
96
|
} catch {
|
|
84
97
|
}
|
|
85
98
|
};
|
|
86
|
-
const
|
|
99
|
+
const keyOf = (id) => memory.has(id) ? memory.get(id) : read(STORAGE_PREFIX + id);
|
|
100
|
+
const keep = (id, value) => {
|
|
101
|
+
memory.set(id, value);
|
|
102
|
+
write(STORAGE_PREFIX + id, value);
|
|
103
|
+
};
|
|
104
|
+
const kept = read(CHOICE_SLOT);
|
|
105
|
+
let current = known(kept) ? kept : (providers.find((provider) => keyOf(provider.id)) || providers[0]).id;
|
|
106
|
+
const get = (id) => keyOf(id === void 0 ? current : String(id));
|
|
87
107
|
const changed = () => {
|
|
88
108
|
snapshot = Object.freeze({ asking: Boolean(pending), refused, connected: Boolean(get()) });
|
|
89
109
|
for (const listener of [...listeners]) listener();
|
|
@@ -92,9 +112,8 @@ function createKeyStore(provider) {
|
|
|
92
112
|
const answer = (key) => {
|
|
93
113
|
const value = String(key || "").trim();
|
|
94
114
|
if (value) {
|
|
95
|
-
|
|
115
|
+
keep(current, value);
|
|
96
116
|
refused = false;
|
|
97
|
-
write(value);
|
|
98
117
|
}
|
|
99
118
|
const waiting = pending;
|
|
100
119
|
pending = null;
|
|
@@ -102,15 +121,28 @@ function createKeyStore(provider) {
|
|
|
102
121
|
waiting?.resolve(value);
|
|
103
122
|
};
|
|
104
123
|
return Object.freeze({
|
|
105
|
-
provider
|
|
124
|
+
get provider() {
|
|
125
|
+
return (
|
|
126
|
+
/** @type {ProviderChoice} */
|
|
127
|
+
providers.find((provider) => provider.id === current)
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
providers,
|
|
106
131
|
get,
|
|
107
132
|
set: (key) => {
|
|
108
133
|
if (typeof key !== "string" || !key.trim()) throw new TypeError("ai.key.set: a non-empty key");
|
|
109
134
|
answer(key);
|
|
110
135
|
},
|
|
111
136
|
forget: () => {
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
keep(current, "");
|
|
138
|
+
changed();
|
|
139
|
+
},
|
|
140
|
+
choose: (id) => {
|
|
141
|
+
if (!known(id)) throw new TypeError(`ai.key.choose: one of ${providers.map((provider) => provider.id).join(", ")}`);
|
|
142
|
+
if (id === current) return;
|
|
143
|
+
current = id;
|
|
144
|
+
refused = false;
|
|
145
|
+
write(CHOICE_SLOT, id);
|
|
114
146
|
changed();
|
|
115
147
|
},
|
|
116
148
|
request: () => {
|
|
@@ -130,8 +162,7 @@ function createKeyStore(provider) {
|
|
|
130
162
|
state: () => snapshot,
|
|
131
163
|
answer: (key) => answer(key || ""),
|
|
132
164
|
refuse: () => {
|
|
133
|
-
|
|
134
|
-
write("");
|
|
165
|
+
keep(current, "");
|
|
135
166
|
refused = true;
|
|
136
167
|
changed();
|
|
137
168
|
}
|
|
@@ -147,28 +178,54 @@ function ConnectAi({ aiKey }) {
|
|
|
147
178
|
const state = useSyncExternalStore(aiKey.subscribe, aiKey.state, aiKey.state);
|
|
148
179
|
const [value, setValue] = useState("");
|
|
149
180
|
const [shown, setShown] = useState(false);
|
|
181
|
+
const [chosenId, setChosenId] = useState(aiKey.provider.id);
|
|
150
182
|
const inputRef = useRef(
|
|
151
183
|
/** @type {HTMLInputElement|null} */
|
|
152
184
|
null
|
|
153
185
|
);
|
|
186
|
+
const choicesRef = useRef(
|
|
187
|
+
/** @type {HTMLDivElement|null} */
|
|
188
|
+
null
|
|
189
|
+
);
|
|
154
190
|
const inputId = useId();
|
|
155
191
|
const hintId = useId();
|
|
156
|
-
const
|
|
192
|
+
const choices = aiKey.providers;
|
|
193
|
+
const chosen = choices.find((provider) => provider.id === chosenId) || aiKey.provider;
|
|
194
|
+
const { name, keyUrl, keyHint, free } = chosen;
|
|
157
195
|
useEffect(() => {
|
|
158
196
|
if (!state.asking) return;
|
|
197
|
+
setChosenId(aiKey.provider.id);
|
|
159
198
|
setValue("");
|
|
160
199
|
setShown(false);
|
|
161
|
-
}, [state.asking]);
|
|
200
|
+
}, [state.asking, aiKey]);
|
|
162
201
|
const key = value.trim();
|
|
202
|
+
const saved = aiKey.get(chosen.id);
|
|
163
203
|
const unusual = Boolean(key && keyHint && !key.startsWith(keyHint));
|
|
204
|
+
const refused = state.refused && chosen.id === aiKey.provider.id;
|
|
164
205
|
const submit = (event) => {
|
|
165
206
|
event.preventDefault();
|
|
166
|
-
|
|
207
|
+
const answer = key || saved;
|
|
208
|
+
if (!answer) return;
|
|
209
|
+
if (chosen.id !== aiKey.provider.id) aiKey.choose(chosen.id);
|
|
210
|
+
aiKey.answer(answer);
|
|
211
|
+
};
|
|
212
|
+
const choose = (id) => {
|
|
213
|
+
setChosenId(id);
|
|
214
|
+
setValue("");
|
|
215
|
+
};
|
|
216
|
+
const onChoicesKeyDown = (event) => {
|
|
217
|
+
const at = choices.findIndex((provider) => provider.id === chosen.id);
|
|
218
|
+
const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
219
|
+
const to = step ? (at + step + choices.length) % choices.length : event.key === "Home" ? 0 : event.key === "End" ? choices.length - 1 : -1;
|
|
220
|
+
if (to === -1) return;
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
choose(choices[to].id);
|
|
223
|
+
choicesRef.current?.querySelectorAll('[role="radio"]')[to]?.focus();
|
|
167
224
|
};
|
|
168
225
|
return /* @__PURE__ */ jsx(Dialog, { open: state.asking, onOpenChange: (open) => !open && aiKey.answer(null), children: /* @__PURE__ */ jsx(
|
|
169
226
|
DialogContent,
|
|
170
227
|
{
|
|
171
|
-
className: "eb:gap-0 eb:overflow-hidden eb:p-0 eb:sm:max-w-md",
|
|
228
|
+
className: cn("eb:gap-0 eb:overflow-hidden eb:p-0", choices.length > 1 ? "eb:sm:max-w-xl" : "eb:sm:max-w-md"),
|
|
172
229
|
"data-testid": "eb-ai-connect",
|
|
173
230
|
onOpenAutoFocus: (event) => {
|
|
174
231
|
event.preventDefault();
|
|
@@ -183,7 +240,44 @@ function ConnectAi({ aiKey }) {
|
|
|
183
240
|
/* @__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 }) })
|
|
184
241
|
] })
|
|
185
242
|
] }),
|
|
186
|
-
|
|
243
|
+
choices.length > 1 && /* @__PURE__ */ jsx(
|
|
244
|
+
"div",
|
|
245
|
+
{
|
|
246
|
+
ref: choicesRef,
|
|
247
|
+
role: "radiogroup",
|
|
248
|
+
"aria-label": t("AI provider"),
|
|
249
|
+
className: "eb:grid eb:grid-cols-2 eb:gap-1.5 eb:sm:grid-cols-3",
|
|
250
|
+
onKeyDown: onChoicesKeyDown,
|
|
251
|
+
"data-testid": "eb-ai-connect-providers",
|
|
252
|
+
children: choices.map((provider) => {
|
|
253
|
+
const checked = provider.id === chosen.id;
|
|
254
|
+
return /* @__PURE__ */ jsxs(
|
|
255
|
+
"button",
|
|
256
|
+
{
|
|
257
|
+
type: "button",
|
|
258
|
+
role: "radio",
|
|
259
|
+
"aria-checked": checked,
|
|
260
|
+
tabIndex: checked ? 0 : -1,
|
|
261
|
+
onClick: () => choose(provider.id),
|
|
262
|
+
className: cn(
|
|
263
|
+
"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",
|
|
264
|
+
checked ? "eb:border-primary eb:bg-primary/10" : "eb:border-border eb:hover:bg-muted"
|
|
265
|
+
),
|
|
266
|
+
children: [
|
|
267
|
+
/* @__PURE__ */ jsxs("span", { className: "eb:flex eb:items-center eb:gap-1.5 eb:text-[13px] eb:leading-tight eb:font-medium eb:text-foreground", children: [
|
|
268
|
+
/* @__PURE__ */ jsx("span", { className: "eb:truncate", children: provider.name }),
|
|
269
|
+
provider.free && /* @__PURE__ */ jsx(Gift, { className: "eb:size-3 eb:shrink-0 eb:text-success", "aria-label": t(provider.free) }),
|
|
270
|
+
aiKey.get(provider.id) && /* @__PURE__ */ jsx("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") })
|
|
271
|
+
] }),
|
|
272
|
+
provider.model && /* @__PURE__ */ jsx("span", { className: "eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", title: provider.model, children: provider.model })
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
provider.id
|
|
276
|
+
);
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
),
|
|
280
|
+
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
281
|
/* @__PURE__ */ jsx(TriangleAlert, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
|
|
188
282
|
t("{provider} refused the last key. Paste another one.", { provider: name })
|
|
189
283
|
] }),
|
|
@@ -230,7 +324,7 @@ function ConnectAi({ aiKey }) {
|
|
|
230
324
|
}
|
|
231
325
|
)
|
|
232
326
|
] }),
|
|
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 })
|
|
327
|
+
/* @__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 }) }) : 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 })
|
|
234
328
|
] })
|
|
235
329
|
] }),
|
|
236
330
|
/* @__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: [
|
|
@@ -239,7 +333,7 @@ function ConnectAi({ aiKey }) {
|
|
|
239
333
|
] }),
|
|
240
334
|
/* @__PURE__ */ jsxs(DialogFooter, { className: "eb:border-t eb:border-border eb:px-6 eb:py-4", children: [
|
|
241
335
|
/* @__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") })
|
|
336
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: !key && !saved, "data-testid": "eb-ai-connect-submit", children: t("Connect") })
|
|
243
337
|
] })
|
|
244
338
|
] })
|
|
245
339
|
}
|
|
@@ -753,7 +847,13 @@ function createAi(options = {}) {
|
|
|
753
847
|
}
|
|
754
848
|
if (typeof askKey !== "boolean") throw new TypeError("createAi: `askKey` must be true or false");
|
|
755
849
|
if (apiKey && askKey) throw new TypeError("createAi: `apiKey` (one key for all) or `askKey` (each user's own), not both");
|
|
756
|
-
const
|
|
850
|
+
const choice = Array.isArray(providerOption);
|
|
851
|
+
if (choice && (!providerOption.length || !askKey)) {
|
|
852
|
+
throw new TypeError("createAi: a list of providers is for `askKey: true` \u2014 each user chooses one and connects their own key");
|
|
853
|
+
}
|
|
854
|
+
const providers = usesProvider ? (choice ? providerOption : [providerOption]).map(providerOf) : [];
|
|
855
|
+
if (new Set(providers.map((provider2) => provider2.id)).size !== providers.length) throw new TypeError("createAi: each provider once");
|
|
856
|
+
const [provider = null] = providers;
|
|
757
857
|
if (provider && !apiKey && !askKey && !provider.keyless) {
|
|
758
858
|
throw new TypeError(`createAi: ${provider.name} needs a key \u2014 \`apiKey\`, or \`askKey: true\` to let each user connect their own`);
|
|
759
859
|
}
|
|
@@ -765,18 +865,31 @@ function createAi(options = {}) {
|
|
|
765
865
|
}
|
|
766
866
|
const ownOnly = !model && !http && PARTS.some((part) => typeof options[part] === "function");
|
|
767
867
|
let transport = {};
|
|
768
|
-
const key = provider && askKey ? createKeyStore(
|
|
868
|
+
const key = provider && askKey ? createKeyStore(providers) : null;
|
|
769
869
|
if (provider) {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
870
|
+
const models = /* @__PURE__ */ new Map();
|
|
871
|
+
const keyFor = () => key ? key.get() : apiKey || "";
|
|
872
|
+
const modelOf = (id) => {
|
|
873
|
+
if (!models.has(id)) {
|
|
874
|
+
const chosen = (
|
|
875
|
+
/** @type {import('./providers.js').AiProvider} */
|
|
876
|
+
providers.find((item) => item.id === id)
|
|
877
|
+
);
|
|
878
|
+
models.set(
|
|
879
|
+
id,
|
|
880
|
+
chosen.createModel ? chosen.createModel({ apiKey: keyFor, fetch: fetchImpl }) : providerEmailModel({ provider: chosen, fetch: fetchImpl, apiKey: keyFor })
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
return (
|
|
884
|
+
/** @type {import('./generator.js').EmailModel} */
|
|
885
|
+
models.get(id)
|
|
886
|
+
);
|
|
887
|
+
};
|
|
888
|
+
const model2 = async (request) => {
|
|
889
|
+
if (key && !key.get() && !providers.find((item) => item.id === key.provider.id)?.keyless) await key.request();
|
|
890
|
+
return modelOf(key ? key.provider.id : provider.id)(request);
|
|
891
|
+
};
|
|
892
|
+
const generator = createEmailGenerator({ model: model2, maxAttempts, compile });
|
|
780
893
|
for (const part of PARTS) transport[part] = key ? askedAgain(generator[part], key) : generator[part];
|
|
781
894
|
} else if (model) {
|
|
782
895
|
transport = createEmailGenerator({ model, maxAttempts, compile });
|
|
@@ -810,6 +923,19 @@ function createAi(options = {}) {
|
|
|
810
923
|
components: Object.freeze({ Generate: GenerateButton, Rewrite: RewriteText, Subjects: SubjectIdeas, Connect: key ? ConnectAi : null })
|
|
811
924
|
});
|
|
812
925
|
}
|
|
926
|
+
function providerOf(option) {
|
|
927
|
+
if (option && typeof option === "object" && typeof option.createModel === "function") {
|
|
928
|
+
if (!option.id || !option.name) throw new TypeError("createAi: a provider with `createModel` needs an `id` and a `name`");
|
|
929
|
+
return (
|
|
930
|
+
/** @type {import('./providers.js').AiProvider} */
|
|
931
|
+
option
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
return resolveProvider(
|
|
935
|
+
/** @type {any} */
|
|
936
|
+
option
|
|
937
|
+
);
|
|
938
|
+
}
|
|
813
939
|
function askedAgain(run, key) {
|
|
814
940
|
return async (input, options) => {
|
|
815
941
|
try {
|
|
@@ -843,9 +969,11 @@ export {
|
|
|
843
969
|
REWRITE_ACTIONS,
|
|
844
970
|
REWRITE_LANGUAGES,
|
|
845
971
|
claudeEmailModel,
|
|
972
|
+
claudeProvider,
|
|
846
973
|
createAi,
|
|
847
974
|
createEmailGenerator,
|
|
848
975
|
emailWords,
|
|
976
|
+
failureCode,
|
|
849
977
|
providerEmailModel,
|
|
850
978
|
resolveProvider
|
|
851
979
|
};
|