@aglyn/plugins-outreach 1.0.0-beta.194 → 1.0.0-beta.196
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 +154 -13
- 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/use-outreach-api.d.ts +8 -1
- package/src/lib/components/use-outreach-api.js +12 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +8 -0
- package/src/lib/constants/api-routes.js +6 -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 +69 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +36 -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 +3 -1
- package/src/lib/model/step-preview.js +6 -3
- 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/preview-routes.js +15 -1
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +17 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/runtime/send-job.js +2 -0
- package/src/lib/runtime/send-job.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-outreach",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.196",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.196",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.196",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.196",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.196",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.196",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.196",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.196",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.196",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.196",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.196",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.196",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.196",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.196",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
*/
|
|
17
|
+
import type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types';
|
|
18
|
+
import type { OutreachApi } from './use-outreach-api';
|
|
19
|
+
/**
|
|
20
|
+
* CURATE THE NEXT STEP OF AN ENROLLMENT (AGL-3324): Enrollments tab › row ›
|
|
21
|
+
* Curate next step.
|
|
22
|
+
*
|
|
23
|
+
* Step 2 of a sequence was written before the person replied, clicked or
|
|
24
|
+
* changed their site; this rewrites it for them now. The dialog asks the
|
|
25
|
+
* workspace's AI for the next email step's copy the moment it opens, shows
|
|
26
|
+
* it in the editor, and stores it only on "Use this" — through the save
|
|
27
|
+
* route, which validates it again and files "Curated step N" on the
|
|
28
|
+
* person's record. "Keep the template" clears a copy the enrollment already
|
|
29
|
+
* carried for the step, so the step goes back to the sequence's own words.
|
|
30
|
+
*
|
|
31
|
+
* When the AI cannot draft — the workspace has no generator, the member
|
|
32
|
+
* lacks the permission, the allotment is spent — the member can still write
|
|
33
|
+
* the copy themselves, from the step's own words.
|
|
34
|
+
*/
|
|
35
|
+
/** The next email step that has not gone out, or `null`. */
|
|
36
|
+
export declare function outreachNextEmailStepIndex(enrollment: Pick<OutreachEnrollment, 'stepIndex' | 'status'>, steps: readonly OutreachSequenceStep[]): number | null;
|
|
37
|
+
/** "Email 2 · step 3", as the sequence's preview names an email. */
|
|
38
|
+
export declare function outreachEmailStepLabel(steps: readonly OutreachSequenceStep[], stepIndex: number): string;
|
|
39
|
+
export interface OutreachCurateStepDialogProps {
|
|
40
|
+
/** The enrollment to curate, or `null` while closed. */
|
|
41
|
+
enrollment: OutreachEnrollment | null;
|
|
42
|
+
steps: readonly OutreachSequenceStep[];
|
|
43
|
+
api: OutreachApi;
|
|
44
|
+
onClose(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function OutreachCurateStepDialog(props: OutreachCurateStepDialogProps): import("react").JSX.Element;
|
|
47
|
+
export declare namespace OutreachCurateStepDialog {
|
|
48
|
+
var displayName: string;
|
|
49
|
+
}
|
|
50
|
+
export default OutreachCurateStepDialog;
|
|
@@ -0,0 +1,235 @@
|
|
|
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 { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
20
|
+
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, useMediaQuery, useTheme } from "@mui/material";
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { OutreachCuratedStepEditor, outreachDraftState, outreachDraftToOverride, outreachMemberDraftStates } from "./curated-step-editor.js";
|
|
23
|
+
import { OutreachLoading } from "./outreach-ui.js";
|
|
24
|
+
/**
|
|
25
|
+
* CURATE THE NEXT STEP OF AN ENROLLMENT (AGL-3324): Enrollments tab › row ›
|
|
26
|
+
* Curate next step.
|
|
27
|
+
*
|
|
28
|
+
* Step 2 of a sequence was written before the person replied, clicked or
|
|
29
|
+
* changed their site; this rewrites it for them now. The dialog asks the
|
|
30
|
+
* workspace's AI for the next email step's copy the moment it opens, shows
|
|
31
|
+
* it in the editor, and stores it only on "Use this" — through the save
|
|
32
|
+
* route, which validates it again and files "Curated step N" on the
|
|
33
|
+
* person's record. "Keep the template" clears a copy the enrollment already
|
|
34
|
+
* carried for the step, so the step goes back to the sequence's own words.
|
|
35
|
+
*
|
|
36
|
+
* When the AI cannot draft — the workspace has no generator, the member
|
|
37
|
+
* lacks the permission, the allotment is spent — the member can still write
|
|
38
|
+
* the copy themselves, from the step's own words.
|
|
39
|
+
*/ /** The next email step that has not gone out, or `null`. */ export function outreachNextEmailStepIndex(enrollment, steps) {
|
|
40
|
+
if (enrollment.status !== 'active' && enrollment.status !== 'paused') return null;
|
|
41
|
+
const index = steps.findIndex((step, position)=>position >= enrollment.stepIndex && step.kind === 'email');
|
|
42
|
+
return index >= 0 ? index : null;
|
|
43
|
+
}
|
|
44
|
+
/** "Email 2 · step 3", as the sequence's preview names an email. */ export function outreachEmailStepLabel(steps, stepIndex) {
|
|
45
|
+
const position = steps.slice(0, stepIndex + 1).filter((step)=>step.kind === 'email').length;
|
|
46
|
+
return `Email ${position} · step ${stepIndex + 1}`;
|
|
47
|
+
}
|
|
48
|
+
export function OutreachCurateStepDialog(props) {
|
|
49
|
+
var _ref;
|
|
50
|
+
var _enrollment_stepOverrides;
|
|
51
|
+
const { enrollment, steps, api } = props;
|
|
52
|
+
const theme = useTheme();
|
|
53
|
+
const narrow = useMediaQuery(theme.breakpoints.down('sm'));
|
|
54
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
55
|
+
const [stage, setStage] = useState({
|
|
56
|
+
kind: 'drafting'
|
|
57
|
+
});
|
|
58
|
+
const stepIndex = enrollment ? outreachNextEmailStepIndex(enrollment, steps) : null;
|
|
59
|
+
const enrollmentId = (_ref = enrollment == null ? void 0 : enrollment.id) != null ? _ref : null;
|
|
60
|
+
const existing = stepIndex !== null ? enrollment == null ? void 0 : (_enrollment_stepOverrides = enrollment.stepOverrides) == null ? void 0 : _enrollment_stepOverrides[String(stepIndex)] : undefined;
|
|
61
|
+
useEffect(()=>{
|
|
62
|
+
if (!enrollmentId || stepIndex === null) return;
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
setStage({
|
|
65
|
+
kind: 'drafting'
|
|
66
|
+
});
|
|
67
|
+
api.curateDrafts({
|
|
68
|
+
enrollmentId,
|
|
69
|
+
stepIndexes: [
|
|
70
|
+
stepIndex
|
|
71
|
+
]
|
|
72
|
+
}).then((answer)=>{
|
|
73
|
+
if (cancelled) return;
|
|
74
|
+
const draft = answer.drafts.find((entry)=>entry.stepIndex === stepIndex);
|
|
75
|
+
if (!draft) {
|
|
76
|
+
setStage({
|
|
77
|
+
kind: 'failed',
|
|
78
|
+
message: 'No draft came back for this step. Try again.'
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setStage({
|
|
83
|
+
kind: 'editing',
|
|
84
|
+
draft: outreachDraftState(draft, steps, 'ai', {
|
|
85
|
+
prompt: answer.prompt,
|
|
86
|
+
model: answer.model
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
}).catch((error)=>{
|
|
90
|
+
if (!cancelled) setStage({
|
|
91
|
+
kind: 'failed',
|
|
92
|
+
message: error.message
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
return ()=>{
|
|
96
|
+
cancelled = true;
|
|
97
|
+
};
|
|
98
|
+
}, [
|
|
99
|
+
api,
|
|
100
|
+
enrollmentId,
|
|
101
|
+
stepIndex,
|
|
102
|
+
steps
|
|
103
|
+
]);
|
|
104
|
+
const writeItYourself = ()=>{
|
|
105
|
+
if (stepIndex === null) return;
|
|
106
|
+
const [draft] = outreachMemberDraftStates(steps, [
|
|
107
|
+
stepIndex
|
|
108
|
+
]);
|
|
109
|
+
if (draft) {
|
|
110
|
+
var _existing_subject, _existing_body;
|
|
111
|
+
// A copy the enrollment already carries is the better starting point.
|
|
112
|
+
setStage({
|
|
113
|
+
kind: 'editing',
|
|
114
|
+
draft: existing ? _extends({}, draft, {
|
|
115
|
+
subject: (_existing_subject = existing.subject) != null ? _existing_subject : draft.subject,
|
|
116
|
+
body: (_existing_body = existing.body) != null ? _existing_body : draft.body
|
|
117
|
+
}) : draft
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const decide = async (draft)=>{
|
|
122
|
+
if (!enrollmentId || stepIndex === null) return;
|
|
123
|
+
if (draft.decision === 'pending') {
|
|
124
|
+
setStage({
|
|
125
|
+
kind: 'editing',
|
|
126
|
+
draft
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const override = outreachDraftToOverride(draft);
|
|
131
|
+
if (draft.decision === 'keep' && !existing) {
|
|
132
|
+
props.onClose();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
setStage({
|
|
136
|
+
kind: 'saving',
|
|
137
|
+
draft
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
await api.saveCuratedStep(enrollmentId, stepIndex, override ? _extends({}, override.subject !== undefined ? {
|
|
141
|
+
subject: override.subject
|
|
142
|
+
} : {}, {
|
|
143
|
+
body: override.body,
|
|
144
|
+
source: override.source
|
|
145
|
+
}, override.edited ? {
|
|
146
|
+
edited: true
|
|
147
|
+
} : {}, override.prompt ? {
|
|
148
|
+
prompt: override.prompt
|
|
149
|
+
} : {}, override.model ? {
|
|
150
|
+
model: override.model
|
|
151
|
+
} : {}) : null);
|
|
152
|
+
enqueueSnackbar(override ? `Curated step ${stepIndex + 1}. It goes out as written.` : 'Template kept.', {
|
|
153
|
+
variant: 'success'
|
|
154
|
+
});
|
|
155
|
+
props.onClose();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
enqueueSnackbar(error.message, {
|
|
158
|
+
variant: 'error',
|
|
159
|
+
allowDuplicate: true
|
|
160
|
+
});
|
|
161
|
+
setStage({
|
|
162
|
+
kind: 'editing',
|
|
163
|
+
draft: _extends({}, draft, {
|
|
164
|
+
decision: 'pending'
|
|
165
|
+
})
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const title = enrollment ? `Curate the next email for ${enrollment.contactName || enrollment.email}` : 'Curate the next email';
|
|
170
|
+
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
171
|
+
open: Boolean(enrollment),
|
|
172
|
+
onClose: props.onClose,
|
|
173
|
+
fullWidth: true,
|
|
174
|
+
maxWidth: "md",
|
|
175
|
+
fullScreen: narrow,
|
|
176
|
+
"aria-label": title,
|
|
177
|
+
children: [
|
|
178
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
179
|
+
children: title
|
|
180
|
+
}),
|
|
181
|
+
/*#__PURE__*/ _jsx(DialogContent, {
|
|
182
|
+
dividers: true,
|
|
183
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
184
|
+
spacing: 2,
|
|
185
|
+
children: [
|
|
186
|
+
stepIndex === null ? /*#__PURE__*/ _jsx(Alert, {
|
|
187
|
+
severity: "info",
|
|
188
|
+
children: "Every email in this sequence has gone out to this person."
|
|
189
|
+
}) : null,
|
|
190
|
+
stepIndex !== null ? /*#__PURE__*/ _jsx(DialogContentText, {
|
|
191
|
+
children: `${outreachEmailStepLabel(steps, stepIndex)}, rewritten for this person from their record and your personal line. Nothing is sent until you confirm it.`
|
|
192
|
+
}) : null,
|
|
193
|
+
stage.kind === 'drafting' && stepIndex !== null ? /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
194
|
+
label: "Drafting the email…"
|
|
195
|
+
}) : null,
|
|
196
|
+
stage.kind === 'failed' ? /*#__PURE__*/ _jsxs(Stack, {
|
|
197
|
+
spacing: 1,
|
|
198
|
+
children: [
|
|
199
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
200
|
+
severity: "warning",
|
|
201
|
+
children: stage.message
|
|
202
|
+
}),
|
|
203
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
204
|
+
direction: "row",
|
|
205
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
206
|
+
size: "small",
|
|
207
|
+
onClick: writeItYourself,
|
|
208
|
+
children: "Write it yourself"
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
}) : null,
|
|
213
|
+
(stage.kind === 'editing' || stage.kind === 'saving') && stepIndex !== null ? /*#__PURE__*/ _jsx(OutreachCuratedStepEditor, {
|
|
214
|
+
draft: stage.draft,
|
|
215
|
+
label: outreachEmailStepLabel(steps, stepIndex),
|
|
216
|
+
onChange: (next)=>void decide(next),
|
|
217
|
+
disabled: stage.kind === 'saving'
|
|
218
|
+
}) : null
|
|
219
|
+
]
|
|
220
|
+
})
|
|
221
|
+
}),
|
|
222
|
+
/*#__PURE__*/ _jsx(DialogActions, {
|
|
223
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
224
|
+
onClick: props.onClose,
|
|
225
|
+
disabled: stage.kind === 'saving',
|
|
226
|
+
children: "Close"
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
]
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
OutreachCurateStepDialog.displayName = 'OutreachCurateStepDialog';
|
|
233
|
+
export default OutreachCurateStepDialog;
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=curate-step-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/curate-step-dialog.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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types'\nimport {\n OutreachCuratedStepEditor,\n outreachDraftState,\n outreachDraftToOverride,\n outreachMemberDraftStates,\n type OutreachCuratedDraftState,\n} from './curated-step-editor'\nimport { OutreachLoading } from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\n\n/**\n * CURATE THE NEXT STEP OF AN ENROLLMENT (AGL-3324): Enrollments tab › row ›\n * Curate next step.\n *\n * Step 2 of a sequence was written before the person replied, clicked or\n * changed their site; this rewrites it for them now. The dialog asks the\n * workspace's AI for the next email step's copy the moment it opens, shows\n * it in the editor, and stores it only on \"Use this\" — through the save\n * route, which validates it again and files \"Curated step N\" on the\n * person's record. \"Keep the template\" clears a copy the enrollment already\n * carried for the step, so the step goes back to the sequence's own words.\n *\n * When the AI cannot draft — the workspace has no generator, the member\n * lacks the permission, the allotment is spent — the member can still write\n * the copy themselves, from the step's own words.\n */\n\n/** The next email step that has not gone out, or `null`. */\nexport function outreachNextEmailStepIndex(\n enrollment: Pick<OutreachEnrollment, 'stepIndex' | 'status'>,\n steps: readonly OutreachSequenceStep[],\n): number | null {\n if (enrollment.status !== 'active' && enrollment.status !== 'paused') return null\n const index = steps.findIndex((step, position) => position >= enrollment.stepIndex && step.kind === 'email')\n return index >= 0 ? index : null\n}\n\n/** \"Email 2 · step 3\", as the sequence's preview names an email. */\nexport function outreachEmailStepLabel(steps: readonly OutreachSequenceStep[], stepIndex: number): string {\n const position = steps.slice(0, stepIndex + 1).filter((step) => step.kind === 'email').length\n return `Email ${position} · step ${stepIndex + 1}`\n}\n\nexport interface OutreachCurateStepDialogProps {\n /** The enrollment to curate, or `null` while closed. */\n enrollment: OutreachEnrollment | null\n steps: readonly OutreachSequenceStep[]\n api: OutreachApi\n onClose(): void\n}\n\ntype Stage =\n | { kind: 'drafting' }\n | { kind: 'failed'; message: string }\n | { kind: 'editing'; draft: OutreachCuratedDraftState }\n | { kind: 'saving'; draft: OutreachCuratedDraftState }\n\nexport function OutreachCurateStepDialog(props: OutreachCurateStepDialogProps) {\n const { enrollment, steps, api } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('sm'))\n const { enqueueSnackbar } = useSnackbar()\n const [stage, setStage] = useState<Stage>({ kind: 'drafting' })\n const stepIndex = enrollment ? outreachNextEmailStepIndex(enrollment, steps) : null\n const enrollmentId = enrollment?.id ?? null\n const existing = stepIndex !== null ? enrollment?.stepOverrides?.[String(stepIndex)] : undefined\n\n useEffect(() => {\n if (!enrollmentId || stepIndex === null) return\n let cancelled = false\n setStage({ kind: 'drafting' })\n api\n .curateDrafts({ enrollmentId, stepIndexes: [stepIndex] })\n .then((answer) => {\n if (cancelled) return\n const draft = answer.drafts.find((entry) => entry.stepIndex === stepIndex)\n if (!draft) {\n setStage({ kind: 'failed', message: 'No draft came back for this step. Try again.' })\n return\n }\n setStage({\n kind: 'editing',\n draft: outreachDraftState(draft, steps, 'ai', { prompt: answer.prompt, model: answer.model }),\n })\n })\n .catch((error) => {\n if (!cancelled) setStage({ kind: 'failed', message: (error as Error).message })\n })\n return () => {\n cancelled = true\n }\n }, [api, enrollmentId, stepIndex, steps])\n\n const writeItYourself = () => {\n if (stepIndex === null) return\n const [draft] = outreachMemberDraftStates(steps, [stepIndex])\n if (draft) {\n // A copy the enrollment already carries is the better starting point.\n setStage({\n kind: 'editing',\n draft: existing\n ? { ...draft, subject: existing.subject ?? draft.subject, body: existing.body ?? draft.body }\n : draft,\n })\n }\n }\n\n const decide = async (draft: OutreachCuratedDraftState) => {\n if (!enrollmentId || stepIndex === null) return\n if (draft.decision === 'pending') {\n setStage({ kind: 'editing', draft })\n return\n }\n const override = outreachDraftToOverride(draft)\n if (draft.decision === 'keep' && !existing) {\n props.onClose()\n return\n }\n setStage({ kind: 'saving', draft })\n try {\n await api.saveCuratedStep(\n enrollmentId,\n stepIndex,\n override\n ? {\n ...(override.subject !== undefined ? { subject: override.subject } : {}),\n body: override.body,\n source: override.source,\n ...(override.edited ? { edited: true } : {}),\n ...(override.prompt ? { prompt: override.prompt } : {}),\n ...(override.model ? { model: override.model } : {}),\n }\n : null,\n )\n enqueueSnackbar(\n override ? `Curated step ${stepIndex + 1}. It goes out as written.` : 'Template kept.',\n { variant: 'success' },\n )\n props.onClose()\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n setStage({ kind: 'editing', draft: { ...draft, decision: 'pending' } })\n }\n }\n\n const title = enrollment\n ? `Curate the next email for ${enrollment.contactName || enrollment.email}`\n : 'Curate the next email'\n\n return (\n <Dialog open={Boolean(enrollment)} onClose={props.onClose} fullWidth maxWidth=\"md\" fullScreen={narrow} aria-label={title}>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n {stepIndex === null ? (\n <Alert severity=\"info\">Every email in this sequence has gone out to this person.</Alert>\n ) : null}\n {stepIndex !== null ? (\n <DialogContentText>\n {`${outreachEmailStepLabel(steps, stepIndex)}, rewritten for this person from their record and your personal line. Nothing is sent until you confirm it.`}\n </DialogContentText>\n ) : null}\n {stage.kind === 'drafting' && stepIndex !== null ? <OutreachLoading label=\"Drafting the email…\" /> : null}\n {stage.kind === 'failed' ? (\n <Stack spacing={1}>\n <Alert severity=\"warning\">{stage.message}</Alert>\n <Stack direction=\"row\">\n <Button size=\"small\" onClick={writeItYourself}>\n Write it yourself\n </Button>\n </Stack>\n </Stack>\n ) : null}\n {(stage.kind === 'editing' || stage.kind === 'saving') && stepIndex !== null ? (\n <OutreachCuratedStepEditor\n draft={stage.draft}\n label={outreachEmailStepLabel(steps, stepIndex)}\n onChange={(next) => void decide(next)}\n disabled={stage.kind === 'saving'}\n />\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={props.onClose} disabled={stage.kind === 'saving'}>\n Close\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nOutreachCurateStepDialog.displayName = 'OutreachCurateStepDialog'\n\nexport default OutreachCurateStepDialog\n"],"names":["useSnackbar","Alert","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","useMediaQuery","useTheme","useEffect","useState","OutreachCuratedStepEditor","outreachDraftState","outreachDraftToOverride","outreachMemberDraftStates","OutreachLoading","outreachNextEmailStepIndex","enrollment","steps","status","index","findIndex","step","position","stepIndex","kind","outreachEmailStepLabel","slice","filter","length","OutreachCurateStepDialog","props","api","theme","narrow","breakpoints","down","enqueueSnackbar","stage","setStage","enrollmentId","id","existing","stepOverrides","String","undefined","cancelled","curateDrafts","stepIndexes","then","answer","draft","drafts","find","entry","message","prompt","model","catch","error","writeItYourself","subject","body","decide","decision","override","onClose","saveCuratedStep","source","edited","variant","allowDuplicate","title","contactName","email","open","Boolean","fullWidth","maxWidth","fullScreen","aria-label","dividers","spacing","severity","label","direction","size","onClick","onChange","next","disabled","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
*/
|
|
17
|
+
import { type OutreachValidationIssue } from '../engine/sequence-validation';
|
|
18
|
+
import type { OutreachCurateDraft, OutreachStepOverrideRequest } from '../model/outreach-api';
|
|
19
|
+
import { type OutreachSequenceStep, type OutreachStepOverrideSource } from '../model/outreach.types';
|
|
20
|
+
/**
|
|
21
|
+
* ONE PERSON'S COPY OF ONE STEP, AS THE MEMBER READS AND DECIDES IT
|
|
22
|
+
* (AGL-3324).
|
|
23
|
+
*
|
|
24
|
+
* The enroll dialog shows one of these under each person for every email
|
|
25
|
+
* step the AI drafted; the Enrollments tab shows one for the next step. The
|
|
26
|
+
* member edits the subject and the body, reads what the validator says,
|
|
27
|
+
* ticks the confirmation — a fourth attestation beside the three a cold
|
|
28
|
+
* contact asks for — and either USES the copy or KEEPS the template. Until
|
|
29
|
+
* they decide, nothing about the draft leaves the browser: the enroll and
|
|
30
|
+
* the save carry only what was used.
|
|
31
|
+
*
|
|
32
|
+
* Validated as they type, with the same rules the route applies again: a
|
|
33
|
+
* draft that breaks one cannot be used as it stands.
|
|
34
|
+
*/
|
|
35
|
+
/** What the member holds for one drafted step. */
|
|
36
|
+
export interface OutreachCuratedDraftState {
|
|
37
|
+
stepIndex: number;
|
|
38
|
+
/** Whether the email starts a thread, and so has a subject of its own. */
|
|
39
|
+
startsThread: boolean;
|
|
40
|
+
subject: string;
|
|
41
|
+
body: string;
|
|
42
|
+
/** The words as they were drafted, so an edit can be told from a confirmation. */
|
|
43
|
+
original: {
|
|
44
|
+
subject: string;
|
|
45
|
+
body: string;
|
|
46
|
+
};
|
|
47
|
+
source: OutreachStepOverrideSource;
|
|
48
|
+
/** The member ticked the confirmation. */
|
|
49
|
+
confirmed: boolean;
|
|
50
|
+
/** Used, kept as the template, or still open. */
|
|
51
|
+
decision: 'pending' | 'use' | 'keep';
|
|
52
|
+
/** The AI's prompt and model, for the override's audit record. */
|
|
53
|
+
prompt?: string;
|
|
54
|
+
model?: string;
|
|
55
|
+
}
|
|
56
|
+
/** A drafted step as the route answered it, ready for the member. */
|
|
57
|
+
export declare function outreachDraftState(draft: Pick<OutreachCurateDraft, 'stepIndex' | 'subject' | 'body'>, steps: readonly OutreachSequenceStep[], source: OutreachStepOverrideSource, audit?: {
|
|
58
|
+
prompt: string;
|
|
59
|
+
model: string;
|
|
60
|
+
}): OutreachCuratedDraftState;
|
|
61
|
+
/** The member's own starting point: the step's words, for them to rewrite by hand. */
|
|
62
|
+
export declare function outreachMemberDraftStates(steps: readonly OutreachSequenceStep[], stepIndexes: readonly number[]): OutreachCuratedDraftState[];
|
|
63
|
+
/** The request a USED draft becomes; `null` for one that was kept or is still open. */
|
|
64
|
+
export declare function outreachDraftToOverride(draft: OutreachCuratedDraftState): OutreachStepOverrideRequest | null;
|
|
65
|
+
/** The validator's verdict on the draft as it stands. */
|
|
66
|
+
export declare function outreachDraftIssues(draft: OutreachCuratedDraftState): OutreachValidationIssue[];
|
|
67
|
+
/** Whether the draft may be used as it stands: confirmed, and nothing the validator refuses. */
|
|
68
|
+
export declare function outreachDraftUsable(draft: OutreachCuratedDraftState): boolean;
|
|
69
|
+
export interface OutreachCuratedStepEditorProps {
|
|
70
|
+
draft: OutreachCuratedDraftState;
|
|
71
|
+
/** "Email 1 · step 1", as the sequence's preview names it. */
|
|
72
|
+
label: string;
|
|
73
|
+
onChange(next: OutreachCuratedDraftState): void;
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export declare function OutreachCuratedStepEditor(props: OutreachCuratedStepEditorProps): import("react").JSX.Element;
|
|
77
|
+
export declare namespace OutreachCuratedStepEditor {
|
|
78
|
+
var displayName: string;
|
|
79
|
+
}
|
|
80
|
+
export default OutreachCuratedStepEditor;
|