best-email-builder 0.4.0 → 0.5.1

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 (83) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +130 -44
  3. package/dist/ai-server.js +68 -0
  4. package/dist/ai-server.js.map +1 -0
  5. package/dist/ai.js +852 -0
  6. package/dist/ai.js.map +1 -0
  7. package/dist/builder.js +16 -8
  8. package/dist/chunk-2RKVJQSD.js +595 -0
  9. package/dist/chunk-2RKVJQSD.js.map +1 -0
  10. package/dist/{chunk-NAEGU6PR.js → chunk-2Y7CDXZM.js} +18 -29
  11. package/dist/chunk-2Y7CDXZM.js.map +1 -0
  12. package/dist/chunk-4U7L2NYQ.js +356 -0
  13. package/dist/chunk-4U7L2NYQ.js.map +1 -0
  14. package/dist/{chunk-MY6WPDT4.js → chunk-7QQH6MTV.js} +473 -994
  15. package/dist/chunk-7QQH6MTV.js.map +1 -0
  16. package/dist/{chunk-JNHFZXDL.js → chunk-BAA5XMWY.js} +645 -646
  17. package/dist/chunk-BAA5XMWY.js.map +1 -0
  18. package/dist/{chunk-TGDZ4NFI.js → chunk-CNJZLZRH.js} +3 -44
  19. package/dist/chunk-CNJZLZRH.js.map +1 -0
  20. package/dist/chunk-H4W65GBX.js +119 -0
  21. package/dist/chunk-H4W65GBX.js.map +1 -0
  22. package/dist/{chunk-HHLOE6OZ.js → chunk-JI36FNWE.js} +12 -333
  23. package/dist/chunk-JI36FNWE.js.map +1 -0
  24. package/dist/{chunk-AT73Y7JA.js → chunk-N7DEVAIP.js} +49 -581
  25. package/dist/chunk-N7DEVAIP.js.map +1 -0
  26. package/dist/chunk-OWVXKMLI.js +121 -0
  27. package/dist/chunk-OWVXKMLI.js.map +1 -0
  28. package/dist/chunk-SAIW46IC.js +98 -0
  29. package/dist/chunk-SAIW46IC.js.map +1 -0
  30. package/dist/chunk-UFKCLU2D.js +255 -0
  31. package/dist/chunk-UFKCLU2D.js.map +1 -0
  32. package/dist/chunk-W424KPCR.js +251 -0
  33. package/dist/chunk-W424KPCR.js.map +1 -0
  34. package/dist/index.js +31 -37
  35. package/dist/index.js.map +1 -1
  36. package/dist/library.js +12 -7
  37. package/dist/server.js +40 -98
  38. package/dist/server.js.map +1 -1
  39. package/dist/styles.css +1 -1
  40. package/dist/types/ai/ConnectAi.d.ts +12 -0
  41. package/dist/types/ai/GenerateButton.d.ts +4 -0
  42. package/dist/types/ai/SubjectIdeas.d.ts +25 -0
  43. package/dist/types/ai/createAi.d.ts +66 -0
  44. package/dist/types/{features/generate → ai}/draft.d.ts +56 -3
  45. package/dist/types/{features/generate → ai}/generator.d.ts +7 -2
  46. package/dist/types/ai/i18n/fr.d.ts +2 -0
  47. package/dist/types/ai/i18n/index.d.ts +2 -0
  48. package/dist/types/ai/index.d.ts +9 -0
  49. package/dist/types/ai/keys.d.ts +46 -0
  50. package/dist/types/ai/providers.d.ts +75 -0
  51. package/dist/types/{features/generate → ai}/rewrite.d.ts +1 -1
  52. package/dist/types/ai/server.d.ts +9 -0
  53. package/dist/types/{features/generate → ai}/subjects.d.ts +18 -4
  54. package/dist/types/core/constants.d.ts +1 -1
  55. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  56. package/dist/types/features/preview/darkMode.d.ts +33 -16
  57. package/dist/types/features/variables/VariablePicker.d.ts +3 -2
  58. package/dist/types/i18n/index.d.ts +8 -0
  59. package/dist/types/index.d.ts +1 -7
  60. package/dist/types/renderer/prepareMjml.d.ts +8 -2
  61. package/dist/types/server/handler.d.ts +5 -4
  62. package/dist/types/server/index.d.ts +2 -9
  63. package/dist/types/server/render.d.ts +5 -2
  64. package/dist/types/storage/client.d.ts +22 -40
  65. package/dist/types/ui/EmailBuilder.d.ts +5 -1
  66. package/dist/types/ui/EmailStudio.d.ts +6 -1
  67. package/dist/types/ui/PropertiesPanel.d.ts +2 -1
  68. package/dist/types/ui/primitives/searchShortcut.d.ts +16 -0
  69. package/package.json +15 -1
  70. package/dist/chunk-AT73Y7JA.js.map +0 -1
  71. package/dist/chunk-HHLOE6OZ.js.map +0 -1
  72. package/dist/chunk-JNHFZXDL.js.map +0 -1
  73. package/dist/chunk-MY6WPDT4.js.map +0 -1
  74. package/dist/chunk-NAEGU6PR.js.map +0 -1
  75. package/dist/chunk-TGDZ4NFI.js.map +0 -1
  76. package/dist/types/features/generate/GenerateButton.d.ts +0 -3
  77. package/dist/types/features/generate/SubjectIdeas.d.ts +0 -16
  78. package/dist/types/features/generate/index.d.ts +0 -2
  79. /package/dist/types/{features/generate → ai}/RewriteText.d.ts +0 -0
  80. /package/dist/types/{features/generate → ai}/chip.d.ts +0 -0
  81. /package/dist/types/{features/generate → ai}/claude.d.ts +0 -0
  82. /package/dist/types/{features/generate → ai}/prompt.d.ts +0 -0
  83. /package/dist/types/{features/generate → ai}/purposes.d.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,94 @@ All notable changes are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] — 2026-10-15
