| 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> |
| ); |
| } |
|
|