EMAILOUT / frontend /src /components /campaigns /EmailGeneratorTab.jsx
Seth
update
84ca4b1
import React, { useMemo, useRef } from 'react';
import { Sparkles, ArrowRight, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { motion, AnimatePresence } from 'framer-motion';
import UploadStep from '@/components/upload/UploadStep';
import ProductSelector from '@/components/products/ProductSelector';
import PromptEditor from '@/components/prompts/PromptEditor';
import SequenceViewer from '@/components/sequences/SequenceViewer';
import { apiFetch } from '@/lib/api';
import { useGeneratorWorkflow } from '@/context/GeneratorWorkflowContext';
export default function EmailGeneratorTab() {
const {
step,
setStep,
uploadedFile,
setUploadedFile,
selectedProducts,
setSelectedProducts,
prompts,
setPrompts,
linkedinPrompts,
setLinkedinPrompts,
includeLinkedin,
setIncludeLinkedin,
isGenerating,
generationComplete,
beginGeneration,
} = useGeneratorWorkflow();
const generateButtonRef = useRef(null);
const canProceedToStep2 = uploadedFile && selectedProducts.length > 0;
const canProceedToStep3 = useMemo(() => {
const emailOk = selectedProducts.length > 0 && selectedProducts.every((p) => (prompts[p.name] || '').trim());
if (!emailOk) return false;
if (!includeLinkedin) return true;
return selectedProducts.every((p) => (linkedinPrompts[p.name] || '').trim());
}, [selectedProducts, prompts, linkedinPrompts, includeLinkedin]);
const scrollToGenerateButton = () => {
if (generateButtonRef.current) {
generateButtonRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
const handleGenerate = async () => {
if (!uploadedFile?.fileId || !canProceedToStep3) {
alert('Please complete all steps before generating sequences.');
return;
}
try {
const res = await apiFetch('/api/save-prompts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_id: uploadedFile.fileId,
prompts,
products: selectedProducts.map((p) => p.name),
linkedin_prompts: includeLinkedin ? linkedinPrompts : {},
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || res.statusText);
}
} catch (error) {
console.error('Error saving prompts:', error);
alert('Failed to save templates. Please try again.');
return;
}
setStep(3);
beginGeneration(true);
};
return (
<div>
<div className="mb-10">
<div className="flex items-center justify-center gap-4">
{[
{ num: 1, label: 'Upload & Select' },
{ num: 2, label: 'Configure Prompts' },
{ num: 3, label: 'Generate & Export' },
].map((s, idx) => (
<React.Fragment key={s.num}>
<div className="flex items-center gap-2">
<div
className={`h-8 w-8 rounded-full flex items-center justify-center text-sm font-semibold transition-all duration-300 ${
step >= s.num
? 'bg-violet-600 text-white shadow-lg shadow-violet-200'
: 'bg-slate-100 text-slate-400'
}`}
>
{s.num}
</div>
<span
className={`text-sm font-medium hidden sm:block ${
step >= s.num ? 'text-slate-800' : 'text-slate-400'
}`}
>
{s.label}
</span>
</div>
{idx < 2 && (
<div
className={`h-0.5 w-12 rounded-full transition-colors duration-300 ${
step > s.num ? 'bg-violet-600' : 'bg-slate-200'
}`}
/>
)}
</React.Fragment>
))}
</div>
</div>
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
className="space-y-8"
>
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-slate-800 mb-2">Upload Your Contacts</h2>
<p className="text-slate-500">
Import your Apollo CSV and select products for your campaign
</p>
</div>
<UploadStep
onFileUploaded={setUploadedFile}
uploadedFile={uploadedFile}
onRemoveFile={() => setUploadedFile(null)}
/>
{uploadedFile && (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<ProductSelector
selectedProducts={selectedProducts}
onProductsChange={setSelectedProducts}
/>
</motion.div>
)}
<div className="flex justify-end pt-4">
<Button
onClick={() => setStep(2)}
disabled={!canProceedToStep2}
className="bg-violet-600 hover:bg-violet-700 px-6"
>
Continue to Prompts
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
</div>
</motion.div>
)}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
className="space-y-8"
>
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-slate-800 mb-2">Customize Your Prompts</h2>
<p className="text-slate-500">Edit templates before generation.</p>
</div>
<PromptEditor
selectedProducts={selectedProducts}
prompts={prompts}
onPromptsChange={setPrompts}
onSaveComplete={scrollToGenerateButton}
variant="email"
/>
<label className="flex cursor-pointer items-start gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
<input
type="checkbox"
className="mt-1 h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500"
checked={includeLinkedin}
onChange={(e) => setIncludeLinkedin(e.target.checked)}
/>
<span>
<span className="font-medium text-slate-800">
Also generate LinkedIn sequences for the same contacts
</span>
</span>
</label>
{includeLinkedin ? (
<div className="space-y-2">
<h3 className="text-lg font-semibold text-slate-800">LinkedIn prompts</h3>
<PromptEditor
selectedProducts={selectedProducts}
prompts={linkedinPrompts}
onPromptsChange={setLinkedinPrompts}
onSaveComplete={scrollToGenerateButton}
variant="linkedin"
/>
</div>
) : null}
<div className="flex justify-between pt-4">
<Button variant="outline" onClick={() => setStep(1)} className="px-6">
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<Button
ref={generateButtonRef}
onClick={handleGenerate}
disabled={!canProceedToStep3}
className="bg-gradient-to-r from-violet-600 to-purple-600 hover:from-violet-700 hover:to-purple-700 px-8 shadow-lg shadow-violet-200"
>
<Sparkles className="h-4 w-4 mr-2" />
Generate Sequences
</Button>
</div>
</motion.div>
)}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
className="space-y-8"
>
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-slate-800 mb-2">
{generationComplete ? 'Your Sequences Are Ready!' : 'Generating…'}
</h2>
<p className="text-slate-500">
{generationComplete
? 'Review and export below.'
: 'You can navigate away; generation continues in background.'}
</p>
</div>
<SequenceViewer />
{!isGenerating && (
<div className="flex justify-start pt-4">
<Button variant="outline" onClick={() => setStep(2)} className="px-6">
<ArrowLeft className="h-4 w-4 mr-2" />
Edit Templates
</Button>
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
}