10
+
11
+ ### Fixed
12
+ - **An image with no picture yet is left out of the email.** `<mj-image>`
13
+ without `src` (AI drafts leave them for the user, so do the gallery's
14
+ newsletter and promotion) compiled to `<img src="">` — a broken image in
15
+ every inbox, a red cross in Outlook. Saving, exporting and sending now leave
16
+ it out; the canvas and the preview (`/render`) still draw the frame to fill.
17
+ Saving says how many are left out. `prepareMjml(…, { emptyImages: 'keep' })`
18
+ keeps them; `countImagesWithoutPicture(mjml)` counts them.
19
+ - **Drafts through `openrouter/free` were cut short.** Most free models
20
+ reason first — 5 000 to 12 000 tokens before the email — and hit the limit
21
+ of 16 000 (`TRUNCATED`). OpenRouter now asks for less reasoning
22
+ (`reasoning: { effort: 'low' }`) and allows 32 000 tokens.
23
+ - **AI drafts, what free models get wrong, fixed without asking again:** an
24
+ `<mj-image>` put inside a text (dead HTML there) becomes an image block in
25
+ its place; attributes MJML reports as illegal (`align` on a column) are
26
+ removed after compiling; a button whose words do not read on its fill (a
27
+ text color given, the fill forgotten) gets white or ink words. The prompt
28
+ shows the greeting's default in three languages: a French email no longer
29
+ starts with "Hello … there".
30
+ - **A 403 was always the key.** One about the model (only for some apps, not
31
+ in this region), the content (moderation) or the money now says so
32
+ (`MODEL_ERROR`, `REFUSED`, `MODEL_CREDITS`), and `askKey` keeps the user's
33
+ key. Gemini's wrong key (a 400) is `KEY_REFUSED`.
34
+
35
+ ### Added
36
+ - `providerEmailModel({ body })` and `provider: { body, maxTokens }`: fields
37
+ of your own in each request (OpenRouter's `models` fallback list, `reasoning`)
38
+ and a provider's own output limit.
39
+
40
+ ## [0.5.0] — 2026-10-15
41
+
42
+ ### Changed — the AI is a module (breaking)
43
+ - Everything AI moved to its own entries: `best-email-builder/ai` (the
44
+ builder's side) and `best-email-builder/ai/server` (the generator, the
45
+ prompts, `claudeEmailModel`). An application that does not import them has
46
+ no AI button anywhere, and none of the AI's code or texts in its bundle.
47
+ - The builder takes the module as a prop: `<EmailBuilder ai={createAi()} />`
48
+ (`EmailStudio` passes it on). `createAi()` calls the handler's routes;
49
+ `createAi({ baseUrl, headers })` other ones; `generate`, `rewrite` and
50
+ `suggestSubjects` can each be `false` (no button) or a function of your own.
51
+ - Removed: `createHttpStorage({ generate: true })` and the storage's
52
+ `generate`, `rewrite` and `suggestSubjects`; `createEmailGenerator`,
53
+ `claudeEmailModel` and the prompts are no longer exported by
54
+ `best-email-builder/server` — import them from `best-email-builder/ai/server`.
55
+ The handler still takes the generator as `generator`.
56
+
57
+ ### Added
58
+ - **AI providers by their key**, nothing to install: `providerEmailModel({ provider, apiKey })`
59
+ and `createAi({ provider, … })` for OpenRouter (default `openrouter/free`,
60
+ free models), Google Gemini, Groq, Cerebras, Mistral, OpenAI, DeepSeek,
61
+ Ollama and any OpenAI-compatible API (`{ baseUrl, model }`). Structured
62
+ output where the model takes it, stepped down to a JSON object, then plain
63
+ JSON, once. `AI_PROVIDERS` lists them with their default model, where to get
64
+ a key and what is free. New errors, translated: `KEY_MISSING`,
65
+ `KEY_REFUSED`, `MODEL_CREDITS`, `MODEL_BUSY`, `MODEL_ERROR`.
66
+ - **Each user's own key**: `createAi({ provider, askKey: true })` (off by
67
+ default) — the first AI action opens a *Connect* dialog (the provider, a
68
+ link to get a key, what is free, the key hidden or shown), the key is kept
69
+ in that browser and sent only to the provider; a refused key is forgotten
70
+ and asked again in the same action. `ai.key` reads and changes it.
71
+ - **Ctrl K** (⌘K on Apple) puts the cursor in the search of *Generate* and of
72
+ the variables menu, as in the template library; each search shows the hint.
73
+ - The live demo runs the AI with the visitor's own OpenRouter key, asked in
74
+ the *Connect* dialog instead of the browser's prompt; its code samples have
75
+ a *With AI* tab.
76
+
77
+ ### Fixed
78
+ - Dark-mode preview: text and lines are now decided against the background
79
+ they really sit on (nested tables, `bgcolor`, CSS), by contrast; legacy
80
+ attributes (`<font color>`, `<body text link>`, `bgcolor` without `#`),
81
+ `hsl()` colors and per-rule CSS are handled; comments, Outlook-only blocks
82
+ and the doctype are left alone; the dark sheet goes in the real `<head>`.
83
+ - *Suggest subjects*: a subject quoting itself, a list introduced by a line of
84
+ prose, entities in the email's words, a merge tag of another tool
85
+ (`*|FNAME|*`, `%name%`), more than 90 characters, capitals the email does not
86
+ use, "!!" or the preheader repeated are no longer offered; the language
87
+ falls back to the server's. In the list, what the user types becomes
88
+ "yours", another email loaded closes it, "More ideas" avoids every idea
89
+ already shown and keeps the chosen one, the arrows move through it, and a
90
+ cancelled request can no longer end the one after it.
91
+ - MJML a model escaped twice (`\"`, `\n` left in the markup — some free
92
+ models do) is read as MJML instead of failing the draft or the rewrite.
93
+ - Types: `createEmailGenerator()`'s `rewrite` and `suggestSubjects` named
94
+ input types the declaration file did not import (a TypeScript error without
95
+ `skipLibCheck`).
96
+
9
97
  ## [0.4.0] — 2026-10-15
