Download frontend/src/components/campaigns/EmailGeneratorTab.jsx from Seth0330/EMAILOUT: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/components/campaigns/EmailGeneratorTab.jsx
- Command line
-
hf download hf://spaces/Seth0330/EMAILOUT/frontend/src/components/campaigns/EmailGeneratorTab.jsx
-
curl -L -o EmailGeneratorTab.jsx https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/components/campaigns/EmailGeneratorTab.jsx
12 kB
| 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> | |
| ); | |
| } | |