Seth commited on
Commit
e72db37
Β·
1 Parent(s): b187543
backend/app/gpt_service.py CHANGED
@@ -21,6 +21,8 @@ def generate_email_sequence(
21
  prompt_template: str,
22
  product_name: str,
23
  campaign_sequence: Optional[Dict[str, Any]] = None,
 
 
24
  ) -> List[Dict]:
25
  """
26
  Generate a personalized email sequence for a contact using GPT.
@@ -46,6 +48,7 @@ def generate_email_sequence(
46
  industry = safe_str(contact.get("industry") or contact.get("Industry") or "")
47
  location = safe_str(contact.get("location") or contact.get("Location") or "")
48
  company_size = safe_str(contact.get("company_size") or contact.get("Company Size") or "")
 
49
 
50
  # Validate required fields
51
  if not email:
@@ -125,17 +128,18 @@ Body: Hi {first_name},
125
 
126
  [email body content]
127
 
128
- Anna
129
  Email 2
130
- Subject: [subject line]
131
  Body: Hi {first_name},
132
 
133
- [email body content]
134
 
135
- Anna
136
  ... (continue for all {num_emails} emails, each starting with "Hi {first_name},")
137
 
138
- Remember: Use only the information provided above. Do not reference anything not explicitly mentioned."""
 
139
  )
140
  else:
141
  # Use standard prompt for other products
@@ -154,8 +158,8 @@ Personalize this email template:
154
  {prompt_template}
155
 
156
  Replace all variables like {{first_name}}, {{company}}, {{sender_name}} with the actual information.
157
- Make it sound natural and personalized. Keep the same structure and format.
158
- If sender_name is not provided, use "Alex Thompson" as the sender name."""
159
  if campaign_sequence and int(campaign_sequence.get("email_count") or 0) > 1:
160
  n = int(campaign_sequence["email_count"])
161
  titles = campaign_sequence.get("email_titles") or []
@@ -309,13 +313,14 @@ Touches in order:
309
  })
310
 
311
  # Replace template variables in all emails
 
312
  for email_data in emails:
313
  email_data["body"] = email_data["body"].replace("{{first_name}}", first_name)
314
  email_data["body"] = email_data["body"].replace("{{company}}", company)
315
- email_data["body"] = email_data["body"].replace("{{sender_name}}", "Alex Thompson")
316
  email_data["subject"] = email_data["subject"].replace("{{first_name}}", first_name)
317
  email_data["subject"] = email_data["subject"].replace("{{company}}", company)
318
- email_data["subject"] = email_data["subject"].replace("{{sender_name}}", "Alex Thompson")
319
 
320
  # Ensure first name is in the greeting if it's missing
321
  # Fix cases where GPT generates "Hi," instead of "Hi {first_name},"
@@ -349,7 +354,9 @@ Touches in order:
349
  # Return a fallback email
350
  first_name = contact.get("first_name", contact.get("First Name", "there"))
351
  company = contact.get("company", contact.get("Company", contact.get("Organization", "your company")))
352
-
 
 
353
  return [{
354
  "first_name": first_name,
355
  "last_name": contact.get("last_name", contact.get("Last Name", "")),
@@ -359,7 +366,7 @@ Touches in order:
359
  "product": product_name,
360
  "email_number": 1,
361
  "subject": f"{first_name}, let's talk about {product_name}",
362
- "email_content": f"Hi {first_name},\n\nI wanted to reach out about how {product_name} could benefit {company}.\n\nBest,\nAlex Thompson"
363
  }]
364
 
365
 
@@ -368,6 +375,8 @@ def generate_linkedin_sequence(
368
  prompt_template: str,
369
  product_name: str,
370
  campaign_sequence: Optional[Dict[str, Any]] = None,
 
 
371
  ) -> List[Dict]:
372
  """
373
  Generate LinkedIn DM-style messages (connection note + follow-ups) for a contact.
@@ -395,6 +404,8 @@ def generate_linkedin_sequence(
395
  if not email:
396
  raise ValueError(f"Contact missing email address: {contact}")
397
 
 
 
398
  system_prompt = prompt_template
399
  li_actions: List[str] = []
400
  if campaign_sequence and isinstance(campaign_sequence.get("linkedin_actions"), list):
@@ -446,6 +457,8 @@ Contact Information:
446
  - Industry: {industry if industry else 'Not specified'}
447
  - Location: {location if location else 'Not specified'}
448
 
 
 
449
  Generate exactly {num_messages} messages (no more, no fewer) following the rules in the system prompt.
450
  {plan_extra}
451
  CRITICAL: Write for LinkedIn DMs or connection notes β€” short, plain, professional. No email-style subjects.
@@ -478,7 +491,7 @@ Use the contact's real first name where a greeting is appropriate. Do not use pl
478
  body = (
479
  body.replace("{{first_name}}", first_name)
480
  .replace("{{company}}", company)
481
- .replace("{{sender_name}}", "Anna")
482
  )
483
  messages.append(
484
  {
@@ -507,7 +520,7 @@ Use the contact's real first name where a greeting is appropriate. Do not use pl
507
  chunk = (
508
  chunk.replace("{{first_name}}", first_name)
509
  .replace("{{company}}", company)
510
- .replace("{{sender_name}}", "Anna")
511
  )
512
  messages.append({"email_number": 1, "subject": "", "body": chunk})
513
 
@@ -538,6 +551,8 @@ Use the contact's real first name where a greeting is appropriate. Do not use pl
538
  contact.get("company") or contact.get("Company") or contact.get("Organization") or "your company"
539
  )
540
  email = safe_str(contact.get("email") or contact.get("Email") or "")
 
 
541
  return [
542
  {
543
  "first_name": fn,
@@ -548,7 +563,7 @@ Use the contact's real first name where a greeting is appropriate. Do not use pl
548
  "product": product_name,
549
  "email_number": 1,
550
  "subject": "",
551
- "email_content": f"Hi {fn},\n\nQuick note on {product_name} for {company} β€” happy to compare notes if useful.\n\nAnna",
552
  }
553
  ]
554
 
 
21
  prompt_template: str,
22
  product_name: str,
23
  campaign_sequence: Optional[Dict[str, Any]] = None,
24
+ *,
25
+ sender_mailbox_name: Optional[str] = None,
26
  ) -> List[Dict]:
27
  """
28
  Generate a personalized email sequence for a contact using GPT.
 
48
  industry = safe_str(contact.get("industry") or contact.get("Industry") or "")
49
  location = safe_str(contact.get("location") or contact.get("Location") or "")
50
  company_size = safe_str(contact.get("company_size") or contact.get("Company Size") or "")
51
+ signoff = (sender_mailbox_name or "").strip()
52
 
53
  # Validate required fields
54
  if not email:
 
128
 
129
  [email body content]
130
 
131
+ {{{{sender_name}}}}
132
  Email 2
133
+ Subject: [subject line β€” must be Re: followed by the EXACT same base subject text as Email 1, with no new topic]
134
  Body: Hi {first_name},
135
 
136
+ [email body content β€” write ONLY the new paragraphs for this touch; do not paste or quote prior emails here]
137
 
138
+ {{{{sender_name}}}}
139
  ... (continue for all {num_emails} emails, each starting with "Hi {first_name},")
140
 
141
+ Remember: Use only the information provided above. Do not reference anything not explicitly mentioned.
142
+ For Email 2 onward, the subject line must stay on-thread: use "Re: <Email 1 subject without any leading Re:>"."""
143
  )
144
  else:
145
  # Use standard prompt for other products
 
158
  {prompt_template}
159
 
160
  Replace all variables like {{first_name}}, {{company}}, {{sender_name}} with the actual information.