10
98
 
11
99
  ### Changed — the builder's own engine (breaking)
package/README.md CHANGED
@@ -59,7 +59,9 @@ Best Email Builder is the second option done once, properly, and given away:
59
59
  shipped…), adds a line of details if they want, and a complete email opens in
60
60
  the editor, subject and preview text included, ready to edit and save. The
61
61
  model's MJML is cleaned and checked like any template before it gets there.
62
- Claude out of the box; any other model in a few lines.
62
+ OpenRouter, Gemini, Groq, Mistral, OpenAI, DeepSeek, Cerebras, Ollama or any
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.
63
65
  - **Any text rewritten in one click** — select a text or a button: *Shorter*,
64
66
  *Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate* into 13
65
67
  languages, or say what to change in your own words. The text changes in
@@ -67,6 +69,8 @@ Best Email Builder is the second option done once, properly, and given away:
67
69
  - **Three subject lines on demand** — drawn from the words of the email, each
68
70
  from its own angle; a click puts one in the subject, your own stays in the
69
71
  list to come back to.
72
+ - **The AI is optional** — a module of its own (`best-email-builder/ai`):
73
+ without it, no AI button, and none of its code in your bundle.
70
74
  - **Your brand in every email** — give your website: the logo, colors and
71
75
  font are read from it (or set by hand), and new emails, templates and
