Seth commited on
Commit
af27681
·
1 Parent(s): b9cd14b
frontend/src/components/campaigns/CreateCampaignWizard.jsx CHANGED
@@ -222,6 +222,7 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
222
  const [genPhase, setGenPhase] = useState('email');
223
  const [genRunId, setGenRunId] = useState(0);
224
  const eventSourceRef = useRef(null);
 
225
  const prevGenRunIdRef = useRef(null);
226
 
227
  const sequenceHasLinkedin = useMemo(
@@ -510,6 +511,7 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
510
  alert('Select products and fill all prompt templates before generating.');
511
  return;
512
  }
 
513
  try {
514
  const res = await apiFetch('/api/save-prompts', {
515
  method: 'POST',
@@ -720,9 +722,11 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
720
  </span>
721
  <h3 className="mt-2 text-lg font-semibold text-slate-900">Prompts & generation</h3>
722
  <p className="mt-1 text-sm text-slate-600">
723
- Same experience as Email / AI Generator: templates follow your product defaults, with
724
- an added block that mirrors the sequence you configured (Gmail + LinkedIn). Touch
725
- counts in the prompt are ignored — the wizard sequence is authoritative.
 
 
726
  </p>
727
  </div>
728
  <ProductSelector
@@ -732,25 +736,15 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
732
  {selectedProducts.length > 0 ? (
733
  <>
734
  <PromptEditor
 
735
  selectedProducts={selectedProducts}
736
  prompts={prompts}
737
  onPromptsChange={setPrompts}
738
- variant="email"
 
 
 
739
  />
740
- {sequenceHasLinkedin ? (
741
- <div className="space-y-2">
742
- <h4 className="text-base font-semibold text-slate-800">LinkedIn prompts</h4>
743
- <p className="text-sm text-slate-500">
744
- Required because this campaign includes LinkedIn steps.
745
- </p>
746
- <PromptEditor
747
- selectedProducts={selectedProducts}
748
- prompts={linkedinPrompts}
749
- onPromptsChange={setLinkedinPrompts}
750
- variant="linkedin"
751
- />
752
- </div>
753
- ) : null}
754
  <div className="flex flex-wrap items-center gap-3">
755
  <Button
756
  type="button"
 
222
  const [genPhase, setGenPhase] = useState('email');
223
  const [genRunId, setGenRunId] = useState(0);
224
  const eventSourceRef = useRef(null);
225
+ const promptEditorRef = useRef(null);
226
  const prevGenRunIdRef = useRef(null);
227
 
228
  const sequenceHasLinkedin = useMemo(
 
511
  alert('Select products and fill all prompt templates before generating.');
512
  return;
513
  }
514
+ promptEditorRef.current?.commitCampaignPrompts?.();
515
  try {
516
  const res = await apiFetch('/api/save-prompts', {
517
  method: 'POST',
 
722
  </span>
723
  <h3 className="mt-2 text-lg font-semibold text-slate-900">Prompts & generation</h3>
724
  <p className="mt-1 text-sm text-slate-600">
725
+ One template per product: Gmail prompt above the divider, LinkedIn system prompt
726
+ below (when your sequence includes LinkedIn). Touch counts in the text are ignored —
727
+ the wizard sequence is authoritative. Click{' '}
728
+ <span className="font-medium text-slate-800">Save template</span> or{' '}
729
+ <span className="font-medium text-slate-800">Generate</span> to apply edits.
730
  </p>
731
  </div>
732
  <ProductSelector
 
736
  {selectedProducts.length > 0 ? (
737
  <>
738
  <PromptEditor
739
+ ref={promptEditorRef}
740
  selectedProducts={selectedProducts}
741
  prompts={prompts}
742
  onPromptsChange={setPrompts}
743
+ variant="campaign"
744
+ includeLinkedinInCampaign={sequenceHasLinkedin}
745
+ linkedinPrompts={linkedinPrompts}
746
+ onLinkedinPromptsChange={setLinkedinPrompts}
747
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  <div className="flex flex-wrap items-center gap-3">
749
  <Button
750
  type="button"
frontend/src/components/prompts/PromptEditor.jsx CHANGED
@@ -1,10 +1,24 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { FileText, Save, RotateCcw, Sparkles, CheckCircle2, Linkedin } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
  import { Textarea } from "@/components/ui/textarea";
5
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
6
  import { motion, AnimatePresence } from 'framer-motion';
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  export const DEFAULT_TEMPLATES = {
9
  'Accounts Payable Automation': `🔒 SYSTEM PROMPT (DO NOT MODIFY)
10
 
@@ -817,13 +831,19 @@ LinkedIn DMs for finance teams about expense workflows. 3 messages, professional
817
  Procurement-focused LinkedIn sequence in 3 messages. Practical questions only. Label Message 1, 2, 3.`,
818
  };
819
 
820
- export default function PromptEditor({
821
- selectedProducts,
822
- prompts,
823
- onPromptsChange,
824
- onSaveComplete,
825
- variant = 'email',
826
- }) {
 
 
 
 
 
 
827
  const [activeTab, setActiveTab] = useState(selectedProducts[0]?.name || '');
828
  const [savedStatus, setSavedStatus] = useState({});
829
  const [localPrompts, setLocalPrompts] = useState({});
@@ -838,6 +858,26 @@ export default function PromptEditor({
838
  }, [selectedProducts, activeTab]);
839
 
840
  useEffect(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
841
  const library = variant === 'linkedin' ? LINKEDIN_DEFAULT_TEMPLATES : DEFAULT_TEMPLATES;
842
  const newPrompts = {};
843
  selectedProducts.forEach((product) => {
@@ -855,7 +895,7 @@ export default function PromptEditor({
855
  }
856
  });
857
  setLocalPrompts(newPrompts);
858
- }, [selectedProducts, prompts, variant]);
859
 
860
  const handlePromptChange = (productName, value) => {
861
  setLocalPrompts(prev => ({
@@ -868,11 +908,30 @@ export default function PromptEditor({
868
  }));
869
  };
870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
  const handleSave = (productName) => {
872
- onPromptsChange({
873
- ...prompts,
874
- [productName]: localPrompts[productName]
875
- });
 
 
 
 
876
  setSavedStatus(prev => ({
877
  ...prev,
878
  [productName]: true
@@ -894,6 +953,23 @@ export default function PromptEditor({
894
  };
895
 
896
  const handleRestoreDefault = (productName) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  const library = variant === 'linkedin' ? LINKEDIN_DEFAULT_TEMPLATES : DEFAULT_TEMPLATES;
898
  const defaultTemplate =
899
  library[productName] ||
@@ -903,6 +979,42 @@ export default function PromptEditor({
903
  handlePromptChange(productName, defaultTemplate);
904
  };
905
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
906
  if (selectedProducts.length === 0) {
907
  return (
908
  <div className="rounded-2xl border border-slate-200 bg-slate-50/50 p-12 text-center">
@@ -945,6 +1057,11 @@ export default function PromptEditor({
945
  <div className="rounded-lg bg-violet-100 p-2">
946
  {variant === 'linkedin' ? (
947
  <Linkedin className="h-4 w-4 text-violet-600" />
 
 
 
 
 
948
  ) : (
949
  <Sparkles className="h-4 w-4 text-violet-600" />
950
  )}
@@ -953,10 +1070,28 @@ export default function PromptEditor({
953
  <h4 className="font-semibold text-slate-800">
954
  {variant === 'linkedin'
955
  ? 'LinkedIn sequence prompt'
956
- : 'Email Template'}
 
 
 
 
957
  </h4>
958
  <p className="text-xs text-slate-500">
959
- Use variables: {"{{first_name}}"}, {"{{company}}"}, {"{{sender_name}}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
960
  </p>
961
  </div>
962
  </div>
@@ -993,13 +1128,24 @@ export default function PromptEditor({
993
  <Textarea
994
  value={localPrompts[product.name] || ''}
995
  onChange={(e) => handlePromptChange(product.name, e.target.value)}
 
 
 
 
 
996
  placeholder={
997
  variant === 'linkedin'
998
  ? 'Enter your LinkedIn sequence system prompt…'
999
- : 'Enter your email template here...'
 
 
 
 
1000
  }
1001
- className="min-h-[320px] font-mono text-sm leading-relaxed resize-none
1002
- border-slate-200 focus:border-violet-300 focus:ring-violet-200"
 
 
1003
  />
1004
  </div>
1005
  {/* Save button at bottom for easy access after scrolling */}
@@ -1029,4 +1175,6 @@ export default function PromptEditor({
1029
  </Tabs>
1030
  </div>
1031
  );
1032
- }
 
 
 
1
+ import React, { useState, useEffect, forwardRef, useImperativeHandle, useCallback } from 'react';
2
  import { FileText, Save, RotateCcw, Sparkles, CheckCircle2, Linkedin } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
  import { Textarea } from "@/components/ui/textarea";
5
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
6
  import { motion, AnimatePresence } from 'framer-motion';
7
 
8
+ /** Do not remove this line in combined campaign prompts — it separates Gmail vs LinkedIn system context. */
9
+ export const CAMPAIGN_COMBINED_PROMPT_SPLIT = '\n\n<<<CAMPAIGN_LINKEDIN_PROMPT>>>\n\n';
10
+
11
+ function splitCampaignRaw(raw, includeLinkedinSection) {
12
+ if (!includeLinkedinSection) {
13
+ return { email: raw, linkedin: '' };
14
+ }
15
+ const idx = raw.indexOf(CAMPAIGN_COMBINED_PROMPT_SPLIT);
16
+ return {
17
+ email: (idx === -1 ? raw : raw.slice(0, idx)).trimEnd(),
18
+ linkedin: (idx === -1 ? '' : raw.slice(idx + CAMPAIGN_COMBINED_PROMPT_SPLIT.length)).trim(),
19
+ };
20
+ }
21
+
22
  export const DEFAULT_TEMPLATES = {
23
  'Accounts Payable Automation': `🔒 SYSTEM PROMPT (DO NOT MODIFY)
24
 
 
831
  Procurement-focused LinkedIn sequence in 3 messages. Practical questions only. Label Message 1, 2, 3.`,
832
  };
833
 
834
+ const PromptEditor = forwardRef(function PromptEditor(
835
+ {
836
+ selectedProducts,
837
+ prompts,
838
+ onPromptsChange,
839
+ onSaveComplete,
840
+ variant = 'email',
841
+ linkedinPrompts = {},
842
+ onLinkedinPromptsChange,
843
+ includeLinkedinInCampaign = false,
844
+ },
845
+ ref
846
+ ) {
847
  const [activeTab, setActiveTab] = useState(selectedProducts[0]?.name || '');
848
  const [savedStatus, setSavedStatus] = useState({});
849
  const [localPrompts, setLocalPrompts] = useState({});
 
858
  }, [selectedProducts, activeTab]);
859
 
860
  useEffect(() => {
861
+ if (variant === 'campaign') {
862
+ const newLocal = {};
863
+ selectedProducts.forEach((product) => {
864
+ const savedEmail = prompts[product.name];
865
+ const savedLi = linkedinPrompts[product.name];
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\nGenerate a 3-message LinkedIn sequence for ${product.name}.\nLabel: Message 1, Message 2, Message 3.\nUse {{first_name}}, {{company}}. Sender: Anna.`;
870
+ const email = savedEmail || emailDefault || emailFallback;
871
+ if (includeLinkedinInCampaign) {
872
+ const li = savedLi || liDefault || liFallback;
873
+ newLocal[product.name] = `${email}${CAMPAIGN_COMBINED_PROMPT_SPLIT}${li}`;
874
+ } else {
875
+ newLocal[product.name] = email;
876
+ }
877
+ });
878
+ setLocalPrompts(newLocal);
879
+ return;
880
+ }
881
  const library = variant === 'linkedin' ? LINKEDIN_DEFAULT_TEMPLATES : DEFAULT_TEMPLATES;
882
  const newPrompts = {};
883
  selectedProducts.forEach((product) => {
 
895
  }
896
  });
897
  setLocalPrompts(newPrompts);
898
+ }, [selectedProducts, prompts, linkedinPrompts, variant, includeLinkedinInCampaign]);
899
 
900
  const handlePromptChange = (productName, value) => {
901
  setLocalPrompts(prev => ({
 
908
  }));
909
  };
910
 
911
+ const flushProductToParent = useCallback(
912
+ (productName) => {
913
+ if (variant !== 'campaign') return;
914
+ const raw = localPrompts[productName] ?? '';
915
+ if (!includeLinkedinInCampaign || !onLinkedinPromptsChange) {
916
+ onPromptsChange((prev) => ({ ...prev, [productName]: raw }));
917
+ return;
918
+ }
919
+ const { email, linkedin } = splitCampaignRaw(raw, true);
920
+ onPromptsChange((prev) => ({ ...prev, [productName]: email }));
921
+ onLinkedinPromptsChange((prev) => ({ ...prev, [productName]: linkedin }));
922
+ },
923
+ [variant, localPrompts, includeLinkedinInCampaign, onPromptsChange, onLinkedinPromptsChange]
924
+ );
925
+
926
  const handleSave = (productName) => {
927
+ if (variant === 'campaign') {
928
+ flushProductToParent(productName);
929
+ } else {
930
+ onPromptsChange({
931
+ ...prompts,
932
+ [productName]: localPrompts[productName] ?? '',
933
+ });
934
+ }
935
  setSavedStatus(prev => ({
936
  ...prev,
937
  [productName]: true
 
953
  };
954
 
955
  const handleRestoreDefault = (productName) => {
956
+ if (variant === 'campaign') {
957
+ const emailDefault =
958
+ DEFAULT_TEMPLATES[productName] ||
959
+ `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}}`;
960
+ if (includeLinkedinInCampaign) {
961
+ const liDefault =
962
+ LINKEDIN_DEFAULT_TEMPLATES[productName] ||
963
+ `🔒 LINKEDIN SYSTEM PROMPT\n\nGenerate a 3-message LinkedIn sequence for ${productName}.\nLabel: Message 1, Message 2, Message 3.\nUse {{first_name}}, {{company}}. Sender: Anna.`;
964
+ handlePromptChange(
965
+ productName,
966
+ `${emailDefault}${CAMPAIGN_COMBINED_PROMPT_SPLIT}${liDefault}`
967
+ );
968
+ } else {
969
+ handlePromptChange(productName, emailDefault);
970
+ }
971
+ return;
972
+ }
973
  const library = variant === 'linkedin' ? LINKEDIN_DEFAULT_TEMPLATES : DEFAULT_TEMPLATES;
974
  const defaultTemplate =
975
  library[productName] ||
 
979
  handlePromptChange(productName, defaultTemplate);
980
  };
981
 
982
+ useImperativeHandle(
983
+ ref,
984
+ () => ({
985
+ commitCampaignPrompts() {
986
+ if (variant !== 'campaign') return;
987
+ const nextP = { ...prompts };
988
+ const nextLi = { ...linkedinPrompts };
989
+ selectedProducts.forEach((product) => {
990
+ const name = product.name;
991
+ const raw = localPrompts[name] ?? '';
992
+ if (!includeLinkedinInCampaign || !onLinkedinPromptsChange) {
993
+ nextP[name] = raw;
994
+ } else {
995
+ const { email, linkedin } = splitCampaignRaw(raw, true);
996
+ nextP[name] = email;
997
+ nextLi[name] = linkedin;
998
+ }
999
+ });
1000
+ onPromptsChange(nextP);
1001
+ if (includeLinkedinInCampaign && onLinkedinPromptsChange) {
1002
+ onLinkedinPromptsChange(nextLi);
1003
+ }
1004
+ },
1005
+ }),
1006
+ [
1007
+ variant,
1008
+ selectedProducts,
1009
+ localPrompts,
1010
+ prompts,
1011
+ linkedinPrompts,
1012
+ includeLinkedinInCampaign,
1013
+ onPromptsChange,
1014
+ onLinkedinPromptsChange,
1015
+ ]
1016
+ );
1017
+
1018
  if (selectedProducts.length === 0) {
1019
  return (
1020
  <div className="rounded-2xl border border-slate-200 bg-slate-50/50 p-12 text-center">
 
1057
  <div className="rounded-lg bg-violet-100 p-2">
1058
  {variant === 'linkedin' ? (
1059
  <Linkedin className="h-4 w-4 text-violet-600" />
1060
+ ) : variant === 'campaign' ? (
1061
+ <div className="flex gap-0.5">
1062
+ <Sparkles className="h-4 w-4 text-violet-600" />
1063
+ <Linkedin className="h-4 w-4 text-sky-600" />
1064
+ </div>
1065
  ) : (
1066
  <Sparkles className="h-4 w-4 text-violet-600" />
1067
  )}
 
1070
  <h4 className="font-semibold text-slate-800">
1071
  {variant === 'linkedin'
1072
  ? 'LinkedIn sequence prompt'
1073
+ : variant === 'campaign'
1074
+ ? includeLinkedinInCampaign
1075
+ ? 'Gmail + LinkedIn prompts'
1076
+ : 'Gmail prompts'
1077
+ : 'Email Template'}
1078
  </h4>
1079
  <p className="text-xs text-slate-500">
1080
+ {variant === 'campaign' && includeLinkedinInCampaign ? (
1081
+ <>
1082
+ Above the line{' '}
1083
+ <code className="rounded bg-slate-100 px-1 text-[10px]">
1084
+ {`<<<CAMPAIGN_LINKEDIN_PROMPT>>>`}
1085
+ </code>{' '}
1086
+ = Gmail (email generation). Below = LinkedIn system prompt.
1087
+ Keep that divider on its own line.
1088
+ </>
1089
+ ) : (
1090
+ <>
1091
+ Use variables: {"{{first_name}}"}, {"{{company}}"},{" "}
1092
+ {"{{sender_name}}"}
1093
+ </>
1094
+ )}
1095
  </p>
1096
  </div>
1097
  </div>
 
1128
  <Textarea
1129
  value={localPrompts[product.name] || ''}
1130
  onChange={(e) => handlePromptChange(product.name, e.target.value)}
1131
+ onBlur={() => {
1132
+ if (variant === 'campaign') {
1133
+ flushProductToParent(product.name);
1134
+ }
1135
+ }}
1136
  placeholder={
1137
  variant === 'linkedin'
1138
  ? 'Enter your LinkedIn sequence system prompt…'
1139
+ : variant === 'campaign'
1140
+ ? includeLinkedinInCampaign
1141
+ ? 'Gmail prompt above the divider, then LinkedIn prompt below…'
1142
+ : 'Enter your Gmail / email generation prompt…'
1143
+ : 'Enter your email template here...'
1144
  }
1145
+ className={`${
1146
+ variant === 'campaign' ? 'min-h-[420px]' : 'min-h-[320px]'
1147
+ } font-mono text-sm leading-relaxed resize-none
1148
+ border-slate-200 focus:border-violet-300 focus:ring-violet-200`}
1149
  />
1150
  </div>
1151
  {/* Save button at bottom for easy access after scrolling */}
 
1175
  </Tabs>
1176
  </div>
1177
  );
1178
+ });
1179
+
1180
+ export default PromptEditor;