Seth commited on
Commit
150f04b
·
1 Parent(s): dca1812
frontend/src/components/campaigns/CampaignSequenceBuilder.jsx ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import {
3
+ Play,
4
+ Plus,
5
+ ChevronRight,
6
+ Flag,
7
+ Mail,
8
+ UserPlus,
9
+ MessageCircle,
10
+ Clock,
11
+ X,
12
+ } from 'lucide-react';
13
+ import { cn } from '@/lib/utils';
14
+
15
+ function uid() {
16
+ return typeof crypto !== 'undefined' && crypto.randomUUID
17
+ ? crypto.randomUUID()
18
+ : `s_${Date.now()}_${Math.random().toString(16).slice(2)}`;
19
+ }
20
+
21
+ /** Default sequence matching the reference layout */
22
+ export function createDefaultSequenceSteps() {
23
+ return [
24
+ {
25
+ id: uid(),
26
+ type: 'action',
27
+ channel: 'gmail',
28
+ action: 'email',
29
+ title: 'Send Email',
30
+ subtitle: 'Subject: Introduction to SequenceAI Efficiency',
31
+ },
32
+ { id: uid(), type: 'wait', days: 1 },
33
+ {
34
+ id: uid(),
35
+ type: 'action',
36
+ channel: 'linkedin',
37
+ action: 'linkedin_connect',
38
+ title: 'LinkedIn Connection Request',
39
+ subtitle: 'Personalized note using {first_name}',
40
+ },
41
+ { id: uid(), type: 'wait', days: 2 },
42
+ {
43
+ id: uid(),
44
+ type: 'action',
45
+ channel: 'linkedin',
46
+ action: 'linkedin_dm',
47
+ title: 'Follow up Message',
48
+ subtitle: 'Reference the previous email content',
49
+ },
50
+ {
51
+ id: uid(),
52
+ type: 'action',
53
+ channel: 'gmail',
54
+ action: 'email',
55
+ title: 'Send Email Followup',
56
+ subtitle: 'Re: Introduction to SequenceAI Efficiency',
57
+ },
58
+ ];
59
+ }
60
+
61
+ const ACTION_PRESETS = {
62
+ email: {
63
+ channel: 'gmail',
64
+ action: 'email',
65
+ title: 'Send Email',
66
+ subtitle: 'Subject: Your outreach subject line',
67
+ },
68
+ linkedin_connect: {
69
+ channel: 'linkedin',
70
+ action: 'linkedin_connect',
71
+ title: 'LinkedIn Connection Request',
72
+ subtitle: 'Personalized note using {first_name}',
73
+ },
74
+ linkedin_dm: {
75
+ channel: 'linkedin',
76
+ action: 'linkedin_dm',
77
+ title: 'LinkedIn Message',
78
+ subtitle: 'Follow-up message body preview',
79
+ },
80
+ };
81
+
82
+ /** Uppercase labels like the reference UI */
83
+ function platformBadge(channel) {
84
+ if (channel === 'gmail') return 'GMAIL';
85
+ if (channel === 'linkedin') return 'LINKEDIN';
86
+ return '—';
87
+ }
88
+
89
+ function ActionIcon({ action }) {
90
+ const wrap = 'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-sky-100 text-sky-800';
91
+ if (action === 'linkedin_connect') {
92
+ return (
93
+ <div className={wrap}>
94
+ <UserPlus className="h-5 w-5" strokeWidth={2} />
95
+ </div>
96
+ );
97
+ }
98
+ if (action === 'linkedin_dm') {
99
+ return (
100
+ <div className={wrap}>
101
+ <MessageCircle className="h-5 w-5" strokeWidth={2} />
102
+ </div>
103
+ );
104
+ }
105
+ return (
106
+ <div className={wrap}>
107
+ <Mail className="h-5 w-5" strokeWidth={2} />
108
+ </div>
109
+ );
110
+ }
111
+
112
+ function InsertStepPicker({ open, onPick, onClose }) {
113
+ if (!open) return null;
114
+ return (
115
+ <div
116
+ className="absolute left-1/2 top-full z-20 mt-2 w-[min(100vw-2rem,22rem)] -translate-x-1/2 rounded-xl border border-slate-200 bg-white p-2 shadow-lg"
117
+ role="menu"
118
+ >
119
+ <p className="mb-2 px-2 text-[11px] font-medium uppercase tracking-wide text-slate-500">
120
+ Add step
121
+ </p>
122
+ <div className="flex flex-col gap-1">
123
+ <button
124
+ type="button"
125
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-slate-800 hover:bg-slate-50"
126
+ onClick={() => onPick('email')}
127
+ >
128
+ <Mail className="h-4 w-4 text-sky-600" />
129
+ Send Email (Gmail)
130
+ </button>
131
+ <button
132
+ type="button"
133
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-slate-800 hover:bg-slate-50"
134
+ onClick={() => onPick('linkedin_connect')}
135
+ >
136
+ <UserPlus className="h-4 w-4 text-sky-600" />
137
+ LinkedIn connection request
138
+ </button>
139
+ <button
140
+ type="button"
141
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-slate-800 hover:bg-slate-50"
142
+ onClick={() => onPick('linkedin_dm')}
143
+ >
144
+ <MessageCircle className="h-4 w-4 text-sky-600" />
145
+ LinkedIn message
146
+ </button>
147
+ <button
148
+ type="button"
149
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-slate-800 hover:bg-slate-50"
150
+ onClick={() => onPick('wait')}
151
+ >
152
+ <Clock className="h-4 w-4 text-blue-600" />
153
+ Wait / delay
154
+ </button>
155
+ </div>
156
+ <button
157
+ type="button"
158
+ className="mt-1 w-full rounded-lg py-1.5 text-xs text-slate-500 hover:bg-slate-50"
159
+ onClick={onClose}
160
+ >
161
+ Cancel
162
+ </button>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ function WaitCard({ step, onRemove, onDaysChange }) {
168
+ return (
169
+ <div className="relative w-full max-w-md">
170
+ <div className="flex items-center justify-center gap-3 rounded-xl border border-dashed border-blue-300 bg-sky-50/90 px-4 py-3 text-sm text-slate-800 shadow-sm">
171
+ <Clock className="h-4 w-4 shrink-0 text-blue-600" />
172
+ <span className="font-medium">Wait</span>
173
+ <label className="flex items-center gap-1 font-medium tabular-nums">
174
+ <input
175
+ type="number"
176
+ min={1}
177
+ max={365}
178
+ className="w-12 rounded-md border border-blue-200 bg-white px-2 py-1 text-center text-sm"
179
+ value={step.days}
180
+ onChange={(e) => onDaysChange?.(Number(e.target.value))}
181
+ />
182
+ {step.days === 1 ? 'day' : 'days'}
183
+ </label>
184
+ {onRemove ? (
185
+ <button
186
+ type="button"
187
+ className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-slate-400 hover:bg-sky-100 hover:text-slate-700"
188
+ aria-label="Remove step"
189
+ onClick={onRemove}
190
+ >
191
+ <X className="h-4 w-4" />
192
+ </button>
193
+ ) : null}
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ function ActionCard({ step, onConfigure, onRemove }) {
200
+ return (
201
+ <div className="relative w-full max-w-md">
202
+ <button
203
+ type="button"
204
+ className="flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-left shadow-sm transition hover:border-slate-300 hover:bg-slate-50/80"
205
+ onClick={() => onConfigure?.(step)}
206
+ >
207
+ <ActionIcon action={step.action} />
208
+ <div className="min-w-0 flex-1">
209
+ <div className="flex items-start justify-between gap-2">
210
+ <p className="font-semibold text-slate-900">{step.title}</p>
211
+ <span className="shrink-0 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
212
+ {platformBadge(step.channel)}
213
+ </span>
214
+ </div>
215
+ <p className="mt-0.5 truncate text-sm text-slate-500">{step.subtitle}</p>
216
+ </div>
217
+ <ChevronRight className="h-5 w-5 shrink-0 text-slate-300" aria-hidden />
218
+ </button>
219
+ {onRemove ? (
220
+ <button
221
+ type="button"
222
+ className="absolute -right-1 -top-1 rounded-full border border-slate-200 bg-white p-1 text-slate-400 shadow-sm hover:border-red-200 hover:bg-red-50 hover:text-red-600"
223
+ aria-label="Remove step"
224
+ onClick={(e) => {
225
+ e.stopPropagation();
226
+ onRemove();
227
+ }}
228
+ >
229
+ <X className="h-3.5 w-3.5" />
230
+ </button>
231
+ ) : null}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ /**
237
+ * Vertical sequence builder: START → steps (+ inserts) → END with dashed spine.
238
+ */
239
+ export default function CampaignSequenceBuilder({ value, onChange }) {
240
+ const steps = value;
241
+ const setSteps = onChange;
242
+ const [pickerIndex, setPickerIndex] = useState(null);
243
+
244
+ const closePicker = useCallback(() => setPickerIndex(null), []);
245
+
246
+ const insertAt = useCallback(
247
+ (index, kind) => {
248
+ let newStep;
249
+ if (kind === 'wait') {
250
+ newStep = { id: uid(), type: 'wait', days: 1 };
251
+ } else {
252
+ const preset = ACTION_PRESETS[kind];
253
+ newStep = {
254
+ id: uid(),
255
+ type: 'action',
256
+ ...preset,
257
+ };
258
+ }
259
+ const next = [...steps];
260
+ next.splice(index, 0, newStep);
261
+ setSteps(next);
262
+ closePicker();
263
+ },
264
+ [steps, setSteps, closePicker]
265
+ );
266
+
267
+ const removeAt = useCallback(
268
+ (index) => {
269
+ const next = steps.filter((_, i) => i !== index);
270
+ setSteps(next);
271
+ },
272
+ [steps, setSteps]
273
+ );
274
+
275
+ const updateWaitDays = useCallback(
276
+ (index, days) => {
277
+ const n = Math.max(1, Math.min(365, Number(days) || 1));
278
+ const next = steps.map((s, i) => (i === index ? { ...s, days: n } : s));
279
+ setSteps(next);
280
+ },
281
+ [steps, setSteps]
282
+ );
283
+
284
+ const stepCount = useMemo(() => steps.filter((s) => s.type === 'action').length, [steps]);
285
+
286
+ return (
287
+ <div className="mx-auto w-full max-w-md pb-4">
288
+ <div className="mb-4">
289
+ <span className="inline-flex rounded-md bg-slate-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-slate-500">
290
+ Configure sequence
291
+ </span>
292
+ <p className="mt-2 text-sm text-slate-600">
293
+ Build your outreach flow. Use <strong>+</strong> to add emails, LinkedIn steps, or delays. Click a box to
294
+ edit preview text (saved with the campaign).
295
+ </p>
296
+ </div>
297
+
298
+ <div className="relative flex flex-col items-center">
299
+ {/* Vertical dashed spine */}
300
+ <div
301
+ className="pointer-events-none absolute left-1/2 top-8 bottom-8 z-0 w-px -translate-x-1/2 border-l-2 border-dashed border-slate-300"
302
+ aria-hidden
303
+ />
304
+
305
+ <div className="relative z-10 flex w-full flex-col items-center">
306
+ {/* START */}
307
+ <div className="flex flex-col items-center">
308
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-violet-600 text-white shadow-md shadow-violet-200">
309
+ <Play className="ml-0.5 h-5 w-5" strokeWidth={2.5} />
310
+ </div>
311
+ <span className="mt-1 text-xs font-bold uppercase tracking-wide text-slate-500">Start</span>
312
+ </div>
313
+
314
+ {/* Insert after START */}
315
+ <div className="relative my-3 flex h-10 w-full items-center justify-center">
316
+ <button
317
+ type="button"
318
+ className="relative z-10 flex h-9 w-9 items-center justify-center rounded-full border-2 border-slate-200 bg-white text-slate-500 shadow-sm transition hover:border-violet-300 hover:bg-violet-50 hover:text-violet-700"
319
+ aria-label="Add step"
320
+ onClick={() => setPickerIndex((i) => (i === 0 ? null : 0))}
321
+ >
322
+ <Plus className="h-4 w-4" />
323
+ </button>
324
+ <InsertStepPicker
325
+ open={pickerIndex === 0}
326
+ onClose={closePicker}
327
+ onPick={(k) => insertAt(0, k)}
328
+ />
329
+ </div>
330
+
331
+ {steps.map((step, index) => (
332
+ <React.Fragment key={step.id}>
333
+ {step.type === 'wait' ? (
334
+ <WaitCard
335
+ step={step}
336
+ onRemove={() => removeAt(index)}
337
+ onDaysChange={(d) => updateWaitDays(index, d)}
338
+ />
339
+ ) : (
340
+ <ActionCard
341
+ step={step}
342
+ onConfigure={(s) => {
343
+ const title = window.prompt('Step title', s.title);
344
+ if (title === null) return;
345
+ const subtitle = window.prompt('Description / preview', s.subtitle);
346
+ if (subtitle === null) return;
347
+ const next = steps.map((x, i) =>
348
+ i === index ? { ...x, title, subtitle } : x
349
+ );
350
+ setSteps(next);
351
+ }}
352
+ onRemove={() => removeAt(index)}
353
+ />
354
+ )}
355
+
356
+ <div className="relative my-3 flex h-10 w-full items-center justify-center">
357
+ <button
358
+ type="button"
359
+ className="relative z-10 flex h-9 w-9 items-center justify-center rounded-full border-2 border-slate-200 bg-white text-slate-500 shadow-sm transition hover:border-violet-300 hover:bg-violet-50 hover:text-violet-700"
360
+ aria-label="Add step"
361
+ onClick={() =>
362
+ setPickerIndex((cur) => (cur === index + 1 ? null : index + 1))
363
+ }
364
+ >
365
+ <Plus className="h-4 w-4" />
366
+ </button>
367
+ <InsertStepPicker
368
+ open={pickerIndex === index + 1}
369
+ onClose={closePicker}
370
+ onPick={(k) => insertAt(index + 1, k)}
371
+ />
372
+ </div>
373
+ </React.Fragment>
374
+ ))}
375
+
376
+ {/* END */}
377
+ <div className="mt-1 flex flex-col items-center">
378
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-violet-100 text-slate-500 ring-4 ring-violet-50">
379
+ <Flag className="h-5 w-5" />
380
+ </div>
381
+ <span className="mt-1 text-xs font-semibold uppercase tracking-wide text-slate-400">End</span>
382
+ </div>
383
+ </div>
384
+ </div>
385
+
386
+ <p className="mt-6 text-center text-xs text-slate-500">
387
+ {steps.length} sequence nodes · {stepCount} action step{stepCount !== 1 ? 's' : ''}
388
+ </p>
389
+ </div>
390
+ );
391
+ }
frontend/src/components/campaigns/CreateCampaignWizard.jsx CHANGED
@@ -4,6 +4,7 @@ import { X, Upload, ArrowRight, ArrowLeft } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
5
  import { Input } from '@/components/ui/input';
6
  import { cn } from '@/lib/utils';
 
7
 
8
  const STEPS = [
9
  { id: 1, label: 'Upload & Select' },
@@ -32,6 +33,7 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
32
  const [prospectFile, setProspectFile] = useState(null);
33
  const [dragOver, setDragOver] = useState(false);
34
  const [estimatedContacts, setEstimatedContacts] = useState(0);
 
35
 
36
  const reset = useCallback(() => {
37
  setStep(1);
@@ -39,6 +41,7 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
39
  setProspectFile(null);
40
  setEstimatedContacts(0);
41
  setDragOver(false);
 
42
  }, []);
43
 
44
  useEffect(() => {
@@ -96,6 +99,7 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
96
  name: campaignName.trim(),
97
  contacts: estimatedContacts || 0,
98
  prospectFileName: prospectFile?.name || '',
 
99
  });
100
  onOpenChange(false);
101
  reset();
@@ -116,7 +120,12 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
116
  aria-label="Close"
117
  onClick={() => onOpenChange(false)}
118
  />
119
- <div className="relative z-[101] flex max-h-[min(92vh,880px)] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl shadow-violet-200/30">
 
 
 
 
 
120
  <div className="flex items-center justify-between border-b border-slate-100 px-5 py-4">
121
  <h2 id="create-campaign-title" className="text-lg font-semibold text-slate-900">
122
  Create Campaign
@@ -261,10 +270,7 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
261
  </div>
262
  </div>
263
  ) : step === 2 ? (
264
- <PlaceholderStep
265
- title="Configure Sequence"
266
- body="Define steps, delays, and channels for this campaign. You’ll set this up in the next iteration."
267
- />
268
  ) : step === 3 ? (
269
  <PlaceholderStep
270
  title="Generate Contents"
@@ -296,6 +302,12 @@ export default function CreateCampaignWizard({ open, onOpenChange, onComplete })
296
  <span className="text-slate-500">File</span>
297
  <p className="font-medium text-slate-800">{prospectFile?.name || '—'}</p>
298
  </div>
 
 
 
 
 
 
299
  </div>
300
  </div>
301
  <div className="flex justify-end">
 
4
  import { Button } from '@/components/ui/button';
5
  import { Input } from '@/components/ui/input';
6
  import { cn } from '@/lib/utils';
7
+ import CampaignSequenceBuilder, { createDefaultSequenceSteps } from '@/components/campaigns/CampaignSequenceBuilder';
8
 
9
  const STEPS = [
10
  { id: 1, label: 'Upload & Select' },
 
33
  const [prospectFile, setProspectFile] = useState(null);
34
  const [dragOver, setDragOver] = useState(false);
35
  const [estimatedContacts, setEstimatedContacts] = useState(0);
36
+ const [sequenceSteps, setSequenceSteps] = useState(() => createDefaultSequenceSteps());
37
 
38
  const reset = useCallback(() => {
39
  setStep(1);
 
41
  setProspectFile(null);
42
  setEstimatedContacts(0);
43
  setDragOver(false);
44
+ setSequenceSteps(createDefaultSequenceSteps());
45
  }, []);
46
 
47
  useEffect(() => {
 
99
  name: campaignName.trim(),
100
  contacts: estimatedContacts || 0,
101
  prospectFileName: prospectFile?.name || '',
102
+ sequence: sequenceSteps,
103
  });
104
  onOpenChange(false);
105
  reset();
 
120
  aria-label="Close"
121
  onClick={() => onOpenChange(false)}
122
  />
123
+ <div
124
+ className={cn(
125
+ 'relative z-[101] flex max-h-[min(92vh,900px)] w-full flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl shadow-violet-200/30',
126
+ step === 2 ? 'max-w-4xl' : 'max-w-3xl'
127
+ )}
128
+ >
129
  <div className="flex items-center justify-between border-b border-slate-100 px-5 py-4">
130
  <h2 id="create-campaign-title" className="text-lg font-semibold text-slate-900">
131
  Create Campaign
 
270
  </div>
271
  </div>
272
  ) : step === 2 ? (
273
+ <CampaignSequenceBuilder value={sequenceSteps} onChange={setSequenceSteps} />
 
 
 
274
  ) : step === 3 ? (
275
  <PlaceholderStep
276
  title="Generate Contents"
 
302
  <span className="text-slate-500">File</span>
303
  <p className="font-medium text-slate-800">{prospectFile?.name || '—'}</p>
304
  </div>
305
+ <div className="sm:col-span-2">
306
+ <span className="text-slate-500">Sequence</span>
307
+ <p className="font-medium text-slate-800">
308
+ {sequenceSteps.length} step{sequenceSteps.length !== 1 ? 's' : ''} configured
309
+ </p>
310
+ </div>
311
  </div>
312
  </div>
313
  <div className="flex justify-end">
frontend/src/pages/EmailSequenceGenerator.jsx CHANGED
@@ -1,31 +1,17 @@
1
  import React, { useState } from 'react';
2
- import { Button } from '@/components/ui/button';
3
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
4
  import AppShell from '@/components/layout/AppShell';
5
  import EmailGeneratorTab from '@/components/campaigns/EmailGeneratorTab';
6
  import LinkedinCampaignsTab from '@/components/campaigns/LinkedinCampaignsTab';
7
  import CampaignsDashboardTab from '@/components/campaigns/CampaignsDashboardTab';
8
- import { useGeneratorWorkflow } from '@/context/GeneratorWorkflowContext';
9
 
10
  export default function EmailSequenceGenerator() {
11
- const { resetWorkflow } = useGeneratorWorkflow();
12
  const [activeTab, setActiveTab] = useState('generator');
13
 
14
  return (
15
  <AppShell
16
  title="Campaigns"
17
  subtitle="Create and run email + LinkedIn campaigns from one workspace."
18
- rightContent={
19
- activeTab === 'generator' ? (
20
- <Button
21
- variant="ghost"
22
- onClick={resetWorkflow}
23
- className="text-slate-500 hover:text-slate-700"
24
- >
25
- Start Over
26
- </Button>
27
- ) : undefined
28
- }
29
  >
30
  <Tabs value={activeTab} onValueChange={setActiveTab}>
31
  <TabsList className="mb-6 flex-wrap gap-1">
 
1
  import React, { useState } from 'react';
 
2
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
3
  import AppShell from '@/components/layout/AppShell';
4
  import EmailGeneratorTab from '@/components/campaigns/EmailGeneratorTab';
5
  import LinkedinCampaignsTab from '@/components/campaigns/LinkedinCampaignsTab';
6
  import CampaignsDashboardTab from '@/components/campaigns/CampaignsDashboardTab';
 
7
 
8
  export default function EmailSequenceGenerator() {
 
9
  const [activeTab, setActiveTab] = useState('generator');
10
 
11
  return (
12
  <AppShell
13
  title="Campaigns"
14
  subtitle="Create and run email + LinkedIn campaigns from one workspace."
 
 
 
 
 
 
 
 
 
 
 
15
  >
16
  <Tabs value={activeTab} onValueChange={setActiveTab}>
17
  <TabsList className="mb-6 flex-wrap gap-1">