72
76
  blocks start in them.
@@ -78,7 +82,8 @@ Best Email Builder is the second option done once, properly, and given away:
78
82
  uploads, server preview with quality checks (missing alt text, missing
79
83
  unsubscribe link, unknown variables, Gmail clipping…) and a dark-mode view
80
84
  (as the Gmail and Outlook apps repaint emails), test sends.
81
- - **Merge-tag variables your users create themselves** — searchable menu,
85
+ - **Merge-tag variables your users create themselves** — searchable menu
86
+ (Ctrl K, like every search of the builder),
82
87
  insertion at the caret, in the subject line too, sample values for the
83
88
  preview and fallbacks for recipients without a value.
84
89
  - **Framework-agnostic server**: one Fetch-API handler for Next.js, Hono, Bun,
@@ -298,29 +303,43 @@ const subject = applyVariablesToText(template.subject, values, variables);
298
303
  With an email provider that does its own merge, map the tags instead:
299
304
  `mapVariables(html, { 'contact.firstname': '{{ contact.FIRSTNAME }}' })`.
300
305
 
301
- ### 4. Generating emails with AI (optional)
306
+ ### 4. AI (an optional module)
302
307
 
303
- The *Generate* button asks one thing: what the email is for (a badge among
304
- 68, grouped: sales, holidays, events, news, account, orders, relationship,
305
- organization), with an optional line of details. The email is written in the
306
- interface's language, with the builder's sender and variables, and replaces
307
- the one being edited — the dialog says so before, not after.
308
+ The AI is a module of its own, `best-email-builder/ai`. An application that
309
+ does not import it has no AI button anywhere, and none of the AI's code or
310
+ texts in its bundle. Given to the builder, it brings three things:
308
311
 
309
- The model runs on the server, where its key is:
312
+ - **Generate** (toolbar) — asks one thing: what the email is for (a badge
313
+ among 68, grouped: sales, holidays, events, news, account, orders,
314
+ relationship, organization), with an optional line of details. The email is
315
+ written in the interface's language, with the builder's sender and
316
+ variables, and replaces the one being edited — the dialog says so before,
317
+ not after.
318
+ - **Rewrite with AI** (properties of a text or a button) and **Suggest
319
+ subjects** (under the subject), below.
320
+
321
+ ```jsx
322
+ import { createAi } from 'best-email-builder/ai';
323
+
324
+ <EmailBuilder storage={storage} ai={createAi(/* where the model runs, below */)} />
325
+ ```
326
+
327
+ Pass `generate: false`, `rewrite: false` or `suggestSubjects: false` to leave
328
+ a part out (its button with it), or functions of your own for any of them.
329
+
330
+ #### Where the model runs
331
+
332
+ **On your server** — the key stays there; the right choice for production:
310
333
 
