Email Generator
The email generator produces brand-on marketing emails from a short brief. It is a kept v1 feature: the UI is now served by Next.js, but the pipeline still runs on the Express server, independently of the Director video workflow.
Served by Next.js, backed by Express
The page lives at /email in the Next.js app (web/src/app/(shell)/email/page.tsx → web/src/legacy/pages/EmailFlyer.tsx). All /api/email/* calls are proxied to the Express server (:3001) via next.config rewrites — the pipeline code below is unchanged from v1.
Entry point: server/src/pipeline/emailPipeline.ts
UI: web/src/legacy/pages/EmailFlyer.tsx (route: /email)
Routes: server/src/routes/email.ts (proxied from Next.js)
Renderer: @dexian/renderers/email (MJML → HTML)
Stages
| Stage | What it does | Engine |
|---|---|---|
| brief | Parses the user prompt into an EmailBrief (purpose, tone, audience, key messages, CTA, stats, collaborators) |
Azure OpenAI |
| copy | Generates section-by-section copy (subject line, preheader, headline, body paragraphs, CTA copy) | Azure OpenAI |
| layout | Selects an MJML layout template and maps copy into slots | Template engine + LLM slot-fill |
| html | Renders MJML → cross-client HTML; final asset is sent or downloaded | @dexian/renderers/email (MJML) |
The full status machine has 9 states (queued → 4 in-flight + 4 done states + error). See EmailPipelineStatus in server/src/pipeline/emailTypes.ts.
Inputs
type EmailPrompt = {
purpose: string; // "Launch announcement for X"
tone: 'confident' | 'energetic' | 'calm' | 'innovative';
keyMessages: string[];
cta: { label: string; url: string };
audience?: string;
stats?: { label: string; value: string }[];
collaborators?: string[]; // names/roles for sign-off
layoutTemplate?: string; // override automatic template selection
};
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/email |
Start the pipeline; returns runId |
| GET | /api/email/:runId/status |
Poll status (also streamed via SSE) |
| GET | /api/email/:runId |
Get the full EmailRun (brief + copy + layout) |
| POST | /api/email/:runId/regenerate/:section |
Regenerate one section (brief, copy, layout, or a sub-slot like headline, cta) |
| POST | /api/email/:runId/render-html |
Re-render MJML → HTML after edits |
| POST | /api/email/:runId/send-preview |
Send the rendered HTML to a preview address via Azure Communication Services |
Sectional regeneration
The defining UX feature: every section can be regenerated independently without redoing the whole brief. The flow is backed by a backup/revert system — each edit snapshots the prior state so users can compare and revert.
sequenceDiagram
User->>UI: Edit headline → "Regenerate"
UI->>API: POST /api/email/:id/regenerate/headline
API->>LLM: Re-prompt with current brief + retained sections
LLM-->>API: New headline
API->>UI: Updated EmailRun (snapshot stored)
User->>UI: Don't like it → "Revert"
UI->>API: POST /api/email/:id/revert
API->>UI: Restored prior snapshot
UI (EmailFlyer.tsx)
- Live preview — kinetic text animation while the LLM streams new copy
- Side panel — section list with per-section regenerate buttons
- Layout switcher — try alternative MJML templates without losing copy
- Send preview — input an email address; ACS sends a real test email
Render output
The final HTML is stored under runs/<runId>/email.html alongside the JSON run state. It is also returned inline from /render-html so the frontend can download it directly.
Voice in emails
Some templates include a "voiceover" section — short narration text that can be synthesized via the same @dexian/voice Azure TTS path and embedded as an <audio> link in the email.
Future work
See docs/MASTER-PLAN.md Layer-2 templates section — additional MJML layouts (event invite, product update, monthly digest) are planned but not yet shipped.