161
+ {f'For {{sender_name}}, use exactly this sender sign-off name (not the prospect): {signoff}' if signoff else 'If {{sender_name}} appears and no sender name was provided for this run, leave the literal token {{sender_name}} in the output.'}
162
+ Make it sound natural and personalized. Keep the same structure and format."""
163
  if campaign_sequence and int(campaign_sequence.get("email_count") or 0) > 1:
164
  n = int(campaign_sequence["email_count"])
165
  titles = campaign_sequence.get("email_titles") or []
 
313
  })
314
 
315
  # Replace template variables in all emails
316
+ repl_sender = signoff if signoff else ""
317
  for email_data in emails:
318
  email_data["body"] = email_data["body"].replace("{{first_name}}", first_name)
319
  email_data["body"] = email_data["body"].replace("{{company}}", company)
320
+ email_data["body"] = email_data["body"].replace("{{sender_name}}", repl_sender)
321
  email_data["subject"] = email_data["subject"].replace("{{first_name}}", first_name)
322
  email_data["subject"] = email_data["subject"].replace("{{company}}", company)
323
+ email_data["subject"] = email_data["subject"].replace("{{sender_name}}", repl_sender)
324
 
325
  # Ensure first name is in the greeting if it's missing
326
  # Fix cases where GPT generates "Hi," instead of "Hi {first_name},"
 
354
  # Return a fallback email
355
  first_name = contact.get("first_name", contact.get("First Name", "there"))
356
  company = contact.get("company", contact.get("Company", contact.get("Organization", "your company")))
357
+ fb = (sender_mailbox_name or "").strip()
358
+ tail = f"\n\nBest,\n{fb}" if fb else "\n\nBest,"
359
+
360
  return [{
361
  "first_name": first_name,
362
  "last_name": contact.get("last_name", contact.get("Last Name", "")),
 
366
  "product": product_name,
367
  "email_number": 1,
368
  "subject": f"{first_name}, let's talk about {product_name}",
369
+ "email_content": f"Hi {first_name},\n\nI wanted to reach out about how {product_name} could benefit {company}.{tail}",
370
  }]
371
 
372
 
 
375
  prompt_template: str,
376
  product_name: str,
377
  campaign_sequence: Optional[Dict[str, Any]] = None,
378
+ *,
379
+ sender_linkedin_name: Optional[str] = None,
380
  ) -> List[Dict]:
381
  """
382
  Generate LinkedIn DM-style messages (connection note + follow-ups) for a contact.
 
404
  if not email:
405
  raise ValueError(f"Contact missing email address: {contact}")
406
 
407
+ li_sign = (sender_linkedin_name or "").strip()
408
+
409
  system_prompt = prompt_template
410
  li_actions: List[str] = []
411
  if campaign_sequence and isinstance(campaign_sequence.get("linkedin_actions"), list):
 
457
  - Industry: {industry if industry else 'Not specified'}
458
  - Location: {location if location else 'Not specified'}
459
 
460
+ {f'Sender first name / sign-off to use when addressing yourself (not the prospect): {li_sign}' if li_sign else 'If you sign with a name, output the literal token {{sender_name}} on its own line; the app substitutes the LinkedIn profile name.'}
461
+
462
  Generate exactly {num_messages} messages (no more, no fewer) following the rules in the system prompt.
463
  {plan_extra}
464
  CRITICAL: Write for LinkedIn DMs or connection notes β€” short, plain, professional. No email-style subjects.
 
491
  body = (
492
  body.replace("{{first_name}}", first_name)
493
  .replace("{{company}}", company)
494
+ .replace("{{sender_name}}", li_sign if li_sign else "")
495
  )