311
334
  ```js
312
- import Anthropic from '@anthropic-ai/sdk'; // npm install @anthropic-ai/sdk
313
- import {
314
- createEmailBuilderHandler,
315
- createEmailGenerator,
316
- claudeEmailModel,
317
- compileMjml,
318
- } from 'best-email-builder/server';
335
+ // server
336
+ import { createEmailBuilderHandler, compileMjml } from 'best-email-builder/server';
337
+ import { createEmailGenerator, providerEmailModel } from 'best-email-builder/ai/server';
319
338
 
320
339
  const handler = createEmailBuilderHandler({
321
340
  /* … */
322
341
  generator: createEmailGenerator({
323
- model: claudeEmailModel({ client: new Anthropic() }), // ANTHROPIC_API_KEY
342
+ model: providerEmailModel({ provider: 'openrouter', apiKey: process.env.OPENROUTER_API_KEY }),
324
343
  compile: compileMjml, // MJML's own warnings go back to the model
325
344
  }),
326
345
  rateLimits: { generatePerHour: 30, rewritePerHour: 200, subjectsPerHour: 100 }, // per user (defaults)
@@ -328,24 +347,94 @@ const handler = createEmailBuilderHandler({
328
347
  ```
329
348
 
330
349
  ```jsx
331
- <EmailBuilder storage={createHttpStorage({ generate: true })} />
350
+ // browser: the handler's /generate, /rewrite and /subjects routes
351
+ <EmailBuilder storage={createHttpStorage()} ai={createAi()} />
352
+ ```
353
+
354
+ `createAi({ baseUrl, headers })` points at the same routes elsewhere, or with
355
+ the user's credentials. Without a `generator`, those routes answer 501.
356
+
357
+ **In the page, with each user's own key** — no server needed: the first AI
358
+ action opens a *Connect* dialog (the provider, a link to get a key, what is
359
+ free), the key is kept in that browser and sent only to the provider. A key
360
+ the provider refuses is forgotten and asked again. Off by default:
361
+
362
+ ```jsx
363
+ const ai = createAi({ provider: 'openrouter', askKey: true });
364
+ ```
365
+
366
+ `ai.key` reads and changes it: `get()`, `set(key)`, `forget()`,
367
+ `request()`, `subscribe(fn)`, `state()` (`{ asking, refused, connected }`).
368
+ This is what the live demo does.
369
+
370
+ **In the page, with one key for everyone** — prototypes and internal tools
371
+ only, the key is in the page's code: `createAi({ provider: 'groq', apiKey })`.
372
+
373
+ #### Providers
374
+
375
+ Every provider here speaks the OpenAI *chat completions* API; nothing to
376
+ install. `provider` is an id below, or `{ id, model, baseUrl, headers, maxTokens, body }`
377
+ to change the model or reach an endpoint of your own. `AI_PROVIDERS` lists them
378
+ (name, address, default model, where to get a key, what is free).
379
+
380
+ | `provider` | Default model | Free to start | Key |
381
+ |---|---|---|---|
382
+ | `'openrouter'` | `openrouter/free` (a router over the free models of the day) | free models, with a free account | [openrouter.ai/keys](https://openrouter.ai/keys) |
383
+ | `'gemini'` | `gemini-3.5-flash-lite` | free tier, rate-limited | [aistudio.google.com/apikey](https://aistudio.google.com/apikey) |
384
+ | `'groq'` | `openai/gpt-oss-120b` | free tier, rate-limited | [console.groq.com/keys](https://console.groq.com/keys) |
385
+ | `'cerebras'` | `gpt-oss-120b` | free tier, rate-limited | [cloud.cerebras.ai](https://cloud.cerebras.ai/) |
386
+ | `'mistral'` | `mistral-small-latest` | free *Experiment* plan | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) |
387
+ | `'openai'` | `gpt-5-mini` | — | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
388
+ | `'deepseek'` | `deepseek-chat` | — | [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) |
389
+ | `'ollama'` | — (give `model`) | runs on your machine, no key | — |
390
+ | `'openai-compatible'` | — (give `baseUrl` and `model`) | LM Studio, vLLM, Together, a proxy of your own… | as it needs |
391
+
392
+ ```js
393
+ providerEmailModel({ provider: { id: 'groq', model: 'llama-4-scout' }, apiKey });
394
+ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qwen3' } });
332
395
  ```
333
396
 
334
- - `claudeEmailModel({ client, model, effort, maxTokens })` — `claude-opus-5-5`
335
- by default, `effort: 'medium'`; the answer is structured output (a JSON
336
- schema: name, subject, preheader, MJML) and is streamed, so a long email does
337
- not hit a request timeout. A generation takes from 20 seconds to a minute or
338
- two: give the route that long (`maxDuration` on Vercel, the proxy's timeout).
339
- - **Another provider** is a function `({ system, messages, schema, signal, task }) =>
340
- Promise<string>` returning the JSON text — pass it as `model`.
341
- - What comes out is never trusted as is: shorthand padding is expanded,
342
- elements the builder does not edit (`mj-hero`, `mj-wrapper`, `mj-head`…)
343
- are turned into sections and columns, stock photos are removed (the user
344
- picks real images; the alt text stays), unknown variables become visible
345
- `[placeholders]`, and the result must pass the same validation as a saved
346
- template. When a problem remains, the model gets one more turn with the list.
347
- - The prompt (`SYSTEM_PROMPT`, `buildUserPrompt`) and the purposes
348
- (`EMAIL_PURPOSES`) are exported, to read or to build on.
397
+ - Model names change often: the defaults were current at this release —
398
+ pass `model` to be sure of yours.
399
+ - `openrouter/free` costs nothing and is slow: most free models reason before
400
+ they write, so an email takes two to six minutes, and some days a free
401
+ model is busy upstream. OpenRouter is asked for less reasoning
402
+ (`reasoning: { effort: 'low' }`) and given room for it (`maxTokens: 32000`;
403
+ 16 000 elsewhere). For drafts in seconds, use a free tier with a key of its
404
+ own (Gemini, Groq, Cerebras) or a paid model.
405
+ - `body` adds fields of your own to each request — OpenRouter's fallback list,
406
+ for one: `providerEmailModel({ provider: 'openrouter', apiKey, body: { models: [...] } })`.
407
+ It cannot change the model, the messages or the output limit.
408
+ - The answer is asked as structured output (a JSON schema); a model that
409
+ refuses it is asked for a JSON object, then for plain JSON — once, then
410
+ remembered. A model whose structured output breaks the JSON (some do on the
411
+ quotes of HTML) can be told to skip it: `provider: { …, format: 'text' }`.
412
+ - The provider's failures become errors the interface explains, in its
413
+ language: `KEY_MISSING`, `KEY_REFUSED`, `MODEL_CREDITS`, `MODEL_BUSY`,
414
+ `MODEL_ERROR` (with the provider's own text in `detail`). None is a 401:
415
+ the key is the AI's, not the user's session. A 403 about the model (not
416
+ available here, in this region) or the content is not `KEY_REFUSED`: with
417
+ `askKey`, the user's key is not thrown away for it.
418
+ - **Claude**: `claudeEmailModel({ client: new Anthropic() })` with the
419
+ Anthropic SDK (`npm install @anthropic-ai/sdk`) — `claude-opus-5-5` by
420
+ default, `effort: 'medium'`, structured output, streamed so a long email
421
+ does not hit a request timeout — or through OpenRouter with an
422
+ `anthropic/…` model and one key for every model.
423
+ - **Anything else** is a function `({ system, messages, schema, signal, task })
424
+ => Promise<string>` returning the JSON text — `createEmailGenerator({ model })`
425
+ on the server, `createAi({ model })` in the page.
426
+
427
+ A generation takes from 20 seconds to a minute or two: on a server, give the
428
+ route that long (`maxDuration` on Vercel, the proxy's timeout).
429
+
430
+ What comes out is never trusted as is: shorthand padding is expanded,
431
+ elements the builder does not edit (`mj-hero`, `mj-wrapper`, `mj-head`…) are
432
+ turned into sections and columns, stock photos are removed (the user picks
433
+ real images; the alt text stays), unknown variables become visible
434
+ `[placeholders]`, and the result must pass the same validation as a saved
435
+ template. When a problem remains, the model gets one more turn with the list.
436
+ The prompt (`SYSTEM_PROMPT`, `buildUserPrompt`) and the purposes
437
+ (`EMAIL_PURPOSES`) are exported from `best-email-builder/ai/server`.
349
438
 
350
439
  **Rewrite with AI.** The same generator rewrites one text: with it, selecting
351
440
  a text block or a button shows a *Rewrite with AI* section in its properties —
@@ -356,14 +445,14 @@ the previous text back, so there is nothing to accept or compare. Selecting
356
445
  another element cancels a rewrite in progress; if the text was edited
357
446
  meanwhile, the user's words win and the answer is dropped.
358
447
 
359
- - It is `POST {base}/rewrite`, enabled with `createHttpStorage({ generate: true })`
360
- like generation; a generator without `rewrite` answers 501.
448
+ - On a server it is `POST {base}/rewrite`; a generator without `rewrite`
449
+ answers 501.
361
450
  - Every link (the same `href`, none added) and every variable (the same keys,
362
451
  none added; a fallback after `|` may be translated) must survive, and the
363
452
  HTML must pass the validator — otherwise the model gets one more turn, then
364
453
  nothing is changed. A button label comes back as plain words.
365
454
  - `claudeEmailModel` asks a rewrite for `rewriteEffort: 'low'` (a few seconds);
366
- another provider gets `task: 'rewrite'` in its request to tell the two apart.
455
+ a model of your own gets `task: 'rewrite'` in its request to tell the two apart.
367
456
  - `REWRITE_ACTIONS`, `REWRITE_LANGUAGES` and `REWRITE_SYSTEM_PROMPT` are
368
457
  exported.
369
458
 
@@ -374,8 +463,8 @@ pick from, with the user's own subject first: a click puts one in the field,
374
463
  another click another one, the first row brings the user's subject back.
375
464
  *More ideas* asks for three new ones, *Done* closes the list.
376
465
 
377
- - It is `POST {base}/subjects` (`generator.suggestSubjects`), enabled with
378
- `createHttpStorage({ generate: true })` too; 100 per user per hour.
466
+ - On a server it is `POST {base}/subjects` (`generator.suggestSubjects`);
467
+ 100 per user per hour.
379
468
  - The subjects are plain lines, different from the current subject and from
380
469
  each other, in the language of the email; a merge tag is allowed only from
381
470
  the server's catalog. Otherwise the model gets one more turn; fewer than
@@ -384,9 +473,8 @@ another click another one, the first row brings the user's subject back.
384
473
  `emailWords(mjml)` (the words a reader sees) and `SUBJECTS_SYSTEM_PROMPT`
385
474
  are exported.
386
475
 
387
- The live demo has no server: there, *Generate*, *Rewrite with AI* and
388
- *Suggest subjects* ask for the visitor's own Anthropic key, keep it in the
389
- browser and call Anthropic directly.
476
+ The live demo has no server: there, the AI runs in the page with the
477
+ visitor's own OpenRouter key (`createAi({ provider: 'openrouter', askKey: true })`).
390
478
 
391
479
  ### 5. Your brand (optional)
392
480
 
@@ -461,6 +549,7 @@ The live demo keeps the brand in `localStorage` and has no website field
461
549
  | `onSaved` | — | Called with the saved template. |
462
550
  | `onDirtyChange` | — | Called when the unsaved state changes. |
463
551
  | `dirtyCheckRef` | — | Ref receiving a function `() => boolean` (synchronous dirty check). |
552
+ | `ai` | — | The AI module, `createAi()` from `best-email-builder/ai` (see *AI*). Without it, no AI button and no AI code. |
464
553
 
465
554
  ## Storage
466
555
 
@@ -479,9 +568,6 @@ interface TemplateStorage {
479
568
  getVersion(id, versionId): Promise<Version & { document; mjml }>;
480
569
  uploadImage(file): Promise<{ path }>;
481
570
  render?(input): Promise<{ html; warnings; quality }>; // enables Preview
482
- generate?(input, { signal }): Promise<{ name; subject; preheader; mjml; warnings }>; // enables Generate
483
- rewrite?(input, { signal }): Promise<{ text }>; // enables Rewrite with AI
484
- suggestSubjects?(input, { signal }): Promise<{ subjects }>; // enables Suggest subjects
485
571
  sendTest?(input): Promise<{ sent; failed; invalid; results }>; // enables Send test
486
572
  listVariables?(): Promise<Variable[]>; // enable user-created variables
487
573
  saveVariable?(variable, { create }): Promise<Variable>;
@@ -0,0 +1,68 @@
1
+ import {
2
+ AI_MESSAGES,
3
+ AI_PROVIDERS,
4
+ DEFAULT_CLAUDE_MODEL,
5
+ EMAIL_DRAFT_SCHEMA,
6
+ EMAIL_PURPOSES,
7
+ EmailGenerationError,
8
+ GENERATION_ERRORS,
9
+ MAX_DETAILS_LENGTH,
10
+ PURPOSE_CATEGORIES,
11
+ REWRITE_ACTIONS,
12
+ REWRITE_LANGUAGES,
13
+ REWRITE_SYSTEM_PROMPT,
14
+ SUBJECTS_SYSTEM_PROMPT,
15
+ SYSTEM_PROMPT,
16
+ buildUserPrompt,
17
+ claudeEmailModel,
18
+ cleanGenerateInput,
19
+ createEmailGenerator,
20
+ createRewriter,
21
+ createSubjectSuggester,
22
+ emailWords,
23
+ normalizeGeneratedMjml,
24
+ parseDraft,
25
+ providerEmailModel,
26
+ purposeById,
27
+ purposesByCategory,
28
+ resolveProvider,
29
+ reviewDraft,
30
+ reviewRewrite,
31
+ reviewSubjects
32
+ } from "./chunk-BAA5XMWY.js";
33
+ import "./chunk-OWVXKMLI.js";
34
+ import "./chunk-W424KPCR.js";
35
+ import "./chunk-JI36FNWE.js";
36
+ export {
37
+ AI_MESSAGES,
38
+ AI_PROVIDERS,
39
+ DEFAULT_CLAUDE_MODEL,
40
+ EMAIL_DRAFT_SCHEMA,
41
+ EMAIL_PURPOSES,
42
+ EmailGenerationError,
43
+ GENERATION_ERRORS,
44
+ MAX_DETAILS_LENGTH,
45
+ PURPOSE_CATEGORIES,
46
+ REWRITE_ACTIONS,
47
+ REWRITE_LANGUAGES,
48
+ REWRITE_SYSTEM_PROMPT,
49
+ SUBJECTS_SYSTEM_PROMPT,
50
+ SYSTEM_PROMPT,
51
+ buildUserPrompt,
52
+ claudeEmailModel,
53
+ cleanGenerateInput,
54
+ createEmailGenerator,
55
+ createRewriter,
56
+ createSubjectSuggester,
57
+ emailWords,
58
+ normalizeGeneratedMjml,
59
+ parseDraft,
60
+ providerEmailModel,
61
+ purposeById,
62
+ purposesByCategory,
63
+ resolveProvider,
64
+ reviewDraft,
65
+ reviewRewrite,
66
+ reviewSubjects
67
+ };
68
+ //# sourceMappingURL=ai-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}