Seth commited on
Commit ·
42babfe
1
Parent(s): a13ea2b
update
Browse files
frontend/src/components/campaigns/CampaignSequenceBuilder.jsx
CHANGED
|
@@ -11,6 +11,13 @@ import {
|
|
| 11 |
X,
|
| 12 |
} from 'lucide-react';
|
| 13 |
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
function uid() {
|
| 16 |
return typeof crypto !== 'undefined' && crypto.randomUUID
|
|
@@ -196,13 +203,25 @@ function WaitCard({ step, onRemove, onDaysChange }) {
|
|
| 196 |
);
|
| 197 |
}
|
| 198 |
|
| 199 |
-
function ActionCard({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
const sender = (senderHint || '').trim();
|
| 201 |
const account = (accountHint || '').trim();
|
| 202 |
const showSender = sender && (!account || sender !== account);
|
| 203 |
const showLiMeta = step.channel === 'linkedin' && (showSender || account);
|
| 204 |
const mailbox = (emailAccountHint || '').trim();
|
| 205 |
const showEmailMeta = step.channel === 'gmail' && !!mailbox;
|
|
|
|
|
|
|
|
|
|
| 206 |
return (
|
| 207 |
<div className="relative w-full max-w-md">
|
| 208 |
<div className="flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-left shadow-sm">
|
|
@@ -224,7 +243,35 @@ function ActionCard({ step, onRemove, senderHint, accountHint, emailAccountHint
|
|
| 224 |
{account ? <span>Profile: {account}</span> : null}
|
| 225 |
</p>
|
| 226 |
) : null}
|
| 227 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
<p className="mt-1 text-[11px] leading-snug text-violet-700">Mailbox: {mailbox}</p>
|
| 229 |
) : null}
|
| 230 |
</div>
|
|
@@ -291,13 +338,30 @@ export default function CampaignSequenceBuilder({ value, onChange, linkedinDefau
|
|
| 291 |
linkedinDefaults?.default_unipile_account_ref_id ?? null;
|
| 292 |
newStep.sender_profile_name = linkedinDefaults?.linkedin_profile_display_name || '';
|
| 293 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
}
|
| 295 |
const next = [...steps];
|
| 296 |
next.splice(index, 0, newStep);
|
| 297 |
setSteps(next);
|
| 298 |
closePicker();
|
| 299 |
},
|
| 300 |
-
[steps, setSteps, closePicker, linkedinDefaults]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
);
|
| 302 |
|
| 303 |
const removeAt = useCallback(
|
|
@@ -367,7 +431,21 @@ export default function CampaignSequenceBuilder({ value, onChange, linkedinDefau
|
|
| 367 |
/>
|
| 368 |
</div>
|
| 369 |
|
| 370 |
-
{steps.map((step, index) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
<React.Fragment key={step.id}>
|
| 372 |
{step.type === 'wait' ? (
|
| 373 |
<WaitCard
|
|
@@ -409,6 +487,15 @@ export default function CampaignSequenceBuilder({ value, onChange, linkedinDefau
|
|
| 409 |
)
|
| 410 |
: ''
|
| 411 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
onRemove={() => removeAt(index)}
|
| 413 |
/>
|
| 414 |
)}
|
|
@@ -431,7 +518,8 @@ export default function CampaignSequenceBuilder({ value, onChange, linkedinDefau
|
|
| 431 |
/>
|
| 432 |
</div>
|
| 433 |
</React.Fragment>
|
| 434 |
-
|
|
|
|
| 435 |
|
| 436 |
{/* END */}
|
| 437 |
<div className="mt-1 flex flex-col items-center">
|
|
|
|
| 11 |
X,
|
| 12 |
} from 'lucide-react';
|
| 13 |
import { cn } from '@/lib/utils';
|
| 14 |
+
import {
|
| 15 |
+
Select,
|
| 16 |
+
SelectContent,
|
| 17 |
+
SelectItem,
|
| 18 |
+
SelectTrigger,
|
| 19 |
+
SelectValue,
|
| 20 |
+
} from '@/components/ui/select';
|
| 21 |
|
| 22 |
function uid() {
|
| 23 |
return typeof crypto !== 'undefined' && crypto.randomUUID
|
|
|
|
| 203 |
);
|
| 204 |
}
|
| 205 |
|
| 206 |
+
function ActionCard({
|
| 207 |
+
step,
|
| 208 |
+
onRemove,
|
| 209 |
+
senderHint,
|
| 210 |
+
accountHint,
|
| 211 |
+
emailAccountHint,
|
| 212 |
+
emailMailboxAccounts,
|
| 213 |
+
emailMailboxSelectValue,
|
| 214 |
+
onEmailMailboxChange,
|
| 215 |
+
}) {
|
| 216 |
const sender = (senderHint || '').trim();
|
| 217 |
const account = (accountHint || '').trim();
|
| 218 |
const showSender = sender && (!account || sender !== account);
|
| 219 |
const showLiMeta = step.channel === 'linkedin' && (showSender || account);
|
| 220 |
const mailbox = (emailAccountHint || '').trim();
|
| 221 |
const showEmailMeta = step.channel === 'gmail' && !!mailbox;
|
| 222 |
+
const mailAccounts = emailMailboxAccounts || [];
|
| 223 |
+
const showEmailMailboxSelect =
|
| 224 |
+
step.channel === 'gmail' && mailAccounts.length > 1 && typeof onEmailMailboxChange === 'function';
|
| 225 |
return (
|
| 226 |
<div className="relative w-full max-w-md">
|
| 227 |
<div className="flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-left shadow-sm">
|
|
|
|
| 243 |
{account ? <span>Profile: {account}</span> : null}
|
| 244 |
</p>
|
| 245 |
) : null}
|
| 246 |
+
{showEmailMailboxSelect ? (
|
| 247 |
+
<div
|
| 248 |
+
className="mt-2 space-y-1"
|
| 249 |
+
onClick={(e) => e.stopPropagation()}
|
| 250 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 251 |
+
>
|
| 252 |
+
<p className="text-[10px] font-medium uppercase tracking-wide text-slate-500">
|
| 253 |
+
Mailbox
|
| 254 |
+
</p>
|
| 255 |
+
<Select
|
| 256 |
+
value={String(emailMailboxSelectValue ?? mailAccounts[0]?.id ?? '')}
|
| 257 |
+
onValueChange={(v) => onEmailMailboxChange(Number(v))}
|
| 258 |
+
>
|
| 259 |
+
<SelectTrigger className="h-9 w-full max-w-[240px] border-slate-200 text-xs">
|
| 260 |
+
<SelectValue placeholder="Choose mailbox" />
|
| 261 |
+
</SelectTrigger>
|
| 262 |
+
<SelectContent>
|
| 263 |
+
{mailAccounts.map((a) => {
|
| 264 |
+
const label = (a.display_name || a.label || 'Mailbox').trim();
|
| 265 |
+
return (
|
| 266 |
+
<SelectItem key={a.id} value={String(a.id)}>
|
| 267 |
+
{label}
|
| 268 |
+
</SelectItem>
|
| 269 |
+
);
|
| 270 |
+
})}
|
| 271 |
+
</SelectContent>
|
| 272 |
+
</Select>
|
| 273 |
+
</div>
|
| 274 |
+
) : showEmailMeta ? (
|
| 275 |
<p className="mt-1 text-[11px] leading-snug text-violet-700">Mailbox: {mailbox}</p>
|
| 276 |
) : null}
|
| 277 |
</div>
|
|
|
|
| 338 |
linkedinDefaults?.default_unipile_account_ref_id ?? null;
|
| 339 |
newStep.sender_profile_name = linkedinDefaults?.linkedin_profile_display_name || '';
|
| 340 |
}
|
| 341 |
+
if (preset.channel === 'gmail') {
|
| 342 |
+
newStep.unipile_account_ref_id =
|
| 343 |
+
mailboxDefaults?.default_mailbox_unipile_account_ref_id ?? null;
|
| 344 |
+
}
|
| 345 |
}
|
| 346 |
const next = [...steps];
|
| 347 |
next.splice(index, 0, newStep);
|
| 348 |
setSteps(next);
|
| 349 |
closePicker();
|
| 350 |
},
|
| 351 |
+
[steps, setSteps, closePicker, linkedinDefaults, mailboxDefaults]
|
| 352 |
+
);
|
| 353 |
+
|
| 354 |
+
const updateStepMailbox = useCallback(
|
| 355 |
+
(index, refId) => {
|
| 356 |
+
setSteps((prev) =>
|
| 357 |
+
prev.map((s, i) =>
|
| 358 |
+
i === index && s.type === 'action' && s.channel === 'gmail'
|
| 359 |
+
? { ...s, unipile_account_ref_id: refId }
|
| 360 |
+
: s
|
| 361 |
+
)
|
| 362 |
+
);
|
| 363 |
+
},
|
| 364 |
+
[setSteps]
|
| 365 |
);
|
| 366 |
|
| 367 |
const removeAt = useCallback(
|
|
|
|
| 431 |
/>
|
| 432 |
</div>
|
| 433 |
|
| 434 |
+
{steps.map((step, index) => {
|
| 435 |
+
const mailAccts = mailboxDefaults?.accounts || [];
|
| 436 |
+
const defaultMailRef = mailboxDefaults?.default_mailbox_unipile_account_ref_id ?? null;
|
| 437 |
+
const effMailRef =
|
| 438 |
+
step.type === 'action' && step.channel === 'gmail'
|
| 439 |
+
? step.unipile_account_ref_id ?? defaultMailRef
|
| 440 |
+
: null;
|
| 441 |
+
const mailIdSet = new Set(mailAccts.map((a) => Number(a.id)));
|
| 442 |
+
const emailSelectValue =
|
| 443 |
+
effMailRef != null && mailIdSet.has(Number(effMailRef))
|
| 444 |
+
? Number(effMailRef)
|
| 445 |
+
: mailAccts[0]?.id != null
|
| 446 |
+
? Number(mailAccts[0].id)
|
| 447 |
+
: null;
|
| 448 |
+
return (
|
| 449 |
<React.Fragment key={step.id}>
|
| 450 |
{step.type === 'wait' ? (
|
| 451 |
<WaitCard
|
|
|
|
| 487 |
)
|
| 488 |
: ''
|
| 489 |
}
|
| 490 |
+
emailMailboxAccounts={step.channel === 'gmail' ? mailAccts : []}
|
| 491 |
+
emailMailboxSelectValue={
|
| 492 |
+
step.channel === 'gmail' ? emailSelectValue : null
|
| 493 |
+
}
|
| 494 |
+
onEmailMailboxChange={
|
| 495 |
+
step.channel === 'gmail' && mailAccts.length > 1
|
| 496 |
+
? (id) => updateStepMailbox(index, id)
|
| 497 |
+
: undefined
|
| 498 |
+
}
|
| 499 |
onRemove={() => removeAt(index)}
|
| 500 |
/>
|
| 501 |
)}
|
|
|
|
| 518 |
/>
|
| 519 |
</div>
|
| 520 |
</React.Fragment>
|
| 521 |
+
);
|
| 522 |
+
})}
|
| 523 |
|
| 524 |
{/* END */}
|
| 525 |
<div className="mt-1 flex flex-col items-center">
|
frontend/src/components/campaigns/CreateCampaignWizard.jsx
CHANGED
|
@@ -54,7 +54,16 @@ function estimateCsvRows(file) {
|
|
| 54 |
});
|
| 55 |
}
|
| 56 |
|
| 57 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
const lines = [
|
| 59 |
WIZARD_SEQUENCE_TAG,
|
| 60 |
'(AUTHORITATIVE — OVERRIDES ANY FIXED “N EMAILS” OR “N MESSAGES” COUNT IN THIS PROMPT)',
|
|
@@ -71,7 +80,10 @@ function buildCampaignSequenceAppendix(steps) {
|
|
| 71 |
if (s.type === 'action') {
|
| 72 |
n += 1;
|
| 73 |
if (s.channel === 'gmail') {
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
} else if (s.channel === 'linkedin') {
|
| 76 |
const kind =
|
| 77 |
s.action === 'linkedin_connect'
|
|
@@ -542,9 +554,27 @@ export default function CreateCampaignWizard({
|
|
| 542 |
);
|
| 543 |
}, [linkedinDefaults]);
|
| 544 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
useEffect(() => {
|
| 546 |
if (step !== 3 || selectedProducts.length === 0) return;
|
| 547 |
-
const appendix = buildCampaignSequenceAppendix(sequenceSteps);
|
| 548 |
setPrompts((prev) => {
|
| 549 |
const next = { ...prev };
|
| 550 |
let changed = false;
|
|
@@ -576,7 +606,7 @@ export default function CreateCampaignWizard({
|
|
| 576 |
});
|
| 577 |
return changed ? next : prev;
|
| 578 |
});
|
| 579 |
-
}, [step, sequenceSteps, selectedProducts, sequenceHasLinkedin]);
|
| 580 |
|
| 581 |
useEffect(() => {
|
| 582 |
if (!genRunning || !wizardUpload?.fileId) return;
|
|
|
|
| 54 |
});
|
| 55 |
}
|
| 56 |
|
| 57 |
+
function mailboxLabelForAppendixStep(step, mailboxDefaults) {
|
| 58 |
+
if (step.channel !== 'gmail' || !mailboxDefaults) return '';
|
| 59 |
+
const ref =
|
| 60 |
+
step.unipile_account_ref_id ?? mailboxDefaults.default_mailbox_unipile_account_ref_id ?? null;
|
| 61 |
+
if (ref == null) return '';
|
| 62 |
+
const a = (mailboxDefaults.accounts || []).find((x) => Number(x.id) === Number(ref));
|
| 63 |
+
return ((a?.display_name || a?.label || '') || '').trim();
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function buildCampaignSequenceAppendix(steps, mailboxDefaults = null) {
|
| 67 |
const lines = [
|
| 68 |
WIZARD_SEQUENCE_TAG,
|
| 69 |
'(AUTHORITATIVE — OVERRIDES ANY FIXED “N EMAILS” OR “N MESSAGES” COUNT IN THIS PROMPT)',
|
|
|
|
| 80 |
if (s.type === 'action') {
|
| 81 |
n += 1;
|
| 82 |
if (s.channel === 'gmail') {
|
| 83 |
+
const mb = mailboxLabelForAppendixStep(s, mailboxDefaults);
|
| 84 |
+
lines.push(
|
| 85 |
+
`${n}. Gmail — ${s.title || 'Email'}${mb ? ` (sending mailbox: ${mb})` : ''}`
|
| 86 |
+
);
|
| 87 |
} else if (s.channel === 'linkedin') {
|
| 88 |
const kind =
|
| 89 |
s.action === 'linkedin_connect'
|
|
|
|
| 554 |
);
|
| 555 |
}, [linkedinDefaults]);
|
| 556 |
|
| 557 |
+
useEffect(() => {
|
| 558 |
+
if (!mailboxDefaults) return;
|
| 559 |
+
const accounts = mailboxDefaults.accounts || [];
|
| 560 |
+
const fallbackId = accounts[0]?.id != null ? Number(accounts[0].id) : null;
|
| 561 |
+
setSequenceSteps((prev) =>
|
| 562 |
+
prev.map((s) => {
|
| 563 |
+
if (s.type !== 'action' || s.channel !== 'gmail') return s;
|
| 564 |
+
const ref =
|
| 565 |
+
s.unipile_account_ref_id ??
|
| 566 |
+
(mailboxDefaults.default_mailbox_unipile_account_ref_id != null
|
| 567 |
+
? Number(mailboxDefaults.default_mailbox_unipile_account_ref_id)
|
| 568 |
+
: null) ??
|
| 569 |
+
fallbackId;
|
| 570 |
+
return { ...s, unipile_account_ref_id: ref };
|
| 571 |
+
})
|
| 572 |
+
);
|
| 573 |
+
}, [mailboxDefaults]);
|
| 574 |
+
|
| 575 |
useEffect(() => {
|
| 576 |
if (step !== 3 || selectedProducts.length === 0) return;
|
| 577 |
+
const appendix = buildCampaignSequenceAppendix(sequenceSteps, mailboxDefaults);
|
| 578 |
setPrompts((prev) => {
|
| 579 |
const next = { ...prev };
|
| 580 |
let changed = false;
|
|
|
|
| 606 |
});
|
| 607 |
return changed ? next : prev;
|
| 608 |
});
|
| 609 |
+
}, [step, sequenceSteps, selectedProducts, sequenceHasLinkedin, mailboxDefaults]);
|
| 610 |
|
| 611 |
useEffect(() => {
|
| 612 |
if (!genRunning || !wizardUpload?.fileId) return;
|