496
  messages.append(
497
  {
 
520
  chunk = (
521
  chunk.replace("{{first_name}}", first_name)
522
  .replace("{{company}}", company)
523
+ .replace("{{sender_name}}", li_sign if li_sign else "")
524
  )
525
  messages.append({"email_number": 1, "subject": "", "body": chunk})
526
 
 
551
  contact.get("company") or contact.get("Company") or contact.get("Organization") or "your company"
552
  )
553
  email = safe_str(contact.get("email") or contact.get("Email") or "")
554
+ li_fb = (sender_linkedin_name or "").strip()
555
+ li_tail = f"\n\n{li_fb}" if li_fb else ""
556
  return [
557
  {
558
  "first_name": fn,
 
563
  "product": product_name,
564
  "email_number": 1,
565
  "subject": "",
566
+ "email_content": f"Hi {fn},\n\nQuick note on {product_name} for {company} β€” happy to compare notes if useful.{li_tail}",
567
  }
568
  ]
569
 
backend/app/main.py CHANGED
@@ -2234,11 +2234,18 @@ async def generate_linkedin_campaign_sequences(
2234
  GeneratedSequence.channel == "linkedin",
2235
  ).delete()
2236
 
 
 
 
 
 
2237
  sequence_id = 1
2238
  generated_rows = 0
2239
  for _, row in df.iterrows():
2240
  contact = row.to_dict()
2241
- li_list = generate_linkedin_sequence(contact, body.prompt_template, campaign.name)
 
 
2242
  for seq_data in li_list:
2243
  db.add(
2244
  GeneratedSequence(
@@ -3916,6 +3923,19 @@ async def generate_sequences(
3916
  ).delete()
3917
  db.commit()
3918
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3919
  total_contacts = len(df)
3920
 
3921
  def progress_pct(seq_id: int, *, linkedin_phase: bool) -> float:
@@ -4024,7 +4044,11 @@ async def generate_sequences(
4024
  loop = asyncio.get_event_loop()
4025
  with concurrent.futures.ThreadPoolExecutor() as executor:
4026
  if plan_pack and email_kw:
4027
- gen_fn = partial(generate_email_sequence, campaign_sequence=email_kw)
 
 
 
 
4028
  sequence_data_list = await loop.run_in_executor(
4029
  executor,
4030
  gen_fn,
@@ -4035,7 +4059,7 @@ async def generate_sequences(
4035
  else:
4036
  sequence_data_list = await loop.run_in_executor(
4037
  executor,
4038
- generate_email_sequence,
4039
  contact,
4040
  prompt_template,
4041
  product_name,
@@ -4156,7 +4180,11 @@ async def generate_sequences(
4156
  loop = asyncio.get_event_loop()
4157
  with concurrent.futures.ThreadPoolExecutor() as executor:
4158
  if plan_pack and li_kw:
4159
- gen_li = partial(generate_linkedin_sequence, campaign_sequence=li_kw)
 
 
 
 
4160
  li_list = await loop.run_in_executor(
4161
  executor,
4162
  gen_li,
@@ -4167,7 +4195,7 @@ async def generate_sequences(
4167
  else:
4168
  li_list = await loop.run_in_executor(
4169
  executor,
4170
- generate_linkedin_sequence,
4171
  contact,
4172
  li_template,
4173
  li_product,
 
2234
  GeneratedSequence.channel == "linkedin",
2235
  ).delete()
2236
 
2237
+ urow = db.query(User).filter(User.id == t.user_id).first()
2238
+ li_sig = ""
2239
+ if urow:
2240
+ li_sig = (getattr(urow, "linkedin_profile_display_name", None) or "").strip() or (urow.name or "").strip()
2241
+
2242
  sequence_id = 1
2243
  generated_rows = 0
2244
  for _, row in df.iterrows():
2245
  contact = row.to_dict()
2246
+ li_list = generate_linkedin_sequence(
2247
+ contact, body.prompt_template, campaign.name, sender_linkedin_name=li_sig or None
2248
+ )
2249
  for seq_data in li_list:
2250
  db.add(
2251
  GeneratedSequence(
 
3923
  ).delete()
3924
  db.commit()
3925
 
3926
+ user_row = db.query(User).filter(User.id == t.user_id).first()
3927
+ mailbox_sig = ""
3928
+ linkedin_sig = ""
3929
+ if user_row:
3930
+ mailbox_sig = (
3931
+ (getattr(user_row, "mailbox_profile_display_name", None) or "").strip()
3932
+ or (user_row.name or "").strip()
3933
+ )
3934
+ linkedin_sig = (
3935
+ (getattr(user_row, "linkedin_profile_display_name", None) or "").strip()
3936
+ or (user_row.name or "").strip()
3937
+ )
3938
+
3939
  total_contacts = len(df)
3940
 
3941
  def progress_pct(seq_id: int, *, linkedin_phase: bool) -> float:
 
4044
  loop = asyncio.get_event_loop()
4045
  with concurrent.futures.ThreadPoolExecutor() as executor:
4046
  if plan_pack and email_kw:
4047
+ gen_fn = partial(
4048
+ generate_email_sequence,
4049
+ campaign_sequence=email_kw,
4050
+ sender_mailbox_name=mailbox_sig or None,
4051
+ )
4052
  sequence_data_list = await loop.run_in_executor(
4053
  executor,
4054
  gen_fn,
 
4059
  else:
4060
  sequence_data_list = await loop.run_in_executor(
4061
  executor,
4062
+ partial(generate_email_sequence, sender_mailbox_name=mailbox_sig or None),
4063
  contact,
4064
  prompt_template,
4065
  product_name,
 
4180
  loop = asyncio.get_event_loop()
4181
  with concurrent.futures.ThreadPoolExecutor() as executor:
4182
  if plan_pack and li_kw:
4183
+ gen_li = partial(
4184
+ generate_linkedin_sequence,
4185
+ campaign_sequence=li_kw,
4186
+ sender_linkedin_name=linkedin_sig or None,
4187
+ )
4188
  li_list = await loop.run_in_executor(
4189
  executor,
4190
  gen_li,
 
4195
  else:
4196
  li_list = await loop.run_in_executor(
4197
  executor,
4198
+ partial(generate_linkedin_sequence, sender_linkedin_name=linkedin_sig or None),
4199
  contact,
4200
  li_template,
4201
  li_product,
backend/app/outreach_routes.py CHANGED
@@ -635,19 +635,113 @@ def _find_next_send(
635
  def _fetch_generated(
636
  db: Session, tenant_id: int, file_id: str, row_index: int, channel: str, step_order: int
637
  ) -> Optional[GeneratedSequence]:
638
- return (
639
  db.query(GeneratedSequence)
640
  .filter(
641
  GeneratedSequence.tenant_id == tenant_id,
642
  GeneratedSequence.file_id == file_id,
643
  GeneratedSequence.sequence_id == row_index,
644
- GeneratedSequence.channel == channel,
645
  GeneratedSequence.step_order == step_order,
646
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  .first()
648
  )
649
 
650
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  def _execute_one_send(
652
  db: Session,
653
  tc: TenantContext,
@@ -701,12 +795,13 @@ def _execute_one_send(
701
  display = " ".join(
702
  p for p in [_safe_str(gen.first_name or contact.first_name), _safe_str(gen.last_name or contact.last_name)] if p
703
  ).strip()
 
704
  send_ids = _send_unipile_email(
705
  acc.unipile_account_id,
706
  to_email,
707
  display or to_email,
708
- gen.subject or "",
709
- gen.email_content or "",
710
  label,
711
  )
712
  rec.unipile_message_id = send_ids.get("message_id") or send_ids.get("tracking_id")
 
635
  def _fetch_generated(
636
  db: Session, tenant_id: int, file_id: str, row_index: int, channel: str, step_order: int
637
  ) -> Optional[GeneratedSequence]:
638
+ q = (
639
  db.query(GeneratedSequence)
640
  .filter(
641
  GeneratedSequence.tenant_id == tenant_id,
642
  GeneratedSequence.file_id == file_id,
643
  GeneratedSequence.sequence_id == row_index,
 
644
  GeneratedSequence.step_order == step_order,
645
  )
646
+ )
647
+ if channel == "gmail":
648
+ q = q.filter(or_(GeneratedSequence.channel == "gmail", GeneratedSequence.channel == "email"))
649
+ else:
650
+ q = q.filter(GeneratedSequence.channel == channel)
651
+ return q.first()
652
+
653
+
654
+ def _strip_re_subject(subject: str) -> str:
655
+ s = _safe_str(subject)
656
+ while True:
657
+ t = s.lstrip()
658
+ tl = t.lower()
659
+ if tl.startswith("re:"):
660
+ s = t[3:].lstrip()
661
+ elif tl.startswith("fwd:"):
662
+ s = t[4:].lstrip()
663
+ else:
664
+ break
665
+ return s.strip()
666
+
667
+
668
+ def _gmail_channel_filter():
669
+ return or_(GeneratedSequence.channel == "gmail", GeneratedSequence.channel == "email")
670
+
671
+
672
+ def _first_gmail_root_subject(db: Session, tenant_id: int, file_id: str, seq_id: int) -> str:
673
+ row = (
674
+ db.query(GeneratedSequence)
675
+ .filter(
676
+ GeneratedSequence.tenant_id == tenant_id,
677
+ GeneratedSequence.file_id == file_id,
678
+ GeneratedSequence.sequence_id == seq_id,
679
+ _gmail_channel_filter(),
680
+ )
681
+ .order_by(func.coalesce(GeneratedSequence.step_order, 999999).asc(), GeneratedSequence.email_number.asc())
682
+ .first()
683
+ )
684
+ if not row or not row.subject:
685
+ return ""
686
+ return _strip_re_subject(row.subject)
687
+
688
+
689
+ def _prior_gmail_for_thread(
690
+ db: Session, tenant_id: int, file_id: str, seq_id: int, gen: GeneratedSequence, act_order: int
691
+ ) -> Optional[GeneratedSequence]:
692
+ cur_so = getattr(gen, "step_order", None)
693
+ if cur_so is None:
694
+ cur_so = int(act_order)
695
+ prior = (
696
+ db.query(GeneratedSequence)
697
+ .filter(
698
+ GeneratedSequence.tenant_id == tenant_id,
699
+ GeneratedSequence.file_id == file_id,
700
+ GeneratedSequence.sequence_id == seq_id,
701
+ _gmail_channel_filter(),
702
+ GeneratedSequence.step_order < int(cur_so),
703
+ )
704
+ .order_by(GeneratedSequence.step_order.desc())
705
+ .first()
706
+ )
707
+ if prior:
708
+ return prior
709
+ return (
710
+ db.query(GeneratedSequence)
711
+ .filter(
712
+ GeneratedSequence.tenant_id == tenant_id,
713
+ GeneratedSequence.file_id == file_id,
714
+ GeneratedSequence.sequence_id == seq_id,
715
+ _gmail_channel_filter(),
716
+ GeneratedSequence.email_number < int(gen.email_number or 0),
717
+ )
718
+ .order_by(GeneratedSequence.email_number.desc())
719
  .first()
720
  )
721
 
722
 
723
+ def _compose_threaded_gmail_send(
724
+ db: Session, gen: GeneratedSequence, contact: Contact, act_order: int
725
+ ) -> Tuple[str, str]:
726
+ """Same-thread follow-ups: Re: <root subject>; append quoted prior body (send layer)."""
727
+ seq_id = int(contact.row_index or 0)
728
+ fresh_body = (gen.email_content or "").strip()
729
+ root = _first_gmail_root_subject(db, int(gen.tenant_id or 0), gen.file_id, seq_id)
730
+ prior = _prior_gmail_for_thread(db, int(gen.tenant_id or 0), gen.file_id, seq_id, gen, act_order)
731
+ if not prior:
732
+ subj = gen.subject or root or "(no subject)"
733
+ return (_strip_re_subject(subj) if subj else "(no subject)", fresh_body)
734
+ base = root or _strip_re_subject(prior.subject or gen.subject or "")
735
+ if not base:
736
+ base = _strip_re_subject(gen.subject or "(no subject)")
737
+ out_subj = f"Re: {base}"
738
+ prev_body = (prior.email_content or "").strip()
739
+ quoted = "\n".join(f"> {ln}" for ln in prev_body.splitlines())
740
+ sep = "\n\n---------- Previous message ----------\n"
741
+ body = f"{fresh_body}{sep}{quoted}\n"
742
+ return (out_subj, body)
743
+
744
+
745
  def _execute_one_send(
746
  db: Session,
747
  tc: TenantContext,
 
795
  display = " ".join(
796
  p for p in [_safe_str(gen.first_name or contact.first_name), _safe_str(gen.last_name or contact.last_name)] if p
797
  ).strip()
798
+ out_subj, out_body = _compose_threaded_gmail_send(db, gen, contact, order)
799
  send_ids = _send_unipile_email(
800
  acc.unipile_account_id,
801
  to_email,
802
  display or to_email,
803
+ out_subj,
804
+ out_body,
805
  label,
806
  )
807
  rec.unipile_message_id = send_ids.get("message_id") or send_ids.get("tracking_id")
frontend/src/components/campaigns/CreateCampaignWizard.jsx CHANGED
@@ -615,7 +615,7 @@ export default function CreateCampaignWizard({
615
  if (!cur) {
616
  cur =
617
  LINKEDIN_DEFAULT_TEMPLATES[p.name] ||
618
- `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${p.name}. Sender: Anna.\nMatch the Message count in the generation request exactly.`;
619
  }
620
  if (!cur || cur.includes(WIZARD_SEQUENCE_TAG)) return;
621
  next[p.name] = `${cur}\n\n${appendix}`;
 
615
  if (!cur) {
616
  cur =
617
  LINKEDIN_DEFAULT_TEMPLATES[p.name] ||
618
+ `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${p.name}. Sign with {{sender_name}} when signing off.\nMatch the Message count in the generation request exactly.`;
619
  }
620
  if (!cur || cur.includes(WIZARD_SEQUENCE_TAG)) return;
621
  next[p.name] = `${cur}\n\n${appendix}`;
frontend/src/components/prompts/PromptEditor.jsx CHANGED
@@ -22,172 +22,78 @@ function splitCampaignRaw(raw, includeLinkedinSection) {
22
  export const DEFAULT_TEMPLATES = {
23
  'Accounts Payable Automation': `πŸ”’ SYSTEM PROMPT (DO NOT MODIFY)
24
 
25
- You are an expert B2B outbound copywriter writing cold emails that feel like internal work conversations, not marketing.
26
- Your audience is Accounts Payable professionals (Accounts Payable Managers, Finance Managers, Controllers) at North American mid-market manufacturing and industrial companies.
27
- Your goal is to generate reply-worthy AP email sequences that feel:
28
- β€’ Familiar
29
- β€’ Simple
30
- β€’ Relevant
31
- β€’ Calm
32
- β€’ Non-salesy
33
-
34
- The objective is interest and response, not persuasion.
35
 
36
- πŸ”’ SENDER IDENTITY (CRITICAL – FIXES SIGNATURE BUG)
37
 
38
- The sender of all emails is a fixed identity.
39
 
40
- Sender first name: Anna
 
41
 
42
  Rules:
43
- β€’ The sender name must ALWAYS be exactly: Anna
44
- β€’ NEVER use the contact's first name, last name, or any contact field as the sender
45
- β€’ NEVER infer the sender from the contact
46
- β€’ If there is ambiguity, default to Anna
47
-
48
- πŸ”’ NON-NEGOTIABLE RULES
49
-
50
- 1. No fake personalization
51
- β€’ Never reference LinkedIn activity, posts, likes, hiring, growth, or news
52
- β€’ Never say "I noticed", "based on what I saw", "research", etc.
53
- β€’ Use ONLY information explicitly provided in the input
54
-
55
- 2. AP-native language only
56
- β€’ Avoid words like: workflow, automation (except when naming EZOFIS once), optimization, transformation, AI hype
57
- β€’ Use words like: waiting, on hold, approvals, receiving, matching, backup, follow-ups, status
58
-
59
- 3. Tone
60
- β—‹ Calm
61
- β—‹ Professional
62
- β—‹ Plain language
63
- β—‹ Not alarming
64
- β—‹ Not consultative
65
- β—‹ Not sales-driven
66
-
67
- 4. Structure
68
- β—‹ Short paragraphs
69
- β—‹ No emojis
70
- β—‹ No em dashes
71
- β—‹ No hype words
72
- β—‹ No marketing phrases
73
- β—‹ No long explanations
74
 
75
- 5. Questions
76
- β—‹ Each email must ask exactly ONE question
77
- β—‹ Questions must be easy to answer with "yes", "sometimes", or "no"
78
-
79
- 6. Personalization rules
80
-
81
- You MAY infer operational reality from:
82
- β€’ Role
83
- β€’ Industry
84
- β€’ Company type (manufacturing, service, project-based)
85
- β€’ Scale indicators (employee count, global vs local)
86
-
87
- You MUST NOT:
88
- β€’ Name ERP systems
89
- β€’ Name AP tools
90
- β€’ Name tech stack
91
- β€’ Invent internal processes
92
-
93
- πŸ”’ INPUT FORMAT (BATCH)
94
 
95
- You will receive a list of contacts with structured fields such as:
96
- β€’ First Name
97
- β€’ Last Name
98
- β€’ Role
99
- β€’ Company
100
- β€’ Industry
101
- β€’ Keywords
102
- β€’ Location
103
- β€’ Employee count
104
 
105
- πŸ”’ TASK
106
 
107
- For each contact, generate a 4-email outbound sequence focused only on Accounts Payable.
108
- Each email should feel like a natural continuation of the previous one.
 
 
 
 
 
 
109
 
110
- πŸ”’ EMAIL SEQUENCE LOGIC (MANDATORY)
111
-
112
- πŸ“§ EMAIL 1 – Recognition
113
-
114
- Purpose: Create immediate familiarity.
115
-
116
- Rules:
117
- β€’ Describe a real AP situation relevant to the contact's environment
118
- β€’ No solutions yet
119
- β€’ Ask ONE recognition question
120
-
121
- Length: 4-6 lines
122
- End with a question
123
-
124
- πŸ“§ EMAIL 2 – Checklist Offer
125
-
126
- Purpose: Offer practical value without selling.
127
-
128
- Rules:
129
- β€’ Mention a short, simple AP checklist
130
- β€’ Do NOT attach the checklist
131
- β€’ Do NOT oversell it
132
- β€’ Ask permission to send it
133
-
134
- Allowed phrasing: "short checklist", "simple checklist", "AP checklist"
135
 
136
- Length: 4-6 lines
137
- End with a question
138
 
139
- πŸ“§ EMAIL 3 – Soft Product Introduction (EZOFIS)
140
 
141
- Purpose: Introduce EZOFIS without pressure.
142
 
143
- Rules:
144
- β€’ Mention EZOFIS AP Automation by name
145
- β€’ Describe it in ONE plain sentence
146
- β€’ No feature lists
147
- β€’ No meeting requests
148
- β€’ Frame it as context, not a pitch
149
 
150
- Example style: "Some teams use EZOFIS AP Automation to keep invoice context and approvals together so fewer items get stuck."
151
 
152
- Length: 3-5 lines
153
- End with a question
154
 
155
- πŸ“§ EMAIL 4 – Demo Video Ask
156
 
157
- Purpose: Offer a low-friction next step.
158
 
159
- Rules:
160
- β€’ Ask permission to send a short demo video
161
- β€’ Do NOT ask for a meeting
162
- β€’ Do NOT push urgency
163
- β€’ Keep tone optional and professional
164
 
165
- Length: 3-4 lines
166
- End with a question
167
 
168
- πŸ”’ SUBJECT LINE RULES
 
 
169
 
170
- β€’ Must look like an internal or peer email
171
- β€’ Short and plain
172
- β€’ No marketing language
173
 
174
- Examples:
175
- β€’ Invoice on hold
176
- β€’ Waiting on backup
177
- β€’ Approval follow-ups
178
- β€’ Receiving confirmation
179
- β€’ AP delays
180
 
181
- πŸ”’ SIGNATURE RULE (STRICT)
182
-
183
- End every email with exactly:
184
 
185
- Anna
186
-
187
- Rules:
188
- β€’ Do NOT vary the sender name
189
- β€’ Do NOT substitute the contact's name
190
- β€’ Do NOT add titles or company names
191
 
192
  πŸ”’ OUTPUT FORMAT (STRICT)
193
 
@@ -196,47 +102,44 @@ For each contact, output exactly:
196
  Contact: <First Name> <Last Name> – <Company>
197
 
198
  Email 1
199
- Subject: <subject line>
200
  Body: Hi <First Name>,
201
 
202
- <email body content>
203
 
204
- Anna
205
 
206
  Email 2
207
- Subject: <subject line>
208
  Body: Hi <First Name>,
209
 
210
- <email body content>
211
 
212
- Anna
213
 
214
  Email 3
215
- Subject: <subject line>
216
  Body: Hi <First Name>,
217
 
218
- <email body content>
219
 
220
- Anna
221
 
222
  Email 4
223
- Subject: <subject line>
224
  Body: Hi <First Name>,
225
 
226
- <email body content>
227
-
228
- Anna
229
 
230
- CRITICAL: Every email body MUST start with "Hi <First Name>," where <First Name> is the contact's actual first name from the input. Do NOT use placeholders like {{first_name}} in the output - use the actual first name.
231
 
232
- πŸ”’ FINAL VALIDATION CHECK (MANDATORY)
233
 
234
- Before finalizing output:
235
- β€’ Verify the sender name is NOT the same as the contact name
236
- β€’ If it matches, replace it with Anna
237
- β€’ If unsure about a detail, remove it and keep the email generic
238
 
239
- When in doubt, keep it simpler.`,
 
 
240
 
241
  'Sales Order Processing': `πŸ”’ SYSTEM PROMPT – ACCOUNTS RECEIVABLE (ORDER OPERATIONS)
242
  DO NOT MODIFY
@@ -262,14 +165,25 @@ Relevant
262
  Calm
263
  Non-salesy
264
  The objective is interest and response, not persuasion.
265
- πŸ”’ SENDER IDENTITY (CRITICAL – FIXES SIGNATURE BUG)
266
- The sender of all emails is a fixed identity.
267
- Sender first name: Anna
268
- Rules:
269
- The sender name must ALWAYS be exactly: Anna
270
- NEVER use the contact's first name, last name, or any contact field as the sender
271
- NEVER infer the sender from the contact
272
- If there is ambiguity, default to Anna
 
 
 
 
 
 
 
 
 
 
 
273
  πŸ”’ NON-NEGOTIABLE RULES
274
  1. No fake personalization
275
  Never reference LinkedIn activity, posts, likes, hiring, growth, or news
@@ -353,7 +267,7 @@ Pick slips created late
353
  Delivery slips coming back incomplete
354
  No solutions yet
355
  Ask ONE recognition question
356
- Length: 4–6 lines
357
  End with a question
358
  πŸ“§ EMAIL 2 – Checklist Offer (Order Readiness)
359
  Purpose:
@@ -368,95 +282,72 @@ Allowed phrasing:
368
  "simple checklist"
369
  "order readiness checklist"
370
  "delivery checklist"
371
- Length: 4–6 lines
372
  End with a question
373
  πŸ“§ EMAIL 3 – Soft Product Introduction (EZOFIS)
374
  Purpose:
375
  Introduce EZOFIS naturally, without pressure.
376
  Rules:
377
  Mention EZOFIS AR & Order Automation by name
378
- Describe it in ONE plain sentence
379
- Focus on:
380
- Capturing POs
381
- Digitizing pick slips
382
- Capturing delivery slips in the field
383
  No feature lists
384
- No meeting requests
385
- Frame it as context, not a pitch
386
  Example style (do not copy verbatim):
387
  "Some teams use EZOFIS AR & Order Automation to capture customer POs, pick slips, and delivery confirmations in one place so orders don't stall later."
388
- Length: 3–5 lines
389
  End with a question
390
- πŸ“§ EMAIL 4 – Demo Video Ask
391
  Purpose:
392
- Offer a low-friction next step.
393
  Rules:
394
- Ask permission to send a short demo video
395
- Do NOT ask for a meeting
396
  Do NOT push urgency
397
  Keep tone optional and professional
398
- Length: 3–4 lines
399
  End with a question
400
- πŸ”’ SUBJECT LINE RULES
401
- Must look like an internal or peer-to-peer work email
402
- Short and plain
403
- No marketing language
404
- Examples:
405
- Missing customer PO
406
- Order waiting
407
- Pick slip issue
408
- Delivery confirmation
409
- Order paperwork
410
- πŸ”’ SIGNATURE RULE (STRICT)
411
- End every email with exactly:
412
- Anna
413
- Rules:
414
- Do NOT vary the sender name
415
- Do NOT substitute the contact's name
416
- Do NOT add titles or company names
417
  πŸ”’ OUTPUT FORMAT (STRICT)
418
  For each contact, output exactly:
419
  Contact: <First Name> <Last Name> – <Company>
420
 
421
  Email 1
422
- Subject: <subject line>
423
  Body:
424
  Hi <First Name>,
425
 
426
  <email body content>
427
 
428
- Anna
429
 
430
  Email 2
431
- Subject: <subject line>
432
  Body:
433
  Hi <First Name>,
434
 
435
  <email body content>
436
 
437
- Anna
438
 
439
  Email 3
440
- Subject: <subject line>
441
  Body:
442
  Hi <First Name>,
443
 
444
  <email body content>
445
 
446
- Anna
447
 
448
  Email 4
449
- Subject: <subject line>
450
  Body:
451
  Hi <First Name>,
452
 
453
  <email body content>
454
 
455
- Anna
456
  πŸ”’ FINAL VALIDATION CHECK (MANDATORY)
457
  Before finalizing output:
458
- Verify the sender name is NOT the same as the contact name
459
- If it matches, replace it with Anna
460
  If unsure about a detail, remove it and keep the email generic
461
  When in doubt, keep it simpler.`,
462
 
@@ -478,16 +369,12 @@ Calm
478
  Non-salesy
479
  The objective is interest and response, not persuasion.
480
 
481
- πŸ”’ SENDER IDENTITY (CRITICAL – FIXES SIGNATURE BUG)
482
 
483
- The sender of all emails is a fixed identity.
484
- Sender first name: Jenny
485
 
486
- Rules:
487
- The sender name must ALWAYS be exactly: Jenny
488
- NEVER use the contact's first name, last name, or any contact field as the sender
489
- NEVER infer the sender from the contact
490
- If there is ambiguity, default to Jenny
491
 
492
  πŸ”’ NON-NEGOTIABLE RULES
493
 
@@ -654,27 +541,30 @@ Optional tone
654
  Length: 3–4 lines
655
  End with a question
656
 
 
 
 
 
 
 
657
  πŸ”’ SUBJECT LINE RULES
658
 
659
- Must look like an internal or peer email
660
- Short
661
- Plain
662
- No marketing tone
663
- Examples:
664
  Can't find the file
665
  Latest version?
666
- Document follow-up
667
- Missing attachment
668
- Audit prep
669
 
670
  πŸ”’ SIGNATURE RULE (STRICT)
671
 
672
  End every email with exactly:
673
 
674
- Jenny
675
 
676
  Rules:
677
- Do NOT vary the sender name
678
  Do NOT substitute the contact's name
679
  Do NOT add titles or company names
680
 
@@ -685,57 +575,56 @@ For each contact, output exactly:
685
  Contact: <First Name> <Last Name> – <Company>
686
 
687
  Email 1
688
- Subject:
689
  Body:
690
  Hi <First Name>,
691
 
692
  <email body content>
693
 
694
- Jenny
695
 
696
  Email 2
697
- Subject:
698
  Body:
699
  Hi <First Name>,
700
 
701
  <email body content>
702
 
703
- Jenny
704
 
705
  Email 3
706
- Subject:
707
  Body:
708
  Hi <First Name>,
709
 
710
  <email body content>
711
 
712
- Jenny
713
 
714
  Email 4
715
- Subject:
716
  Body:
717
  Hi <First Name>,
718
 
719
  <email body content>
720
 
721
- Jenny
722
 
723
  Email 5
724
- Subject:
725
  Body:
726
  Hi <First Name>,
727
 
728
  <email body content>
729
 
730
- Jenny
731
 
732
  CRITICAL: Every email body MUST start with "Hi <First Name>," where <First Name> is the contact's actual first name from the input. Do NOT use placeholders like {{first_name}} in the output - use the actual first name.
733
 
734
  πŸ”’ FINAL VALIDATION CHECK (MANDATORY)
735
 
736
  Before finalizing output:
737
- Verify the sender name is NOT the same as the contact name
738
- If it matches, replace it with Jenny
739
  If unsure about a detail, remove it and keep the email generic
740
  When in doubt, keep it simpler.`,
741
 
@@ -797,30 +686,55 @@ Best,
797
  export const LINKEDIN_DEFAULT_TEMPLATES = {
798
  'Accounts Payable Automation': `πŸ”’ LINKEDIN SYSTEM PROMPT (DO NOT MODIFY)
799
 
800
- You are an expert B2B LinkedIn copywriter. Write short, human DMs and connection notes for AP / finance professionals at North American mid-market companies.
801
- Sender first name is always Anna. Never use the prospect as the sender.
802
- No fake LinkedIn "I saw your post" personalization. Use only provided fields.
803
- Tone: calm, peer-to-peer, non-salesy, no emojis, no em dashes.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
 
805
- When the user message specifies how many labeled blocks (Message 1 … Message N) to write, follow that N exactly β€” it mirrors the campaign sequence (connection note vs DM). If no count is given, default to 3 messages.
806
 
807
- Each labeled block:
808
- Message i
809
- [body text]
810
 
811
- Use variables in your reasoning: {{first_name}}, {{company}}, {{sender_name}} but output final text with real first name filled in.`,
812
 
813
  'Sales Order Processing': `πŸ”’ LINKEDIN SYSTEM PROMPT
814
- You write concise LinkedIn DMs for order-ops / AR professionals. Sender: Anna. Plain language, operational focus, one question per message. No hype.
815
- Use the exact number of labeled Message blocks requested in the user prompt (campaign sequence).`,
 
 
816
 
817
  'Document Management': `πŸ”’ LINKEDIN SYSTEM PROMPT
818
- Short LinkedIn touches for document / records leaders. Calm, practical, non-marketing. Sender Anna.
819
- Match the count and order of Message 1…N from the user prompt; do not add extra messages.`,
820
 
821
  'Invoice Processing': `πŸ”’ LINKEDIN SYSTEM PROMPT
822
- LinkedIn outreach for invoice / AP operations. Under ~120 words per touch unless it is a connection note (keep those shorter).
823
- Follow the user prompt's Message count exactly. Anna as sender.`,
824
 
825
  'Expense Management': `πŸ”’ LINKEDIN SYSTEM PROMPT
826
  LinkedIn DMs for finance teams about expense workflows. Professional, no buzzwords.
@@ -866,7 +780,7 @@ const PromptEditor = forwardRef(function PromptEditor(
866
  const emailDefault = DEFAULT_TEMPLATES[product.name];
867
  const liDefault = LINKEDIN_DEFAULT_TEMPLATES[product.name];
868
  const emailFallback = `Subject: {{first_name}}, let's talk about ${product.name}\n\nHi {{first_name}},\n\nI wanted to reach out about how ${product.name} could benefit {{company}}.\n\n[Your personalized message here]\n\nBest,\n{{sender_name}}`;
869
- const liFallback = `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${product.name}. Sender: Anna.\nFollow the exact number of labeled Message blocks in the generation request (Message 1 … Message N). Do not add extra messages.`;
870
  const email = savedEmail || emailDefault || emailFallback;
871
  if (includeLinkedinInCampaign) {
872
  const li = savedLi || liDefault || liFallback;
@@ -889,7 +803,7 @@ const PromptEditor = forwardRef(function PromptEditor(
889
  } else if (defaultTemplate) {
890
  newPrompts[product.name] = defaultTemplate;
891
  } else if (variant === 'linkedin') {
892
- newPrompts[product.name] = `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${product.name}. Sender: Anna.\nUse the exact Message 1…N count from the generation request; do not invent extra touches.\nUse {{first_name}}, {{company}}.`;
893
  } else {
894
  newPrompts[product.name] = `Subject: {{first_name}}, let's talk about ${product.name}\n\nHi {{first_name}},\n\nI wanted to reach out about how ${product.name} could benefit {{company}}.\n\n[Your personalized message here]\n\nBest,\n{{sender_name}}`;
895
  }
@@ -960,7 +874,7 @@ const PromptEditor = forwardRef(function PromptEditor(
960
  if (includeLinkedinInCampaign) {
961
  const liDefault =
962
  LINKEDIN_DEFAULT_TEMPLATES[productName] ||
963
- `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${productName}. Sender: Anna.\nMatch the Message count in the generation request exactly.`;
964
  handlePromptChange(
965
  productName,
966
  `${emailDefault}${CAMPAIGN_COMBINED_PROMPT_SPLIT}${liDefault}`
@@ -974,7 +888,7 @@ const PromptEditor = forwardRef(function PromptEditor(
974
  const defaultTemplate =
975
  library[productName] ||
976
  (variant === 'linkedin'
977
- ? `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${productName}. Sender: Anna.\nMatch the Message count in the generation request exactly.`
978
  : `Subject: {{first_name}}, let's talk about ${productName}\n\nHi {{first_name}},\n\nI wanted to reach out about how ${productName} could benefit {{company}}.\n\n[Your personalized message here]\n\nBest,\n{{sender_name}}`);
979
  handlePromptChange(productName, defaultTemplate);
980
  };
 
22
  export const DEFAULT_TEMPLATES = {
23
  'Accounts Payable Automation': `πŸ”’ SYSTEM PROMPT (DO NOT MODIFY)
24
 
25
+ You are an expert B2B outbound copywriter. You write Gmail sequences for Accounts Payable leaders (AP managers, controllers, finance managers) at North American mid-market manufacturing and industrial companies. Copy must read like a thoughtful peer, not marketing.
 
 
 
 
 
 
 
 
 
26
 
27
+ Priorities: relevance and credibility first, then curiosity, then a clear low-pressure reply path. Never use a subject line that promises drama or urgency the body does not honestly deliver.
28
 
29
+ πŸ”’ SENDER SIGN-OFF (CRITICAL)
30
 
31
+ End every email body with this exact token on its own final line (not the prospect's name):
32
+ {{sender_name}}
33
 
34
  Rules:
35
+ β€’ The app replaces {{sender_name}} with the mailbox owner's display name from Settings. Do not invent a placeholder name like "Anna".
36
+ β€’ NEVER sign with the prospect's first or last name.
37
+ β€’ NEVER use any contact field as the sender.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ πŸ”’ THREAD + SUBJECT (MANDATORY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ β€’ Email 1 defines ONE base subject (no leading "Re:"). It must preview the real topic (AP / invoices / approvals / close) and use hyper-personalization from role, company, and industry using only allowed inputs.
42
+ β€’ Emails 2-4: Subject must be exactly: Re: <copy the Email 1 subject verbatim after stripping any accidental "Re:" so there is a single "Re: " prefix>
43
+ β€’ Keep one coherent conversation β€” do not switch to unrelated subject lines.
44
+ β€’ In Email 2-4 body: write ONLY new paragraphs after the greeting. Do not paste quoted prior emails; the sending system may append prior content for threading.
 
 
 
 
 
45
 
46
+ πŸ”’ SUBJECT LINE QUALITY
47
 
48
+ β€’ Honest preview: the reader should feel the subject matches the first screen of the body.
49
+ β€’ Avoid clickbait or vague alarm ("invoice on hold", "issue", "urgent") unless the body immediately explains a routine AP friction they recognize as plausible.
50
+ β€’ Prefer specific, calm peer subjects, e.g. patterns like:
51
+ - AP throughput at <Company>
52
+ - Invoice cycle for <industry / role descriptor>
53
+ - Fewer stuck invoices before month-end
54
+ - Follow-up: AP at <Company>
55
+ (Adapt; vary wording.)
56
 
57
+ πŸ”’ NON-NEGOTIABLE RULES
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ 1. No fake personalization β€” no "I saw your post", no invented news; only input fields plus allowed inference from role, industry, company type, scale.
 
60
 
61
+ 2. AP-native language β€” avoid buzzwords; use "automation" only when naming EZOFIS AP Automation once when required. Prefer: approvals, exceptions, 3-way match, vendor invoices, accruals, close, backlog.
62
 
63
+ 3. Tone: calm, professional, plain, respectful of time.
64
 
65
+ 4. Format: short paragraphs; no emojis; no em dashes; no ALL CAPS.
 
 
 
 
 
66
 
67
+ 5. One primary question per email that is easy to answer in a few words.
68
 
69
+ 6. Do not name ERPs, AP vendors, or tech stacks unless provided.
 
70
 
71
+ πŸ”’ EMAIL SEQUENCE LOGIC
72
 
73
+ πŸ“§ EMAIL 1 β€” Context + recognition (about 5-8 short lines)
74
 
75
+ β€’ Open with a specific AP situation that fits their world (role, industry, company type).
76
+ β€’ One sentence on why you are reaching out (EZOFIS helps teams with invoice throughput) without pitching.
77
+ β€’ End with ONE recognition question.
 
 
78
 
79
+ πŸ“§ EMAIL 2 β€” Value before product (about 5-7 lines)
 
80
 
81
+ β€’ Offer a compact check or checklist idea (do not attach files).
82
+ β€’ Tie it to a concrete pain (exceptions, approval loops, month-end).
83
+ β€’ One question.
84
 
85
+ πŸ“§ EMAIL 3 β€” EZOFIS AP Automation (about 5-7 lines)
 
 
86
 
87
+ β€’ Name EZOFIS AP Automation once.
88
+ β€’ Two short sentences on operational outcome (visibility + fewer stuck invoices), no feature dump.
89
+ β€’ One sentence that sparks curiosity about how peers improved cycle time β€” no claims about their shop.
90
+ β€’ One question; optionally offer either a short call OR a demo link preference β€” not as two hard demands.
 
 
91
 
92
+ πŸ“§ EMAIL 4 β€” Respectful next step (about 4-6 lines)
 
 
93
 
94
+ β€’ One-line recap of the helpful outcome.
95
+ β€’ Offer EITHER a 15-minute working conversation OR a short demo / Loom β€” their choice.
96
+ β€’ Single closing question.
 
 
 
97
 
98
  πŸ”’ OUTPUT FORMAT (STRICT)
99
 
 
102
  Contact: <First Name> <Last Name> – <Company>
103
 
104
  Email 1
105
+ Subject: <base subject, no Re:>
106
  Body: Hi <First Name>,
107
 
108
+ <body>
109
 
110
+ {{sender_name}}
111
 
112
  Email 2
113
+ Subject: Re: <exact Email 1 subject text, no double Re:>
114
  Body: Hi <First Name>,
115
 
116
+ <body>
117
 
118
+ {{sender_name}}
119
 
120
  Email 3
121
+ Subject: Re: <same base subject as Email 1>
122
  Body: Hi <First Name>,
123
 
124
+ <body>
125
 
126
+ {{sender_name}}
127
 
128
  Email 4
129
+ Subject: Re: <same base subject as Email 1>
130
  Body: Hi <First Name>,
131
 
132
+ <body>
 
 
133
 
134
+ {{sender_name}}
135
 
136
+ CRITICAL: Use the contact's real first name in every greeting β€” not placeholder tokens.
137
 
138
+ πŸ”’ FINAL VALIDATION
 
 
 
139
 
140
+ β€’ Sign-off must be exactly the token {{sender_name}}, never the contact's name.
141
+ β€’ Emails 2-4 subjects must follow the Re: rule.
142
+ β€’ Remove any detail you cannot justify from inputs.`,
143
 
144
  'Sales Order Processing': `πŸ”’ SYSTEM PROMPT – ACCOUNTS RECEIVABLE (ORDER OPERATIONS)
145
  DO NOT MODIFY
 
165
  Calm
166
  Non-salesy
167
  The objective is interest and response, not persuasion.
168
+
169
+ πŸ”’ SENDER SIGN-OFF (CRITICAL)
170
+
171
+ End every email body with this exact token on its own final line:
172
+ {{sender_name}}
173
+
174
+ The app replaces {{sender_name}} with the mailbox owner's display name from Settings. Do not invent a name. NEVER sign with the prospect's name.
175
+
176
+ πŸ”’ THREAD + SUBJECT (MANDATORY)
177
+
178
+ β€’ Email 1: one base subject (no "Re:") that honestly previews the body; hyper-personalize with role, company, industry from inputs only.
179
+ β€’ Emails 2-4: Subject = Re: <exact Email 1 subject, strip duplicate Re:>
180
+ β€’ Email 2-4 bodies: only new paragraphs after the greeting β€” no quoted thread (the system may append prior mail).
181
+
182
+ πŸ”’ SUBJECT LINE QUALITY
183
+
184
+ β€’ No clickbait or vague alarms unless the body immediately grounds them in real order-ops friction.
185
+ β€’ Prefer calm, specific peer subjects (order docs, pick slips, delivery proof, PO gaps) tied to the reader.
186
+
187
  πŸ”’ NON-NEGOTIABLE RULES
188
  1. No fake personalization
189
  Never reference LinkedIn activity, posts, likes, hiring, growth, or news
 
267
  Delivery slips coming back incomplete
268
  No solutions yet
269
  Ask ONE recognition question
270
+ Length: about 5-8 short lines
271
  End with a question
272
  πŸ“§ EMAIL 2 – Checklist Offer (Order Readiness)
273
  Purpose:
 
282
  "simple checklist"
283
  "order readiness checklist"
284
  "delivery checklist"
285
+ Length: about 5-7 lines
286
  End with a question
287
  πŸ“§ EMAIL 3 – Soft Product Introduction (EZOFIS)
288
  Purpose:
289
  Introduce EZOFIS naturally, without pressure.
290
  Rules:
291
  Mention EZOFIS AR & Order Automation by name
292
+ Two short sentences max on outcomes: PO capture, pick slips, delivery proof in one flow
 
 
 
 
293
  No feature lists
294
+ Frame it as context, not a pitch; add one curiosity line about fewer stalled orders
295
+ Optional soft offer: short call OR demo link β€” one light question
296
  Example style (do not copy verbatim):
297
  "Some teams use EZOFIS AR & Order Automation to capture customer POs, pick slips, and delivery confirmations in one place so orders don't stall later."
298
+ Length: about 5-7 lines
299
  End with a question
300
+ πŸ“§ EMAIL 4 – Next step
301
  Purpose:
302
+ Respectful close with choice.
303
  Rules:
304
+ Offer EITHER a 15-minute walkthrough OR a short demo / Loom β€” their choice
 
305
  Do NOT push urgency
306
  Keep tone optional and professional
307
+ Length: about 4-6 lines
308
  End with a question
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  πŸ”’ OUTPUT FORMAT (STRICT)
310
  For each contact, output exactly:
311
  Contact: <First Name> <Last Name> – <Company>
312
 
313
  Email 1
314
+ Subject: <base subject, no Re:>
315
  Body:
316
  Hi <First Name>,
317
 
318
  <email body content>
319
 
320
+ {{sender_name}}
321
 
322
  Email 2
323
+ Subject: Re: <exact Email 1 subject>
324
  Body:
325
  Hi <First Name>,
326
 
327
  <email body content>
328
 
329
+ {{sender_name}}
330
 
331
  Email 3
332
+ Subject: Re: <same base subject as Email 1>
333
  Body:
334
  Hi <First Name>,
335
 
336
  <email body content>
337
 
338
+ {{sender_name}}
339
 
340
  Email 4
341
+ Subject: Re: <same base subject as Email 1>
342
  Body:
343
  Hi <First Name>,
344
 
345
  <email body content>
346
 
347
+ {{sender_name}}
348
  πŸ”’ FINAL VALIDATION CHECK (MANDATORY)
349
  Before finalizing output:
350
+ Verify the sign-off line is exactly {{sender_name}}, not the contact name
 
351
  If unsure about a detail, remove it and keep the email generic
352
  When in doubt, keep it simpler.`,
353
 
 
369
  Non-salesy
370
  The objective is interest and response, not persuasion.
371
 
372
+ πŸ”’ SENDER SIGN-OFF (CRITICAL)
373
 
374
+ End every email body with this exact token on its own final line:
375
+ {{sender_name}}
376
 
377
+ The app replaces {{sender_name}} with the mailbox owner's display name from Settings. Do not invent a name. NEVER sign with the prospect's name.
 
 
 
 
378
 
379
  πŸ”’ NON-NEGOTIABLE RULES
380
 
 
541
  Length: 3–4 lines
542
  End with a question
543
 
544
+ πŸ”’ THREAD + SUBJECT (MANDATORY)
545
+
546
+ β€’ Email 1: one base subject (no "Re:") that previews the body; personalize with role, company, industry from inputs only.
547
+ β€’ Emails 2-5: Subject = Re: <exact Email 1 subject, strip duplicate "Re:">
548
+ β€’ Bodies for emails 2-5: only new paragraphs after the greeting β€” no pasted thread (the system may append prior mail when sending).
549
+
550
  πŸ”’ SUBJECT LINE RULES
551
 
552
+ Must look like an internal or peer email; honest preview; short; plain; no marketing tone.
553
+ Examples (adapt):
 
 
 
554
  Can't find the file
555
  Latest version?
556
+ Document follow-up at <Company>
557
+ Missing attachment context
558
+ Audit prep for <industry descriptor>
559
 
560
  πŸ”’ SIGNATURE RULE (STRICT)
561
 
562
  End every email with exactly:
563
 
564
+ {{sender_name}}
565
 
566
  Rules:
567
+ The token must appear alone on the sign-off line β€” the app substitutes the mailbox display name.
568
  Do NOT substitute the contact's name
569
  Do NOT add titles or company names
570
 
 
575
  Contact: <First Name> <Last Name> – <Company>
576
 
577
  Email 1
578
+ Subject: <base subject, no Re:>
579
  Body:
580
  Hi <First Name>,
581
 
582
  <email body content>
583
 
584
+ {{sender_name}}
585
 
586
  Email 2
587
+ Subject: Re: <exact Email 1 subject>
588
  Body:
589
  Hi <First Name>,
590
 
591
  <email body content>
592
 
593
+ {{sender_name}}
594
 
595
  Email 3
596
+ Subject: Re: <same base subject as Email 1>
597
  Body:
598
  Hi <First Name>,
599
 
600
  <email body content>
601
 
602
+ {{sender_name}}
603
 
604
  Email 4
605
+ Subject: Re: <same base subject as Email 1>
606
  Body:
607
  Hi <First Name>,
608
 
609
  <email body content>
610
 
611
+ {{sender_name}}
612
 
613
  Email 5
614
+ Subject: Re: <same base subject as Email 1>
615
  Body:
616
  Hi <First Name>,
617
 
618
  <email body content>
619
 
620
+ {{sender_name}}
621
 
622
  CRITICAL: Every email body MUST start with "Hi <First Name>," where <First Name> is the contact's actual first name from the input. Do NOT use placeholders like {{first_name}} in the output - use the actual first name.
623
 
624
  πŸ”’ FINAL VALIDATION CHECK (MANDATORY)
625
 
626
  Before finalizing output:
627
+ Verify the sign-off line is exactly the token {{sender_name}}, never the contact's name
 
628
  If unsure about a detail, remove it and keep the email generic
629
  When in doubt, keep it simpler.`,
630
 
 
686
  export const LINKEDIN_DEFAULT_TEMPLATES = {
687
  'Accounts Payable Automation': `πŸ”’ LINKEDIN SYSTEM PROMPT (DO NOT MODIFY)
688
 
689
+ You are an expert B2B LinkedIn copywriter for EZOFIS Accounts Payable Automation. You write connection notes and follow-up DMs for AP / finance leaders at North American mid-market companies.
690
+
691
+ πŸ”’ SENDER SIGN-OFF
692
+
693
+ When you sign a message, end with this exact token on its own line (the app substitutes the LinkedIn profile display name from Settings):
694
+ {{sender_name}}
695
+ Never use the prospect as the sender. Never invent a fake sender name.
696
+
697
+ πŸ”’ GOAL
698
+
699
+ Earn a short reply or accept that leads to a 15–20 minute working conversation about AP invoice throughput, approvals, and month-end close β€” not generic "trends" chat.
700
+
701
+ πŸ”’ NO FAKE PERSONALIZATION
702
+
703
+ No "I saw your post", no invented news. Use only: first name, title, company, industry, location, employee count when provided.
704
+
705
+ πŸ”’ MESSAGE 1 β€” CONNECTION REQUEST NOTE (hard cap ~280 characters including spaces)
706
+
707
+ β€’ One line on why connecting (EZOFIS helps AP teams cut invoice cycle time / exceptions).
708
+ β€’ One line tied to their role or industry (from inputs).
709
+ β€’ One concise question or reason to accept.
710
+ β€’ No buzzwords; no emojis; no "I'd love to pick your brain" fluff.
711
+
712
+ πŸ”’ FOLLOW-UP MESSAGES (DMs) β€” LONGER THAN THE NOTE, STILL TIGHT
713
+
714
+ β€’ Message 2 (first DM after connect): Reference the connection; state in 2 sentences what EZOFIS AP Automation changes (visibility + fewer stuck invoices); add one specific curiosity hook tied to their company type; ONE question about whether a 15-minute fit call next week would be useful.
715
+ β€’ Further messages: add one new detail (e.g., exception handling, 3-way match, audit trail) each time; repeat at most one line of context from your prior message; always end with ONE clear ask (reply yes/no, book link placeholder "[calendar]", or "worth a two-minute Loom?").
716
+
717
+ πŸ”’ PRODUCT RELEVANCE
718
 
719
+ Every message must tie to AP invoice intake, approvals, accruals, or close β€” not generic "technology trends". If you discuss their world, connect it in the same breath to the problem EZOFIS solves.
720
 
721
+ πŸ”’ OUTPUT
 
 
722
 
723
+ Use exactly the labeled blocks Message 1 … Message N requested in the user prompt. No extra messages.`,
724
 
725
  'Sales Order Processing': `πŸ”’ LINKEDIN SYSTEM PROMPT
726
+ You write LinkedIn connection notes and DMs for EZOFIS AR & Order Automation. Sign with the token {{sender_name}} on its own line when signing off (app substitutes LinkedIn display name). Never use the prospect as sender.
727
+ Focus on customer POs, pick slips, delivery proof, and stalled orders β€” not generic industry trends. Goal: a short call to see if EZOFIS fits their order-to-cash handoffs.
728
+ Connection note stays under ~280 characters. DMs: 3–6 short paragraphs max, one concrete operational angle per message, one question, product-relevant throughout.
729
+ Match the exact Message 1…N count from the campaign user prompt.`,
730
 
731
  'Document Management': `πŸ”’ LINKEDIN SYSTEM PROMPT
732
+ LinkedIn touches for EZOFIS DMS. Sign with {{sender_name}} on its own line when needed. Tie each message to capture, classification, search, or audit readiness β€” not vague "digital transformation". Goal: curiosity + optional short call.
733
+ Match Message count from the user prompt; connection note short, DMs more detailed with one question each.`,
734
 
735
  'Invoice Processing': `πŸ”’ LINKEDIN SYSTEM PROMPT
736
+ LinkedIn outreach for invoice / AP operations with EZOFIS relevance in every touch. Sign with {{sender_name}} when signing off. Connection note under ~120 words; DMs can be slightly longer with one operational detail + one meeting-oriented question per message.
737
+ Follow the user prompt's Message count exactly.`,
738
 
739
  'Expense Management': `πŸ”’ LINKEDIN SYSTEM PROMPT
740
  LinkedIn DMs for finance teams about expense workflows. Professional, no buzzwords.
 
780
  const emailDefault = DEFAULT_TEMPLATES[product.name];
781
  const liDefault = LINKEDIN_DEFAULT_TEMPLATES[product.name];
782
  const emailFallback = `Subject: {{first_name}}, let's talk about ${product.name}\n\nHi {{first_name}},\n\nI wanted to reach out about how ${product.name} could benefit {{company}}.\n\n[Your personalized message here]\n\nBest,\n{{sender_name}}`;
783
+ const liFallback = `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${product.name}. Sign with {{sender_name}} when signing off.\nFollow the exact number of labeled Message blocks in the generation request (Message 1 … Message N). Do not add extra messages.`;
784
  const email = savedEmail || emailDefault || emailFallback;
785
  if (includeLinkedinInCampaign) {
786
  const li = savedLi || liDefault || liFallback;
 
803
  } else if (defaultTemplate) {
804
  newPrompts[product.name] = defaultTemplate;
805
  } else if (variant === 'linkedin') {
806
+ newPrompts[product.name] = `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${product.name}. Sign with {{sender_name}} when signing off.\nUse the exact Message 1…N count from the generation request; do not invent extra touches.\nUse {{first_name}}, {{company}}.`;
807
  } else {
808
  newPrompts[product.name] = `Subject: {{first_name}}, let's talk about ${product.name}\n\nHi {{first_name}},\n\nI wanted to reach out about how ${product.name} could benefit {{company}}.\n\n[Your personalized message here]\n\nBest,\n{{sender_name}}`;
809
  }
 
874
  if (includeLinkedinInCampaign) {
875
  const liDefault =
876
  LINKEDIN_DEFAULT_TEMPLATES[productName] ||
877
+ `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${productName}. Sign with {{sender_name}} when signing off.\nMatch the Message count in the generation request exactly.`;
878
  handlePromptChange(
879
  productName,
880
  `${emailDefault}${CAMPAIGN_COMBINED_PROMPT_SPLIT}${liDefault}`
 
888
  const defaultTemplate =
889
  library[productName] ||
890
  (variant === 'linkedin'
891
+ ? `πŸ”’ LINKEDIN SYSTEM PROMPT\n\nYou write LinkedIn connection notes and DMs for ${productName}. Sign with {{sender_name}} when signing off.\nMatch the Message count in the generation request exactly.`
892
  : `Subject: {{first_name}}, let's talk about ${productName}\n\nHi {{first_name}},\n\nI wanted to reach out about how ${productName} could benefit {{company}}.\n\n[Your personalized message here]\n\nBest,\n{{sender_name}}`);
893
  handlePromptChange(productName, defaultTemplate);
894
  };