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.
- package/CHANGELOG.md +63 -0
- package/README.md +74 -8
- package/dist/ai-server.js +116 -1
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.js +639 -176
- package/dist/ai.js.map +1 -1
- package/dist/builder.js +4 -4
- package/dist/{chunk-2Y7CDXZM.js → chunk-3RPWH5PC.js} +3 -3
- package/dist/{chunk-H4W65GBX.js → chunk-C6WHIBGH.js} +2 -2
- package/dist/{chunk-H4W65GBX.js.map → chunk-C6WHIBGH.js.map} +1 -1
- package/dist/{chunk-BAA5XMWY.js → chunk-CTZ56YJG.js} +252 -28
- package/dist/chunk-CTZ56YJG.js.map +1 -0
- package/dist/{chunk-7QQH6MTV.js → chunk-QH5OI43L.js} +4 -4
- package/dist/{chunk-N7DEVAIP.js → chunk-QRUMXCGW.js} +2 -2
- package/dist/{chunk-2RKVJQSD.js → chunk-UMVBT7NW.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/library.js +4 -4
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +5 -0
- package/dist/types/ai/ModelPicker.d.ts +34 -0
- package/dist/types/ai/agent.d.ts +31 -0
- package/dist/types/ai/agentEndpoint.d.ts +69 -0
- package/dist/types/ai/claude.d.ts +31 -5
- package/dist/types/ai/createAi.d.ts +5 -3
- package/dist/types/ai/draft.d.ts +10 -4
- package/dist/types/ai/generator.d.ts +10 -4
- package/dist/types/ai/index.d.ts +3 -2
- package/dist/types/ai/keys.d.ts +28 -26
- package/dist/types/ai/providers.d.ts +58 -1
- package/dist/types/ai/rewrite.d.ts +2 -1
- package/dist/types/ai/server.d.ts +3 -1
- package/dist/types/ai/subjects.d.ts +2 -1
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/engine/NativeAdapter.d.ts +1 -1
- package/dist/types/server/handler.d.ts +3 -0
- package/package.json +1 -1
- package/dist/chunk-BAA5XMWY.js.map +0 -1
- /package/dist/{chunk-2Y7CDXZM.js.map → chunk-3RPWH5PC.js.map} +0 -0
- /package/dist/{chunk-7QQH6MTV.js.map → chunk-QH5OI43L.js.map} +0 -0
- /package/dist/{chunk-N7DEVAIP.js.map → chunk-QRUMXCGW.js.map} +0 -0
- /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-
|
|
27
|
-
import "./chunk-
|
|
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-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
149
|
-
const [shown, setShown] =
|
|
150
|
-
const
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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__ */
|
|
191
|
-
keyUrl && /* @__PURE__ */
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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__ */
|
|
201
|
-
/* @__PURE__ */
|
|
202
|
-
/* @__PURE__ */
|
|
203
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
237
|
-
/* @__PURE__ */
|
|
238
|
-
/* @__PURE__ */
|
|
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__ */
|
|
241
|
-
/* @__PURE__ */
|
|
242
|
-
/* @__PURE__ */
|
|
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
|
|
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
|
|
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] =
|
|
266
|
-
const [purpose, setPurpose] =
|
|
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] =
|
|
271
|
-
const [query, setQuery] =
|
|
272
|
-
const [busy, setBusy] =
|
|
273
|
-
const [error, setError] =
|
|
274
|
-
const [seconds, setSeconds] =
|
|
275
|
-
const [dirty, setDirty] =
|
|
276
|
-
const abortRef =
|
|
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 =
|
|
702
|
+
const searchRef = useRef3(
|
|
281
703
|
/** @type {HTMLInputElement|null} */
|
|
282
704
|
null
|
|
283
705
|
);
|
|
284
706
|
useSearchShortcut(searchRef);
|
|
285
|
-
|
|
286
|
-
|
|
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 =
|
|
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__ */
|
|
354
|
-
/* @__PURE__ */
|
|
355
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
369
|
-
/* @__PURE__ */
|
|
370
|
-
/* @__PURE__ */
|
|
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__ */
|
|
373
|
-
/* @__PURE__ */
|
|
374
|
-
/* @__PURE__ */
|
|
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__ */
|
|
810
|
+
/* @__PURE__ */ jsx3(SearchKeyHint, { className: "eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2" })
|
|
389
811
|
] }) }),
|
|
390
|
-
/* @__PURE__ */
|
|
391
|
-
/* @__PURE__ */
|
|
392
|
-
groups.length === 0 && /* @__PURE__ */
|
|
393
|
-
/* @__PURE__ */
|
|
394
|
-
/* @__PURE__ */
|
|
395
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
416
|
-
/* @__PURE__ */
|
|
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__ */
|
|
432
|
-
/* @__PURE__ */
|
|
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__ */
|
|
435
|
-
/* @__PURE__ */
|
|
436
|
-
busy && /* @__PURE__ */
|
|
437
|
-
busy && /* @__PURE__ */
|
|
438
|
-
/* @__PURE__ */
|
|
439
|
-
busy ? /* @__PURE__ */
|
|
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
|
|
452
|
-
import { ChevronDown, Languages, Loader2 as Loader22, Sparkles as Sparkles2, TriangleAlert as TriangleAlert3 } from "lucide-react";
|
|
453
|
-
import { jsx as
|
|
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] =
|
|
886
|
+
const [running, setRunning] = useState4(
|
|
465
887
|
/** @type {string|null} */
|
|
466
888
|
null
|
|
467
889
|
);
|
|
468
|
-
const [error, setError] =
|
|
469
|
-
const [instruction, setInstruction] =
|
|
470
|
-
const abortRef =
|
|
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
|
-
|
|
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__ */
|
|
510
|
-
return /* @__PURE__ */
|
|
511
|
-
/* @__PURE__ */
|
|
512
|
-
/* @__PURE__ */
|
|
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__ */
|
|
516
|
-
REWRITE_ACTIONS.map((action) => /* @__PURE__ */
|
|
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__ */
|
|
521
|
-
/* @__PURE__ */
|
|
522
|
-
running === "translate" ? spinner : /* @__PURE__ */
|
|
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__ */
|
|
946
|
+
/* @__PURE__ */ jsx4(ChevronDown2, { className: "eb:size-3", "aria-hidden": "true" })
|
|
525
947
|
] }) }),
|
|
526
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
558
|
-
/* @__PURE__ */
|
|
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
|
|
566
|
-
import { Check as
|
|
567
|
-
import { jsx as
|
|
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] =
|
|
572
|
-
const [error, setError] =
|
|
573
|
-
const [ideas, setIdeas] =
|
|
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] =
|
|
578
|
-
const shownRef =
|
|
999
|
+
const [original, setOriginal] = useState5("");
|
|
1000
|
+
const shownRef = useRef5(
|
|
579
1001
|
/** @type {string[]} */
|
|
580
1002
|
[]
|
|
581
1003
|
);
|
|
582
|
-
const abortRef =
|
|
1004
|
+
const abortRef = useRef5(
|
|
583
1005
|
/** @type {AbortController|null} */
|
|
584
1006
|
null
|
|
585
1007
|
);
|
|
586
|
-
const subjectRef =
|
|
1008
|
+
const subjectRef = useRef5(meta.subject);
|
|
587
1009
|
subjectRef.current = meta.subject;
|
|
588
|
-
const rootRef =
|
|
1010
|
+
const rootRef = useRef5(
|
|
589
1011
|
/** @type {HTMLDivElement|null} */
|
|
590
1012
|
null
|
|
591
1013
|
);
|
|
592
|
-
const focusRef =
|
|
1014
|
+
const focusRef = useRef5(
|
|
593
1015
|
/** @type {'list'|'button'|null} */
|
|
594
1016
|
null
|
|
595
1017
|
);
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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__ */
|
|
683
|
-
return /* @__PURE__ */
|
|
684
|
-
!ideas && /* @__PURE__ */
|
|
685
|
-
running ? spinner : /* @__PURE__ */
|
|
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__ */
|
|
689
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
705
|
-
/* @__PURE__ */
|
|
706
|
-
option.yours && /* @__PURE__ */
|
|
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__ */
|
|
713
|
-
/* @__PURE__ */
|
|
714
|
-
running ? spinner : /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
725
|
-
/* @__PURE__ */
|
|
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
|
|
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(
|
|
1196
|
+
const key = provider && askKey ? createKeyStore(providers, { fetch: fetchImpl }) : null;
|
|
769
1197
|
if (provider) {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
};
|