From 7e537aade5a45c6572cc6d63691a2a37946c78b4 Mon Sep 17 00:00:00 2001
From: Jared Zwick <52264361+jaredzwick@users.noreply.github.com>
Date: Sun, 3 May 2026 06:55:20 -0400
Subject: [PATCH 1/2] hir-121: AI personalization helper endpoint
Add POST /api/personalize: takes a template_id (resolved from either the
runtime catalog or the HIR-103 markdown pack) and a contact, fills known
{{vars}} server-side, then asks Claude (haiku-4.5 by default) to fill any
remaining placeholders and add 1-2 light personalization touches. Returns
personalized_subject, personalized_body, used_variables, and the SDK usage
object so we can track spend per call.
Wires a "Personalize with AI" button into the existing campaign-create
flow next to "Browse templates". Opens a dialog that takes a contact
(name / company / role), runs the endpoint, and shows a unified line-by-
line diff between the original template and the personalized variant
before applying.
Authenticated callers are rate-limited to one request every two seconds
via the existing in-memory limiter. Endpoint returns 503 when
ANTHROPIC_API_KEY is missing, 502 if the model leaves any {{vars}}
unfilled or returns malformed output.
Tests: 15 unit cases covering prefill, the JSON envelope parser, prompt
shape, file-template loading from templates/*.md (incl. the HIR-103
subjects that begin with `{{vars}}`), and the line-diff. A live smoke
spec calls Anthropic for real and asserts no leftover placeholders;
auto-skipped without ANTHROPIC_API_KEY.
Co-Authored-By: Paperclip
---
.env.example | 6 +
README.md | 40 +++
package.json | 2 +
pnpm-lock.yaml | 99 +++++--
.../dashboard/campaigns/new/page.tsx | 46 ++-
src/app/api/personalize/route.ts | 209 ++++++++++++++
src/components/PersonalizeDialog/index.tsx | 273 ++++++++++++++++++
src/lib/templates/fileLoader.ts | 164 +++++++++++
src/lib/templates/personalize.ts | 148 ++++++++++
src/lib/templates/resolver.ts | 14 +
src/lib/textDiff.ts | 52 ++++
tests/int/personalize.int.spec.ts | 196 +++++++++++++
tests/int/personalizeSmoke.int.spec.ts | 107 +++++++
13 files changed, 1317 insertions(+), 39 deletions(-)
create mode 100644 src/app/api/personalize/route.ts
create mode 100644 src/components/PersonalizeDialog/index.tsx
create mode 100644 src/lib/templates/fileLoader.ts
create mode 100644 src/lib/templates/personalize.ts
create mode 100644 src/lib/templates/resolver.ts
create mode 100644 src/lib/textDiff.ts
create mode 100644 tests/int/personalize.int.spec.ts
create mode 100644 tests/int/personalizeSmoke.int.spec.ts
diff --git a/.env.example b/.env.example
index 444cba1..7186ce2 100644
--- a/.env.example
+++ b/.env.example
@@ -39,3 +39,9 @@ GMAIL_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET_HERE
# Silent-reply follow-up: days between prospect reply and the auto follow-up.
# Pulled at runtime so we can tune in prod without a deploy.
REPLY_FOLLOWUP_OFFSET_DAYS=3
+
+# AI personalization (POST /api/personalize). When unset the endpoint returns
+# 503 and the "Personalize with AI" button surfaces a clean error to the user.
+ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY_HERE
+# Optional override — defaults to claude-haiku-4-5-20251001 (low cost, fast).
+# ANTHROPIC_PERSONALIZE_MODEL=claude-haiku-4-5-20251001
diff --git a/README.md b/README.md
index cc4c35c..66b7ddb 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,46 @@ and follow-up. Each is plaintext, under ~120 words, with a single CTA and
deliverability notes. See [`templates/README.md`](templates/README.md) for
how to load one into a campaign.
+# AI Personalization
+
+Templates give you a starting point. Personalization is what turns a
+starting point into a draft worth sending. Coldflow ships an opt-in helper
+endpoint that takes a contact and a template and returns a personalized
+variant — filling any remaining `{{vars}}` and adding 1–2 light touches
+that acknowledge the recipient's role and reference their company
+specifically.
+
+**UI:** open `/dashboard/campaigns/new`, pick a template, then click
+**Personalize with AI**. Provide a contact (name, company, role) and
+review the line-by-line diff before applying.
+
+**API:** `POST /api/personalize`
+
+```
+{
+ "template_id": "sales_founder_direct",
+ "contact": {
+ "name": "Alex Chen",
+ "company": "Acme Robotics",
+ "role": "VP of Engineering",
+ "product_name": "Coldflow",
+ "sender_name": "Jared"
+ }
+}
+```
+
+Any extra string fields on `contact` become optional context — variables
+like `{{product_name}}` are filled deterministically server-side before
+the LLM is asked to add personalization touches. The response includes
+`personalized_subject`, `personalized_body`, `used_variables`, and the
+SDK `usage` object so you can track spend. Authenticated callers are
+limited to one request every two seconds.
+
+**Setup:** add `ANTHROPIC_API_KEY=…` to your `.env` (see `.env.example`).
+Without a key the endpoint returns 503 and the UI shows a clean error.
+Default model is `claude-haiku-4-5-20251001`; override with
+`ANTHROPIC_PERSONALIZE_MODEL`.
+
# Move the needle TO-DO list:
- [ ] Integration with GHL / N8N
diff --git a/package.json b/package.json
index 6e09c49..c0cb260 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"db:studio": "pnpm --filter db db:studio"
},
"dependencies": {
+ "@anthropic-ai/sdk": "^0.92.0",
"@payloadcms/admin-bar": "3.64.0",
"@payloadcms/db-postgres": "3.64.0",
"@payloadcms/live-preview-react": "3.64.0",
@@ -67,6 +68,7 @@
"sharp": "0.34.2",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
+ "yaml": "^2.8.4",
"zod": "^4.1.13"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ce5a3f4..9859502 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@anthropic-ai/sdk':
+ specifier: ^0.92.0
+ version: 0.92.0(zod@4.1.13)
'@payloadcms/admin-bar':
specifier: 3.64.0
version: 3.64.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -136,7 +139,10 @@ importers:
version: 2.6.0
tailwindcss-animate:
specifier: ^1.0.7
- version: 1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.2))
+ version: 1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))
+ yaml:
+ specifier: ^2.8.4
+ version: 2.8.4
zod:
specifier: ^4.1.13
version: 4.1.13
@@ -149,7 +155,7 @@ importers:
version: 1.56.1
'@tailwindcss/typography':
specifier: ^0.5.13
- version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.2))
+ version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))
'@testing-library/react':
specifier: 16.3.0
version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -167,7 +173,7 @@ importers:
version: 19.1.6(@types/react@19.1.8)
'@vitejs/plugin-react':
specifier: 4.5.2
- version: 4.5.2(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2))
+ version: 4.5.2(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4))
autoprefixer:
specifier: ^10.4.19
version: 10.4.22(postcss@8.5.6)
@@ -197,16 +203,16 @@ importers:
version: 3.7.4
tailwindcss:
specifier: ^3.4.3
- version: 3.4.18(tsx@4.20.6)(yaml@2.8.2)
+ version: 3.4.18(tsx@4.20.6)(yaml@2.8.4)
typescript:
specifier: 5.7.3
version: 5.7.3
vite-tsconfig-paths:
specifier: 5.1.4
- version: 5.1.4(typescript@5.7.3)(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2))
+ version: 5.1.4(typescript@5.7.3)(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4))
vitest:
specifier: 3.2.3
- version: 3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@1.21.7)(jsdom@26.1.0)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
+ version: 3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@1.21.7)(jsdom@26.1.0)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
libs/auth:
dependencies:
@@ -251,6 +257,15 @@ packages:
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
+ '@anthropic-ai/sdk@0.92.0':
+ resolution: {integrity: sha512-l653JFC83wCglH8H83t1xpgDurCyPyslYW1maPRdCsfuNuGbLvQjQ81sWd3Go3LWRm0jNspzAhuqAYV8r9joSw==}
+ hasBin: true
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+ peerDependenciesMeta:
+ zod:
+ optional: true
+
'@apidevtools/json-schema-ref-parser@11.9.3':
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
engines: {node: '>= 16'}
@@ -4474,6 +4489,10 @@ packages:
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+ json-schema-to-ts@3.1.1:
+ resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
+ engines: {node: '>=16'}
+
json-schema-to-typescript@15.0.3:
resolution: {integrity: sha512-iOKdzTUWEVM4nlxpFudFsWyUiu/Jakkga4OZPEt7CGoSEsAsUgdOZqR6pcgx2STBek9Gm4hcarJpXSzIvZ/hKA==}
engines: {node: '>=16.0.0'}
@@ -6090,6 +6109,9 @@ packages:
truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
+ ts-algebra@2.0.0:
+ resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
+
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
@@ -6544,8 +6566,8 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
- yaml@2.8.2:
- resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
+ yaml@2.8.4:
+ resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
engines: {node: '>= 14.6'}
hasBin: true
@@ -6598,6 +6620,12 @@ snapshots:
package-manager-detector: 1.6.0
tinyexec: 1.0.2
+ '@anthropic-ai/sdk@0.92.0(zod@4.1.13)':
+ dependencies:
+ json-schema-to-ts: 3.1.1
+ optionalDependencies:
+ zod: 4.1.13
+
'@apidevtools/json-schema-ref-parser@11.9.3':
dependencies:
'@jsdevtools/ono': 7.1.3
@@ -8510,10 +8538,10 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.2))':
+ '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))':
dependencies:
postcss-selector-parser: 6.0.10
- tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.2)
+ tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.4)
'@tanstack/react-virtual@3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
@@ -8973,7 +9001,7 @@ snapshots:
is-buffer: 2.0.5
undici: 5.29.0
- '@vitejs/plugin-react@4.5.2(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2))':
+ '@vitejs/plugin-react@4.5.2(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
@@ -8981,7 +9009,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.11
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
- vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
+ vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
transitivePeerDependencies:
- supports-color
@@ -8993,13 +9021,13 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.3(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2))':
+ '@vitest/mocker@3.2.3(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4))':
dependencies:
'@vitest/spy': 3.2.3
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
+ vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
'@vitest/pretty-format@3.2.3':
dependencies:
@@ -11092,6 +11120,11 @@ snapshots:
json-parse-even-better-errors@2.3.1: {}
+ json-schema-to-ts@3.1.1:
+ dependencies:
+ '@babel/runtime': 7.28.4
+ ts-algebra: 2.0.0
+
json-schema-to-typescript@15.0.3:
dependencies:
'@apidevtools/json-schema-ref-parser': 11.9.3
@@ -11936,7 +11969,7 @@ snapshots:
unist-util-remove: 4.0.0
unist-util-visit: 5.0.0
unist-util-visit-children: 3.0.0
- yaml: 2.8.2
+ yaml: 2.8.4
zod: 4.1.13
transitivePeerDependencies:
- supports-color
@@ -12302,14 +12335,14 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.5.6
- postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.2):
+ postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.4):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 1.21.7
postcss: 8.5.6
tsx: 4.20.6
- yaml: 2.8.2
+ yaml: 2.8.4
postcss-nested@6.2.0(postcss@8.5.6):
dependencies:
@@ -13213,11 +13246,11 @@ snapshots:
tailwind-merge@2.6.0: {}
- tailwindcss-animate@1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.2)):
+ tailwindcss-animate@1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4)):
dependencies:
- tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.2)
+ tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.4)
- tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.2):
+ tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -13236,7 +13269,7 @@ snapshots:
postcss: 8.5.6
postcss-import: 15.1.0(postcss@8.5.6)
postcss-js: 4.1.0(postcss@8.5.6)
- postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.2)
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.4)
postcss-nested: 6.2.0(postcss@8.5.6)
postcss-selector-parser: 6.1.2
resolve: 1.22.11
@@ -13327,6 +13360,8 @@ snapshots:
dependencies:
utf8-byte-length: 1.0.5
+ ts-algebra@2.0.0: {}
+
ts-api-utils@2.1.0(typescript@5.7.3):
dependencies:
typescript: 5.7.3
@@ -13611,13 +13646,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- vite-node@3.2.3(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2):
+ vite-node@3.2.3(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
+ vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -13632,18 +13667,18 @@ snapshots:
- tsx
- yaml
- vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)):
+ vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)):
dependencies:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.7.3)
optionalDependencies:
- vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
+ vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
transitivePeerDependencies:
- supports-color
- typescript
- vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2):
+ vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -13657,13 +13692,13 @@ snapshots:
jiti: 1.21.7
sass: 1.77.4
tsx: 4.20.6
- yaml: 2.8.2
+ yaml: 2.8.4
- vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@1.21.7)(jsdom@26.1.0)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2):
+ vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@1.21.7)(jsdom@26.1.0)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.3
- '@vitest/mocker': 3.2.3(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2))
+ '@vitest/mocker': 3.2.3(vite@7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.3
'@vitest/snapshot': 3.2.3
@@ -13681,8 +13716,8 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
- vite-node: 3.2.3(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.2)
+ vite: 7.2.6(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
+ vite-node: 3.2.3(@types/node@22.5.4)(jiti@1.21.7)(sass@1.77.4)(tsx@4.20.6)(yaml@2.8.4)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
@@ -13831,7 +13866,7 @@ snapshots:
yaml@1.10.2: {}
- yaml@2.8.2: {}
+ yaml@2.8.4: {}
yargs-parser@20.2.9: {}
diff --git a/src/app/(frontend)/dashboard/campaigns/new/page.tsx b/src/app/(frontend)/dashboard/campaigns/new/page.tsx
index c3f70f7..a627f45 100644
--- a/src/app/(frontend)/dashboard/campaigns/new/page.tsx
+++ b/src/app/(frontend)/dashboard/campaigns/new/page.tsx
@@ -14,6 +14,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { TemplatePicker } from '@/components/TemplatePicker'
+import { PersonalizeDialog } from '@/components/PersonalizeDialog'
import {
getTemplateById,
type EmailTemplate,
@@ -41,6 +42,8 @@ function NewCampaignPageInner() {
const [variables, setVariables] = useState([])
const [recipientsInput, setRecipientsInput] = useState('')
const [pickerOpen, setPickerOpen] = useState(false)
+ const [personalizeOpen, setPersonalizeOpen] = useState(false)
+ const [activeTemplateId, setActiveTemplateId] = useState(null)
const [accounts, setAccounts] = useState([])
const [accountsError, setAccountsError] = useState(null)
const [emailAccountId, setEmailAccountId] = useState('')
@@ -50,6 +53,7 @@ function NewCampaignPageInner() {
setSubject(template.subject)
setBody(template.body)
setVariables(template.variables)
+ setActiveTemplateId(template.id)
setName((current) => current || template.name)
}, [])
@@ -139,13 +143,29 @@ function NewCampaignPageInner() {
and other variables that get replaced per recipient.
-
+
+
+
+
+ {
+ setSubject(newSubject)
+ setBody(newBody)
+ }}
+ />
+