EMAILOUT / frontend /src /pages /EmailSequenceGenerator.jsx
Seth
update
952e292
import React, { useState, useRef } from 'react';
import { Sparkles, ArrowRight, ArrowLeft, Mail, Zap } 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';
export default function EmailSequenceGenerator() {
const [step, setStep] = useState(1);
const [uploadedFile, setUploadedFile] = useState(null);
const [selectedProducts, setSelectedProducts] = useState([]);
const [prompts, setPrompts] = useState({});
const [isGenerating, setIsGenerating] = useState(false);
const [generationComplete, setGenerationComplete] = useState(false);
const [generationRunId, setGenerationRunId] = useState(0);
const generateButtonRef = useRef(null);
const canProceedToStep2 = uploadedFile && selectedProducts.length > 0;
const canProceedToStep3 = Object.keys(prompts).length > 0;
const scrollToGenerateButton = () => {
if (generateButtonRef.current) {
generateButtonRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
};
const handleGenerate = async () => {
if (!uploadedFile?.fileId || selectedProducts.length === 0 || Object.keys(prompts).length === 0) {
alert('Please complete all steps before generating sequences.');
return;
}
// Save prompts to backend first, then start generation (avoids "No prompts found" race)
try {
const res = await fetch('/api/save-prompts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_id: uploadedFile.fileId,
prompts: prompts,
products: selectedProducts.map(p => p.name)
})
});
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;
}
setGenerationRunId((r) => r + 1);
setStep(3);
setIsGenerating(true);
};
const handleGenerationComplete = () => {
setIsGenerating(false);
setGenerationComplete(true);
};
const handleReset = () => {
setStep(1);
setUploadedFile(null);
setSelectedProducts([]);
setPrompts({});
setIsGenerating(false);
setGenerationComplete(false);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
{/* Header */}
<header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-50">
<div className="max-w-6xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600
flex items-center justify-center shadow-lg shadow-violet-200">
<Zap className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="font-bold text-slate-800 text-lg">SequenceAI</h1>
<p className="text-xs text-slate-500">Personalized Email Outreach</p>
</div>
</div>
{step > 1 && (
<Button
variant="ghost"
onClick={handleReset}
className="text-slate-500 hover:text-slate-700"
>
Start Over
</Button>
)}
</div>
</div>
</header>
<main className="max-w-6xl mx-auto px-6 py-8">
{/* Progress Steps */}
<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: Upload & Product Selection */}
{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 the products for your outreach campaign
</p>
</div>
<UploadStep
onFileUploaded={setUploadedFile}
uploadedFile={uploadedFile}
onRemoveFile={() => setUploadedFile(null)}
/>
{uploadedFile && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<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: Prompt Configuration */}
{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 Email Templates
</h2>
<p className="text-slate-500">
Edit the prompt templates for each product. The AI will personalize these for each contact.
</p>
</div>
<PromptEditor
selectedProducts={selectedProducts}
prompts={prompts}
onPromptsChange={setPrompts}
onSaveComplete={scrollToGenerateButton}
/>
<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: Generation & Results */}
{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 Personalized Emails'}
</h2>
<p className="text-slate-500">
{generationComplete
? 'Review your sequences below and download when ready'
: 'Our AI is crafting personalized emails for each contact'
}
</p>
</div>
<SequenceViewer
isGenerating={isGenerating}
generationRunId={generationRunId}
contactCount={uploadedFile?.contactCount || 50}
selectedProducts={selectedProducts}
uploadedFile={uploadedFile}
prompts={prompts}
onComplete={handleGenerationComplete}
/>
{!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>
</main>
{/* Footer */}
<footer className="border-t border-slate-100 mt-16">
<div className="max-w-6xl mx-auto px-6 py-6">
<p className="text-center text-sm text-slate-400">
Powered by AI • Export ready for Outreaches, Smartlead, and more
</p>
</div>
</footer>
{/* Custom Scrollbar Styles */}
<style>{`
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
`}</style>
</div>
);
}