@aglyn/plugins-outreach 1.0.0-beta.195 → 1.0.0-beta.197

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 (90) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/curate-step-dialog.d.ts +50 -0
  3. package/src/lib/components/curate-step-dialog.js +235 -0
  4. package/src/lib/components/curate-step-dialog.js.map +1 -0
  5. package/src/lib/components/curated-step-editor.d.ts +80 -0
  6. package/src/lib/components/curated-step-editor.js +255 -0
  7. package/src/lib/components/curated-step-editor.js.map +1 -0
  8. package/src/lib/components/enroll-dialog.d.ts +15 -0
  9. package/src/lib/components/enroll-dialog.js +214 -14
  10. package/src/lib/components/enroll-dialog.js.map +1 -1
  11. package/src/lib/components/enrollments-table.d.ts +2 -0
  12. package/src/lib/components/enrollments-table.js +62 -8
  13. package/src/lib/components/enrollments-table.js.map +1 -1
  14. package/src/lib/components/mailbox-card.js +11 -2
  15. package/src/lib/components/mailbox-card.js.map +1 -1
  16. package/src/lib/components/sequence-detail.js +1 -0
  17. package/src/lib/components/sequence-detail.js.map +1 -1
  18. package/src/lib/components/sequence-editor.d.ts +2 -0
  19. package/src/lib/components/sequence-editor.js +26 -2
  20. package/src/lib/components/sequence-editor.js.map +1 -1
  21. package/src/lib/components/sequence-step-card.d.ts +7 -0
  22. package/src/lib/components/sequence-step-card.js +37 -14
  23. package/src/lib/components/sequence-step-card.js.map +1 -1
  24. package/src/lib/components/sequences-section.js +12 -3
  25. package/src/lib/components/sequences-section.js.map +1 -1
  26. package/src/lib/components/step-test-dialog.d.ts +31 -0
  27. package/src/lib/components/step-test-dialog.js +247 -0
  28. package/src/lib/components/step-test-dialog.js.map +1 -0
  29. package/src/lib/components/use-outreach-api.d.ts +15 -1
  30. package/src/lib/components/use-outreach-api.js +16 -0
  31. package/src/lib/components/use-outreach-api.js.map +1 -1
  32. package/src/lib/components/use-outreach-data.d.ts +6 -1
  33. package/src/lib/components/use-outreach-data.js +6 -3
  34. package/src/lib/components/use-outreach-data.js.map +1 -1
  35. package/src/lib/constants/api-routes.d.ts +15 -0
  36. package/src/lib/constants/api-routes.js +12 -0
  37. package/src/lib/constants/api-routes.js.map +1 -1
  38. package/src/lib/engine/compose.d.ts +12 -2
  39. package/src/lib/engine/compose.js +32 -6
  40. package/src/lib/engine/compose.js.map +1 -1
  41. package/src/lib/engine/curation-prompt.d.ts +73 -0
  42. package/src/lib/engine/curation-prompt.js +151 -0
  43. package/src/lib/engine/curation-prompt.js.map +1 -0
  44. package/src/lib/engine/enrollment-activity.d.ts +24 -0
  45. package/src/lib/engine/enrollment-activity.js +25 -0
  46. package/src/lib/engine/enrollment-activity.js.map +1 -1
  47. package/src/lib/engine/sequence-validation.d.ts +10 -1
  48. package/src/lib/engine/sequence-validation.js +54 -0
  49. package/src/lib/engine/sequence-validation.js.map +1 -1
  50. package/src/lib/enrollment/step-overrides.d.ts +53 -0
  51. package/src/lib/enrollment/step-overrides.js +140 -0
  52. package/src/lib/enrollment/step-overrides.js.map +1 -0
  53. package/src/lib/model/outreach-api.d.ts +89 -2
  54. package/src/lib/model/outreach-api.js.map +1 -1
  55. package/src/lib/model/outreach.types.d.ts +42 -0
  56. package/src/lib/model/outreach.types.js +18 -0
  57. package/src/lib/model/outreach.types.js.map +1 -1
  58. package/src/lib/model/step-preview.d.ts +10 -2
  59. package/src/lib/model/step-preview.js +9 -5
  60. package/src/lib/model/step-preview.js.map +1 -1
  61. package/src/lib/model/stored-records.d.ts +9 -1
  62. package/src/lib/model/stored-records.js +47 -2
  63. package/src/lib/model/stored-records.js.map +1 -1
  64. package/src/lib/routes/curate-routes.d.ts +63 -0
  65. package/src/lib/routes/curate-routes.js +424 -0
  66. package/src/lib/routes/curate-routes.js.map +1 -0
  67. package/src/lib/routes/enroll-routes.d.ts +7 -0
  68. package/src/lib/routes/enroll-routes.js +58 -4
  69. package/src/lib/routes/enroll-routes.js.map +1 -1
  70. package/src/lib/routes/platform-deps.d.ts +2 -2
  71. package/src/lib/routes/platform-deps.js +2 -2
  72. package/src/lib/routes/platform-deps.js.map +1 -1
  73. package/src/lib/routes/preview-routes.d.ts +24 -0
  74. package/src/lib/routes/preview-routes.js +103 -85
  75. package/src/lib/routes/preview-routes.js.map +1 -1
  76. package/src/lib/routes/register-routes.d.ts +7 -0
  77. package/src/lib/routes/register-routes.js +41 -0
  78. package/src/lib/routes/register-routes.js.map +1 -1
  79. package/src/lib/routes/step-test-routes.d.ts +88 -0
  80. package/src/lib/routes/step-test-routes.js +275 -0
  81. package/src/lib/routes/step-test-routes.js.map +1 -0
  82. package/src/lib/runtime/click-events.js +10 -0
  83. package/src/lib/runtime/click-events.js.map +1 -1
  84. package/src/lib/runtime/click-link.d.ts +11 -0
  85. package/src/lib/runtime/click-link.js +9 -4
  86. package/src/lib/runtime/click-link.js.map +1 -1
  87. package/src/lib/runtime/click-route.js +3 -0
  88. package/src/lib/runtime/click-route.js.map +1 -1
  89. package/src/lib/runtime/send-job.js +2 -0
  90. package/src/lib/runtime/send-job.js.map +1 -1
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { Alert, Button, Checkbox, Chip, FormControlLabel, Paper, Stack, TextField, Typography } from "@mui/material";
20
+ import { useMemo } from "react";
21
+ import { isInThreadEmailStep, validateOutreachCuratedStep } from "../engine/sequence-validation.js";
22
+ import { OUTREACH_CURATION_CONFIRMATION_LABEL } from "../model/outreach.types.js";
23
+ /** A drafted step as the route answered it, ready for the member. */ export function outreachDraftState(draft, steps, source, audit) {
24
+ var _draft_subject;
25
+ const startsThread = !isInThreadEmailStep(steps, draft.stepIndex);
26
+ const subject = startsThread ? (_draft_subject = draft.subject) != null ? _draft_subject : '' : '';
27
+ return _extends({
28
+ stepIndex: draft.stepIndex,
29
+ startsThread,
30
+ subject,
31
+ body: draft.body,
32
+ original: {
33
+ subject,
34
+ body: draft.body
35
+ },
36
+ source,
37
+ confirmed: false,
38
+ decision: 'pending'
39
+ }, audit ? {
40
+ prompt: audit.prompt,
41
+ model: audit.model
42
+ } : {});
43
+ }
44
+ /** The member's own starting point: the step's words, for them to rewrite by hand. */ export function outreachMemberDraftStates(steps, stepIndexes) {
45
+ return stepIndexes.flatMap((stepIndex)=>{
46
+ const step = steps[stepIndex];
47
+ if ((step == null ? void 0 : step.kind) !== 'email') return [];
48
+ return [
49
+ outreachDraftState(// A step that sends a CRM template has no words here to start from.
50
+ {
51
+ stepIndex,
52
+ subject: step.subject,
53
+ body: step.templateId ? '' : step.body
54
+ }, steps, 'member')
55
+ ];
56
+ });
57
+ }
58
+ /** The request a USED draft becomes; `null` for one that was kept or is still open. */ export function outreachDraftToOverride(draft) {
59
+ if (draft.decision !== 'use') return null;
60
+ const edited = draft.source === 'ai' && (draft.body !== draft.original.body || draft.startsThread && draft.subject !== draft.original.subject);
61
+ return _extends({
62
+ stepIndex: draft.stepIndex
63
+ }, draft.startsThread ? {
64
+ subject: draft.subject
65
+ } : {}, {
66
+ body: draft.body,
67
+ source: draft.source
68
+ }, edited ? {
69
+ edited: true
70
+ } : {}, draft.source === 'ai' && draft.prompt ? {
71
+ prompt: draft.prompt
72
+ } : {}, draft.source === 'ai' && draft.model ? {
73
+ model: draft.model
74
+ } : {});
75
+ }
76
+ /** The validator's verdict on the draft as it stands. */ export function outreachDraftIssues(draft) {
77
+ return validateOutreachCuratedStep({
78
+ subject: draft.startsThread ? draft.subject : undefined,
79
+ body: draft.body,
80
+ startsThread: draft.startsThread
81
+ }, `stepOverrides.${draft.stepIndex}`);
82
+ }
83
+ /** Whether the draft may be used as it stands: confirmed, and nothing the validator refuses. */ export function outreachDraftUsable(draft) {
84
+ return draft.confirmed && !outreachDraftIssues(draft).some((issue)=>issue.severity === 'error');
85
+ }
86
+ export function OutreachCuratedStepEditor(props) {
87
+ const { draft, label, onChange } = props;
88
+ const disabled = props.disabled === true;
89
+ const issues = useMemo(()=>outreachDraftIssues(draft), [
90
+ draft
91
+ ]);
92
+ const errors = issues.filter((issue)=>issue.severity === 'error');
93
+ const warnings = issues.filter((issue)=>issue.severity === 'warning');
94
+ const change = (next)=>onChange(_extends({}, draft, next));
95
+ const who = draft.source === 'ai' ? 'AI draft' : 'Your words';
96
+ if (draft.decision !== 'pending') {
97
+ return /*#__PURE__*/ _jsx(Paper, {
98
+ variant: "outlined",
99
+ sx: {
100
+ p: 1.5
101
+ },
102
+ "aria-label": `${label} — ${draft.decision === 'use' ? 'curated' : 'template kept'}`,
103
+ children: /*#__PURE__*/ _jsxs(Stack, {
104
+ direction: "row",
105
+ spacing: 1,
106
+ sx: {
107
+ alignItems: 'center',
108
+ flexWrap: 'wrap',
109
+ rowGap: 1
110
+ },
111
+ children: [
112
+ /*#__PURE__*/ _jsx(Typography, {
113
+ variant: "body2",
114
+ sx: {
115
+ flexGrow: 1
116
+ },
117
+ children: label
118
+ }),
119
+ /*#__PURE__*/ _jsx(Chip, {
120
+ size: "small",
121
+ color: draft.decision === 'use' ? 'success' : 'default',
122
+ label: draft.decision === 'use' ? `Curated · ${who}` : 'Template kept'
123
+ }),
124
+ /*#__PURE__*/ _jsx(Button, {
125
+ size: "small",
126
+ onClick: ()=>change({
127
+ decision: 'pending'
128
+ }),
129
+ disabled: disabled,
130
+ children: "Change"
131
+ })
132
+ ]
133
+ })
134
+ });
135
+ }
136
+ return /*#__PURE__*/ _jsx(Paper, {
137
+ variant: "outlined",
138
+ sx: {
139
+ p: 1.5
140
+ },
141
+ "aria-label": `${label} — draft`,
142
+ children: /*#__PURE__*/ _jsxs(Stack, {
143
+ spacing: 1.5,
144
+ children: [
145
+ /*#__PURE__*/ _jsxs(Stack, {
146
+ direction: "row",
147
+ spacing: 1,
148
+ sx: {
149
+ alignItems: 'center'
150
+ },
151
+ children: [
152
+ /*#__PURE__*/ _jsx(Typography, {
153
+ variant: "subtitle2",
154
+ sx: {
155
+ flexGrow: 1
156
+ },
157
+ children: label
158
+ }),
159
+ /*#__PURE__*/ _jsx(Chip, {
160
+ size: "small",
161
+ variant: "outlined",
162
+ label: who
163
+ })
164
+ ]
165
+ }),
166
+ draft.startsThread ? /*#__PURE__*/ _jsx(TextField, {
167
+ label: "Subject",
168
+ value: draft.subject,
169
+ onChange: (event)=>change({
170
+ subject: event.target.value,
171
+ confirmed: false
172
+ }),
173
+ disabled: disabled,
174
+ size: "small",
175
+ fullWidth: true,
176
+ error: errors.some((issue)=>issue.path.endsWith('.subject'))
177
+ }) : /*#__PURE__*/ _jsx(Typography, {
178
+ variant: "caption",
179
+ color: "text.secondary",
180
+ children: "Sent as a reply in the thread, under the thread’s subject."
181
+ }),
182
+ /*#__PURE__*/ _jsx(TextField, {
183
+ label: "Email",
184
+ value: draft.body,
185
+ onChange: (event)=>change({
186
+ body: event.target.value,
187
+ confirmed: false
188
+ }),
189
+ disabled: disabled,
190
+ multiline: true,
191
+ minRows: 6,
192
+ fullWidth: true,
193
+ error: errors.some((issue)=>issue.path.endsWith('.body')),
194
+ helperText: "Plain text. The footer is added when it’s sent."
195
+ }),
196
+ errors.length ? /*#__PURE__*/ _jsx(Alert, {
197
+ severity: "error",
198
+ children: /*#__PURE__*/ _jsx(Stack, {
199
+ component: "ul",
200
+ spacing: 0.5,
201
+ sx: {
202
+ m: 0,
203
+ pl: 2
204
+ },
205
+ children: errors.map((issue)=>/*#__PURE__*/ _jsx("li", {
206
+ children: issue.message
207
+ }, `${issue.path}:${issue.code}`))
208
+ })
209
+ }) : null,
210
+ warnings.length ? /*#__PURE__*/ _jsx(Typography, {
211
+ variant: "caption",
212
+ color: "text.secondary",
213
+ children: warnings.map((issue)=>issue.message).join(' ')
214
+ }) : null,
215
+ /*#__PURE__*/ _jsx(FormControlLabel, {
216
+ control: /*#__PURE__*/ _jsx(Checkbox, {
217
+ checked: draft.confirmed,
218
+ onChange: (event)=>change({
219
+ confirmed: event.target.checked
220
+ }),
221
+ disabled: disabled || errors.length > 0
222
+ }),
223
+ label: OUTREACH_CURATION_CONFIRMATION_LABEL
224
+ }),
225
+ /*#__PURE__*/ _jsxs(Stack, {
226
+ direction: "row",
227
+ spacing: 1,
228
+ children: [
229
+ /*#__PURE__*/ _jsx(Button, {
230
+ size: "small",
231
+ variant: "contained",
232
+ disabled: disabled || !outreachDraftUsable(draft),
233
+ onClick: ()=>change({
234
+ decision: 'use'
235
+ }),
236
+ children: "Use this"
237
+ }),
238
+ /*#__PURE__*/ _jsx(Button, {
239
+ size: "small",
240
+ disabled: disabled,
241
+ onClick: ()=>change({
242
+ decision: 'keep'
243
+ }),
244
+ children: "Keep the template"
245
+ })
246
+ ]
247
+ })
248
+ ]
249
+ })
250
+ });
251
+ }
252
+ OutreachCuratedStepEditor.displayName = 'OutreachCuratedStepEditor';
253
+ export default OutreachCuratedStepEditor;
254
+
255
+ //# sourceMappingURL=curated-step-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/curated-step-editor.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport {\n Alert,\n Button,\n Checkbox,\n Chip,\n FormControlLabel,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo } from 'react'\nimport {\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n type OutreachValidationIssue,\n} from '../engine/sequence-validation'\nimport type { OutreachCurateDraft, OutreachStepOverrideRequest } from '../model/outreach-api'\nimport {\n OUTREACH_CURATION_CONFIRMATION_LABEL,\n type OutreachSequenceStep,\n type OutreachStepOverrideSource,\n} from '../model/outreach.types'\n\n/**\n * ONE PERSON'S COPY OF ONE STEP, AS THE MEMBER READS AND DECIDES IT\n * (AGL-3324).\n *\n * The enroll dialog shows one of these under each person for every email\n * step the AI drafted; the Enrollments tab shows one for the next step. The\n * member edits the subject and the body, reads what the validator says,\n * ticks the confirmation — a fourth attestation beside the three a cold\n * contact asks for — and either USES the copy or KEEPS the template. Until\n * they decide, nothing about the draft leaves the browser: the enroll and\n * the save carry only what was used.\n *\n * Validated as they type, with the same rules the route applies again: a\n * draft that breaks one cannot be used as it stands.\n */\n\n/** What the member holds for one drafted step. */\nexport interface OutreachCuratedDraftState {\n stepIndex: number\n /** Whether the email starts a thread, and so has a subject of its own. */\n startsThread: boolean\n subject: string\n body: string\n /** The words as they were drafted, so an edit can be told from a confirmation. */\n original: { subject: string; body: string }\n source: OutreachStepOverrideSource\n /** The member ticked the confirmation. */\n confirmed: boolean\n /** Used, kept as the template, or still open. */\n decision: 'pending' | 'use' | 'keep'\n /** The AI's prompt and model, for the override's audit record. */\n prompt?: string\n model?: string\n}\n\n/** A drafted step as the route answered it, ready for the member. */\nexport function outreachDraftState(\n draft: Pick<OutreachCurateDraft, 'stepIndex' | 'subject' | 'body'>,\n steps: readonly OutreachSequenceStep[],\n source: OutreachStepOverrideSource,\n audit?: { prompt: string; model: string },\n): OutreachCuratedDraftState {\n const startsThread = !isInThreadEmailStep(steps, draft.stepIndex)\n const subject = startsThread ? (draft.subject ?? '') : ''\n return {\n stepIndex: draft.stepIndex,\n startsThread,\n subject,\n body: draft.body,\n original: { subject, body: draft.body },\n source,\n confirmed: false,\n decision: 'pending',\n ...(audit ? { prompt: audit.prompt, model: audit.model } : {}),\n }\n}\n\n/** The member's own starting point: the step's words, for them to rewrite by hand. */\nexport function outreachMemberDraftStates(\n steps: readonly OutreachSequenceStep[],\n stepIndexes: readonly number[],\n): OutreachCuratedDraftState[] {\n return stepIndexes.flatMap((stepIndex) => {\n const step = steps[stepIndex]\n if (step?.kind !== 'email') return []\n return [\n outreachDraftState(\n // A step that sends a CRM template has no words here to start from.\n { stepIndex, subject: step.subject, body: step.templateId ? '' : step.body },\n steps,\n 'member',\n ),\n ]\n })\n}\n\n/** The request a USED draft becomes; `null` for one that was kept or is still open. */\nexport function outreachDraftToOverride(\n draft: OutreachCuratedDraftState,\n): OutreachStepOverrideRequest | null {\n if (draft.decision !== 'use') return null\n const edited =\n draft.source === 'ai' &&\n (draft.body !== draft.original.body || (draft.startsThread && draft.subject !== draft.original.subject))\n return {\n stepIndex: draft.stepIndex,\n ...(draft.startsThread ? { subject: draft.subject } : {}),\n body: draft.body,\n source: draft.source,\n ...(edited ? { edited: true } : {}),\n ...(draft.source === 'ai' && draft.prompt ? { prompt: draft.prompt } : {}),\n ...(draft.source === 'ai' && draft.model ? { model: draft.model } : {}),\n }\n}\n\n/** The validator's verdict on the draft as it stands. */\nexport function outreachDraftIssues(draft: OutreachCuratedDraftState): OutreachValidationIssue[] {\n return validateOutreachCuratedStep(\n { subject: draft.startsThread ? draft.subject : undefined, body: draft.body, startsThread: draft.startsThread },\n `stepOverrides.${draft.stepIndex}`,\n )\n}\n\n/** Whether the draft may be used as it stands: confirmed, and nothing the validator refuses. */\nexport function outreachDraftUsable(draft: OutreachCuratedDraftState): boolean {\n return draft.confirmed && !outreachDraftIssues(draft).some((issue) => issue.severity === 'error')\n}\n\nexport interface OutreachCuratedStepEditorProps {\n draft: OutreachCuratedDraftState\n /** \"Email 1 · step 1\", as the sequence's preview names it. */\n label: string\n onChange(next: OutreachCuratedDraftState): void\n disabled?: boolean\n}\n\nexport function OutreachCuratedStepEditor(props: OutreachCuratedStepEditorProps) {\n const { draft, label, onChange } = props\n const disabled = props.disabled === true\n const issues = useMemo(() => outreachDraftIssues(draft), [draft])\n const errors = issues.filter((issue) => issue.severity === 'error')\n const warnings = issues.filter((issue) => issue.severity === 'warning')\n const change = (next: Partial<OutreachCuratedDraftState>) => onChange({ ...draft, ...next })\n const who = draft.source === 'ai' ? 'AI draft' : 'Your words'\n\n if (draft.decision !== 'pending') {\n return (\n <Paper variant=\"outlined\" sx={{ p: 1.5 }} aria-label={`${label} — ${draft.decision === 'use' ? 'curated' : 'template kept'}`}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Typography variant=\"body2\" sx={{ flexGrow: 1 }}>\n {label}\n </Typography>\n <Chip\n size=\"small\"\n color={draft.decision === 'use' ? 'success' : 'default'}\n label={draft.decision === 'use' ? `Curated · ${who}` : 'Template kept'}\n />\n <Button size=\"small\" onClick={() => change({ decision: 'pending' })} disabled={disabled}>\n Change\n </Button>\n </Stack>\n </Paper>\n )\n }\n\n return (\n <Paper variant=\"outlined\" sx={{ p: 1.5 }} aria-label={`${label} — draft`}>\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n {label}\n </Typography>\n <Chip size=\"small\" variant=\"outlined\" label={who} />\n </Stack>\n {draft.startsThread ? (\n <TextField\n label=\"Subject\"\n value={draft.subject}\n onChange={(event) => change({ subject: event.target.value, confirmed: false })}\n disabled={disabled}\n size=\"small\"\n fullWidth\n error={errors.some((issue) => issue.path.endsWith('.subject'))}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n Sent as a reply in the thread, under the thread’s subject.\n </Typography>\n )}\n <TextField\n label=\"Email\"\n value={draft.body}\n onChange={(event) => change({ body: event.target.value, confirmed: false })}\n disabled={disabled}\n multiline\n minRows={6}\n fullWidth\n error={errors.some((issue) => issue.path.endsWith('.body'))}\n helperText=\"Plain text. The footer is added when it’s sent.\"\n />\n {errors.length ? (\n <Alert severity=\"error\">\n <Stack component=\"ul\" spacing={0.5} sx={{ m: 0, pl: 2 }}>\n {errors.map((issue) => (\n <li key={`${issue.path}:${issue.code}`}>{issue.message}</li>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {warnings.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {warnings.map((issue) => issue.message).join(' ')}\n </Typography>\n ) : null}\n <FormControlLabel\n control={\n <Checkbox\n checked={draft.confirmed}\n onChange={(event) => change({ confirmed: event.target.checked })}\n disabled={disabled || errors.length > 0}\n />\n }\n label={OUTREACH_CURATION_CONFIRMATION_LABEL}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={disabled || !outreachDraftUsable(draft)}\n onClick={() => change({ decision: 'use' })}\n >\n Use this\n </Button>\n <Button size=\"small\" disabled={disabled} onClick={() => change({ decision: 'keep' })}>\n Keep the template\n </Button>\n </Stack>\n </Stack>\n </Paper>\n )\n}\nOutreachCuratedStepEditor.displayName = 'OutreachCuratedStepEditor'\n\nexport default OutreachCuratedStepEditor\n"],"names":["Alert","Button","Checkbox","Chip","FormControlLabel","Paper","Stack","TextField","Typography","useMemo","isInThreadEmailStep","validateOutreachCuratedStep","OUTREACH_CURATION_CONFIRMATION_LABEL","outreachDraftState","draft","steps","source","audit","startsThread","stepIndex","subject","body","original","confirmed","decision","prompt","model","outreachMemberDraftStates","stepIndexes","flatMap","step","kind","templateId","outreachDraftToOverride","edited","outreachDraftIssues","undefined","outreachDraftUsable","some","issue","severity","OutreachCuratedStepEditor","props","label","onChange","disabled","issues","errors","filter","warnings","change","next","who","variant","sx","p","aria-label","direction","spacing","alignItems","flexWrap","rowGap","flexGrow","size","color","onClick","value","event","target","fullWidth","error","path","endsWith","multiline","minRows","helperText","length","component","m","pl","map","li","message","code","join","control","checked","displayName"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { type OutreachEnrollPreviewPerson } from '../model/outreach-api';
2
2
  import { type OutreachAttestationKind, type OutreachSequence } from '../model/outreach.types';
