@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.
- package/package.json +14 -14
- package/src/lib/components/curate-step-dialog.d.ts +50 -0
- package/src/lib/components/curate-step-dialog.js +235 -0
- package/src/lib/components/curate-step-dialog.js.map +1 -0
- package/src/lib/components/curated-step-editor.d.ts +80 -0
- package/src/lib/components/curated-step-editor.js +255 -0
- package/src/lib/components/curated-step-editor.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +15 -0
- package/src/lib/components/enroll-dialog.js +214 -14
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +2 -0
- package/src/lib/components/enrollments-table.js +62 -8
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/mailbox-card.js +11 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.d.ts +2 -0
- package/src/lib/components/sequence-editor.js +26 -2
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-step-card.d.ts +7 -0
- package/src/lib/components/sequence-step-card.js +37 -14
- package/src/lib/components/sequence-step-card.js.map +1 -1
- package/src/lib/components/sequences-section.js +12 -3
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/step-test-dialog.d.ts +31 -0
- package/src/lib/components/step-test-dialog.js +247 -0
- package/src/lib/components/step-test-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +15 -1
- package/src/lib/components/use-outreach-api.js +16 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +6 -1
- package/src/lib/components/use-outreach-data.js +6 -3
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +15 -0
- package/src/lib/constants/api-routes.js +12 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/compose.d.ts +12 -2
- package/src/lib/engine/compose.js +32 -6
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/curation-prompt.d.ts +73 -0
- package/src/lib/engine/curation-prompt.js +151 -0
- package/src/lib/engine/curation-prompt.js.map +1 -0
- package/src/lib/engine/enrollment-activity.d.ts +24 -0
- package/src/lib/engine/enrollment-activity.js +25 -0
- package/src/lib/engine/enrollment-activity.js.map +1 -1
- package/src/lib/engine/sequence-validation.d.ts +10 -1
- package/src/lib/engine/sequence-validation.js +54 -0
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/enrollment/step-overrides.d.ts +53 -0
- package/src/lib/enrollment/step-overrides.js +140 -0
- package/src/lib/enrollment/step-overrides.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +89 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +42 -0
- package/src/lib/model/outreach.types.js +18 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +10 -2
- package/src/lib/model/step-preview.js +9 -5
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +9 -1
- package/src/lib/model/stored-records.js +47 -2
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/curate-routes.d.ts +63 -0
- package/src/lib/routes/curate-routes.js +424 -0
- package/src/lib/routes/curate-routes.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +7 -0
- package/src/lib/routes/enroll-routes.js +58 -4
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +2 -2
- package/src/lib/routes/platform-deps.js +2 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.d.ts +24 -0
- package/src/lib/routes/preview-routes.js +103 -85
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +7 -0
- package/src/lib/routes/register-routes.js +41 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/step-test-routes.d.ts +88 -0
- package/src/lib/routes/step-test-routes.js +275 -0
- package/src/lib/routes/step-test-routes.js.map +1 -0
- package/src/lib/runtime/click-events.js +10 -0
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +11 -0
- package/src/lib/runtime/click-link.js +9 -4
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/click-route.js +3 -0
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/send-job.js +2 -0
- 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
|
|
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: (
|
|
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__*/
|
|
807
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
680
808
|
direction: "row",
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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,
|