3
+ import { type OutreachCuratedDraftState } from './curated-step-editor';
3
4
  import type { OutreachApi } from './use-outreach-api';
4
5
  export interface OutreachEnrollDialogProps {
5
6
  open: boolean;
@@ -11,11 +12,25 @@ export interface OutreachEnrollDialogProps {
11
12
  uid: string | null;
12
13
  api: OutreachApi;
13
14
  }
15
+ /** Where one person's curated drafts stand (AGL-3324). */
16
+ type Curation = {
17
+ status: 'idle';
18
+ } | {
19
+ status: 'drafting';
20
+ } | {
21
+ status: 'failed';
22
+ message: string;
23
+ } | {
24
+ status: 'ready';
25
+ };
14
26
  /** What the rep has supplied for one person. */
15
27
  interface Supplied {
16
28
  include: boolean;
17
29
  personalLine: string;
18
30
  attestations: OutreachAttestationKind[];
31
+ /** The email steps drafted for this person, by step index; absent until they curate. */
32
+ drafts?: Record<number, OutreachCuratedDraftState>;
33
+ curation?: Curation;
19
34
  }
20
35
  /** Whether a person may be sent to Confirm as the rep has left them. */
21
36
  export declare function outreachPersonReady(person: OutreachEnrollPreviewPerson, supplied: Supplied | undefined): boolean;
@@ -22,6 +22,8 @@ import { Alert, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, Di
22
22
  import { useMemo, useState } from "react";
23
23
  import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
24
24
  import { OUTREACH_ATTESTATION_LABELS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
25
+ import { outreachEmailStepLabel } from "./curate-step-dialog.js";
26
+ import { OutreachCuratedStepEditor, outreachDraftState, outreachDraftToOverride, outreachMemberDraftStates } from "./curated-step-editor.js";
25
27
  import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
26
28
  import { useOutreachContactSearch, useOutreachLeadSearch, useOutreachSavedViews } from "./use-outreach-crm.js";
27
29
  const STATUS_LABELS = {
@@ -34,10 +36,21 @@ const STATUS_COLORS = {
34
36
  needs_confirmation: 'warning',
35
37
  blocked: 'default'
36
38
  };
39
+ /** The drafts the rep used, as the enroll and the preview carry them. */ function usedOverrides(supplied) {
40
+ var _ref;
41
+ return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).map(outreachDraftToOverride).filter((override)=>override !== null);
42
+ }
43
+ /** Whether a drafted step is still waiting on the rep to use it or keep the template. */ function hasOpenDrafts(supplied) {
44
+ var _ref;
45
+ return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).some((draft)=>draft.decision === 'pending');
46
+ }
37
47
  /** Whether a person may be sent to Confirm as the rep has left them. */ export function outreachPersonReady(person, supplied) {
38
48
  if (person.status === 'blocked' || !(supplied == null ? void 0 : supplied.include)) return false;
39
49
  const line = supplied.personalLine.trim();
40
50
  if (line.length > OUTREACH_PERSONAL_LINE_MAX) return false;
51
+ // A draft the rep has read but neither used nor kept holds them back
52
+ // (AGL-3324): nothing is enrolled with a decision still open.
53
+ if (hasOpenDrafts(supplied)) return false;
41
54
  if (person.status === 'eligible') return true;
42
55
  if (person.requires.personalLine && !line) return false;
43
56
  return person.requires.attestations.every((kind)=>supplied.attestations.includes(kind));
@@ -69,6 +82,7 @@ const STATUS_COLORS = {
69
82
  const [problem, setProblem] = useState(null);
70
83
  const [supplied, setSupplied] = useState({});
71
84
  const [emailPreview, setEmailPreview] = useState({});
85
+ const [stepTests, setStepTests] = useState({});
72
86
  const views = useOutreachSavedViews(props.open ? orgId : null, props.uid);
73
87
  const search = useOutreachContactSearch({
74
88
  orgId: props.open && tab === 'search' ? orgId : null,
@@ -90,6 +104,7 @@ const STATUS_COLORS = {
90
104
  setProblem(null);
91
105
  setSupplied({});
92
106
  setEmailPreview({});
107
+ setStepTests({});
93
108
  };
94
109
  const close = ()=>{
95
110
  reset();
@@ -133,7 +148,7 @@ const STATUS_COLORS = {
133
148
  ]);
134
149
  const waiting = answer ? answer.people.filter((person)=>{
135
150
  var _supplied_person_personId;
136
- return person.status === 'needs_confirmation' && ((_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.include) && !outreachPersonReady(person, supplied[person.personId]);
151
+ return person.status !== 'blocked' && ((_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.include) && !outreachPersonReady(person, supplied[person.personId]);
137
152
  }).length : 0;
138
153
  const enroll = async ()=>{
139
154
  if (!answer || !ready.length) return;
@@ -153,7 +168,9 @@ const STATUS_COLORS = {
153
168
  }, {
154
169
  personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : '',
155
170
  attestations: (_ref1 = (_supplied_person_personId1 = supplied[person.personId]) == null ? void 0 : _supplied_person_personId1.attestations) != null ? _ref1 : []
156
- });
171
+ }, usedOverrides(supplied[person.personId]).length ? {
172
+ stepOverrides: usedOverrides(supplied[person.personId])
173
+ } : {});
157
174
  }));
158
175
  setStage({
159
176
  kind: 'done',
@@ -180,6 +197,7 @@ const STATUS_COLORS = {
180
197
  try {
181
198
  var _ref;
182
199
  var _supplied_person_personId;
200
+ const overrides = usedOverrides(supplied[person.personId]);
183
201
  const result = await api.previewEmail(_extends({
184
202
  sequenceId: sequence.id
185
203
  }, person.target === 'lead' && person.leadId ? {
@@ -188,7 +206,9 @@ const STATUS_COLORS = {
188
206
  contactId: person.contactId
189
207
  }, {
190
208
  personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
191
- }));
209
+ }, overrides.length ? {
210
+ stepOverrides: overrides
211
+ } : {}));
192
212
  setEmailPreview((previous)=>_extends({}, previous, {
193
213
  [person.personId]: {
194
214
  status: 'ready',
@@ -204,6 +224,104 @@ const STATUS_COLORS = {
204
224
  }));
205
225
  }
206
226
  };
227
+ /**
228
+ * Curate for this person (AGL-3324): every email step drafted from their
229
+ * record, the sequence's steps and the personal line as written so far,
230
+ * shown under them for the rep to edit and confirm. Nothing is stored
231
+ * until Enroll carries the drafts they used.
232
+ */ const curate = async (person)=>{
233
+ supply(person.personId, {
234
+ curation: {
235
+ status: 'drafting'
236
+ }
237
+ });
238
+ try {
239
+ var _ref;
240
+ var _supplied_person_personId;
241
+ const answer = await api.curateDrafts(_extends({
242
+ sequenceId: sequence.id
243
+ }, person.target === 'lead' && person.leadId ? {
244
+ leadId: person.leadId
245
+ } : {
246
+ contactId: person.contactId
247
+ }, {
248
+ personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
249
+ }));
250
+ const audit = {
251
+ prompt: answer.prompt,
252
+ model: answer.model
253
+ };
254
+ supply(person.personId, {
255
+ curation: {
256
+ status: 'ready'
257
+ },
258
+ drafts: Object.fromEntries(answer.drafts.map((draft)=>[
259
+ draft.stepIndex,
260
+ outreachDraftState(draft, sequence.steps, 'ai', audit)
261
+ ]))
262
+ });
263
+ } catch (error) {
264
+ supply(person.personId, {
265
+ curation: {
266
+ status: 'failed',
267
+ message: error.message
268
+ }
269
+ });
270
+ }
271
+ };
272
+ /** The rep's own words for every email step, when the AI cannot draft. */ const writeThemselves = (person)=>{
273
+ const stepIndexes = sequence.steps.map((step, index)=>step.kind === 'email' ? index : -1).filter((index)=>index >= 0);
274
+ supply(person.personId, {
275
+ curation: {
276
+ status: 'ready'
277
+ },
278
+ drafts: Object.fromEntries(outreachMemberDraftStates(sequence.steps, stepIndexes).map((draft)=>[
279
+ draft.stepIndex,
280
+ draft
281
+ ]))
282
+ });
283
+ };
284
+ /**
285
+ * This person's first email, sent to the member as a test (AGL-3325):
286
+ * their record, the personal line as written so far and the curated copy
287
+ * they used (AGL-3324) fill it, and nothing is enrolled or stored for
288
+ * them.
289
+ */ const sendTest = async (person)=>{
290
+ setStepTests((previous)=>_extends({}, previous, {
291
+ [person.personId]: {
292
+ status: 'sending'
293
+ }
294
+ }));
295
+ try {
296
+ var _ref;
297
+ var _supplied_person_personId;
298
+ const overrides = usedOverrides(supplied[person.personId]);
299
+ const answer = await api.sendStepTest(_extends({
300
+ sequenceId: sequence.id
301
+ }, person.target === 'lead' && person.leadId ? {
302
+ leadId: person.leadId
303
+ } : {
304
+ contactId: person.contactId
305
+ }, {
306
+ personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
307
+ }, overrides.length ? {
308
+ stepOverrides: overrides
309
+ } : {}));
310
+ setStepTests((previous)=>_extends({}, previous, {
311
+ [person.personId]: {
312
+ status: 'sent',
313
+ answer
314
+ }
315
+ }));
316
+ } catch (error) {
317
+ setStepTests((previous)=>_extends({}, previous, {
318
+ [person.personId]: {
319
+ status: 'failed',
320
+ message: error.message
321
+ }
322
+ }));
323
+ }
324
+ };
207
325
  const title = `Enroll people in ${sequence.name}`;
208
326
  return /*#__PURE__*/ _jsxs(Dialog, {
209
327
  open: props.open,
@@ -464,17 +582,22 @@ const STATUS_COLORS = {
464
582
  "aria-label": "People",
465
583
  children: answer.people.map((person)=>/*#__PURE__*/ _jsx(PersonRow, {
466
584
  person: person,
585
+ steps: sequence.steps,
467
586
  supplied: supplied[person.personId],
468
587
  onSupply: (next)=>supply(person.personId, next),
469
588
  emailPreview: emailPreview[person.personId],
470
589
  onPreviewEmail: ()=>void previewEmail(person),
590
+ stepTest: stepTests[person.personId],
591
+ onSendTest: ()=>void sendTest(person),
592
+ onCurate: ()=>void curate(person),
593
+ onWriteThemselves: ()=>writeThemselves(person),
471
594
  disabled: stage.kind === 'enrolling'
472
595
  }, person.personId))
473
596
  }),
474
597
  waiting ? /*#__PURE__*/ _jsx(Typography, {
475
598
  variant: "body2",
476
599
  color: "text.secondary",
477
- children: `${waiting} ${waiting === 1 ? 'person still needs' : 'people still need'} a personal line or a confirmation, and won't be enrolled until they have one.`
600
+ children: `${waiting} ${waiting === 1 ? 'person still needs' : 'people still need'} a personal line or a confirmation, or a decision on a draft, and won't be enrolled until they have one.`
478
601
  }) : null
479
602
  ]
480
603
  }) : null,
@@ -541,12 +664,17 @@ function summary(answer) {
541
664
  return `${count('eligible')} eligible · ${count('needs_confirmation')} need you · ${count('blocked')} blocked`;
542
665
  }
543
666
  function PersonRow(props) {
544
- var _ref, _ref1;
545
- var _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
667
+ var _ref, _ref1, _ref2, _ref3;
668
+ var _props_stepTest, _props_stepTest1, _props_stepTest2, _props_stepTest3, _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
546
669
  const { person, supplied, disabled } = props;
547
670
  const name = person.name || person.email || person.personId;
548
671
  const line = (_ref = supplied == null ? void 0 : supplied.personalLine) != null ? _ref : '';
549
672
  const tooLong = line.trim().length > OUTREACH_PERSONAL_LINE_MAX;
673
+ const curation = (_ref1 = supplied == null ? void 0 : supplied.curation) != null ? _ref1 : {
674
+ status: 'idle'
675
+ };
676
+ const drafts = Object.values((_ref2 = supplied == null ? void 0 : supplied.drafts) != null ? _ref2 : {}).sort((a, b)=>a.stepIndex - b.stepIndex);
677
+ const usedCount = drafts.filter((draft)=>draft.decision === 'use').length;
550
678
  return /*#__PURE__*/ _jsx(Paper, {
551
679
  component: "li",
552
680
  variant: "outlined",
@@ -630,7 +758,7 @@ function PersonRow(props) {
630
758
  children: [
631
759
  /*#__PURE__*/ _jsx(FormControlLabel, {
632
760
  control: /*#__PURE__*/ _jsx(Checkbox, {
633
- checked: (_ref1 = supplied == null ? void 0 : supplied.include) != null ? _ref1 : true,
761
+ checked: (_ref3 = supplied == null ? void 0 : supplied.include) != null ? _ref3 : true,
634
762
  onChange: (event)=>props.onSupply({
635
763
  include: event.target.checked
636
764
  }),
@@ -676,15 +804,87 @@ function PersonRow(props) {
676
804
  }, kind);
677
805
  })
678
806
  }) : null,
679
- /*#__PURE__*/ _jsx(Stack, {
807
+ /*#__PURE__*/ _jsxs(Stack, {
680
808
  direction: "row",
681
- children: /*#__PURE__*/ _jsx(Button, {
682
- size: "small",
683
- onClick: props.onPreviewEmail,
684
- disabled: disabled,
685
- children: "Preview the first email"
686
- })
809
+ spacing: 1,
810
+ sx: {
811
+ flexWrap: 'wrap',
812
+ rowGap: 1
813
+ },
814
+ children: [
815
+ /*#__PURE__*/ _jsx(Button, {
816
+ size: "small",
817
+ onClick: props.onPreviewEmail,
818
+ disabled: disabled,
819
+ children: "Preview the first email"
820
+ }),
821
+ /*#__PURE__*/ _jsx(Button, {
822
+ size: "small",
823
+ onClick: props.onSendTest,
824
+ disabled: disabled || ((_props_stepTest = props.stepTest) == null ? void 0 : _props_stepTest.status) === 'sending',
825
+ children: ((_props_stepTest1 = props.stepTest) == null ? void 0 : _props_stepTest1.status) === 'sending' ? 'Sending…' : 'Send me this as a test'
826
+ }),
827
+ /*#__PURE__*/ _jsx(Button, {
828
+ size: "small",
829
+ onClick: props.onCurate,
830
+ disabled: disabled || curation.status === 'drafting',
831
+ children: drafts.length ? 'Curate again' : 'Curate for this person'
832
+ })
833
+ ]
687
834
  }),
835
+ ((_props_stepTest2 = props.stepTest) == null ? void 0 : _props_stepTest2.status) === 'failed' ? /*#__PURE__*/ _jsx(Alert, {
836
+ severity: "error",
837
+ children: props.stepTest.message
838
+ }) : null,
839
+ ((_props_stepTest3 = props.stepTest) == null ? void 0 : _props_stepTest3.status) === 'sent' ? /*#__PURE__*/ _jsx(Alert, {
840
+ severity: "success",
841
+ children: `Sent to ${props.stepTest.answer.sentTo} as “${props.stepTest.answer.subject}”. ${name} was not enrolled.`
842
+ }) : null,
843
+ curation.status === 'drafting' ? /*#__PURE__*/ _jsx(OutreachLoading, {
844
+ label: "Drafting their emails…"
845
+ }) : null,
846
+ curation.status === 'failed' ? /*#__PURE__*/ _jsxs(Stack, {
847
+ spacing: 1,
848
+ children: [
849
+ /*#__PURE__*/ _jsx(Alert, {
850
+ severity: "warning",
851
+ children: curation.message
852
+ }),
853
+ /*#__PURE__*/ _jsx(Stack, {
854
+ direction: "row",
855
+ children: /*#__PURE__*/ _jsx(Button, {
856
+ size: "small",
857
+ onClick: props.onWriteThemselves,
858
+ disabled: disabled,
859
+ children: "Write them yourself"
860
+ })
861
+ })
862
+ ]
863
+ }) : null,
864
+ drafts.length ? /*#__PURE__*/ _jsxs(Stack, {
865
+ spacing: 1,
866
+ "aria-label": `Curated emails for ${name}`,
867
+ children: [
868
+ /*#__PURE__*/ _jsx(Typography, {
869
+ variant: "caption",
870
+ color: "text.secondary",
871
+ children: usedCount ? `${usedCount} of ${drafts.length} ${drafts.length === 1 ? 'email' : 'emails'} curated for ${name}. Nothing is sent that you haven't confirmed.` : `Their emails, rewritten for them. Read each one, edit it, and use it or keep the template.`
872
+ }),
873
+ drafts.map((draft)=>/*#__PURE__*/ _jsx(OutreachCuratedStepEditor, {
874
+ draft: draft,
875
+ label: outreachEmailStepLabel(props.steps, draft.stepIndex),
876
+ onChange: (next)=>{
877
+ var _ref;
878
+ return props.onSupply({
879
+ drafts: _extends({}, (_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}, {
880
+ [next.stepIndex]: next
881
+ })
882
+ });
883
+ },
884
+ disabled: disabled
885
+ }, draft.stepIndex))
886
+ ]
887
+ }) : null,
688
888
  ((_props_emailPreview = props.emailPreview) == null ? void 0 : _props_emailPreview.status) === 'loading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
689
889
  label: "Writing the email…"
690
890
  }) : null,