| import React, { useState, useEffect } from 'react'; |
| import { Download, Mail, Loader2, CheckCircle2, Search, Filter } from 'lucide-react'; |
| import { Button } from "@/components/ui/button"; |
| import { Input } from "@/components/ui/input"; |
| import { Progress } from "@/components/ui/progress"; |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; |
| import { motion, AnimatePresence } from 'framer-motion'; |
| import SequenceCard from './SequenceCard'; |
|
|
| export default function SequenceViewer({ isGenerating, contactCount, selectedProducts, uploadedFile, prompts, onComplete }) { |
| const [sequences, setSequences] = useState([]); |
| const [contacts, setContacts] = useState([]); |
| const [progress, setProgress] = useState(0); |
| const [searchQuery, setSearchQuery] = useState(''); |
| const [filterProduct, setFilterProduct] = useState('all'); |
| const [isComplete, setIsComplete] = useState(false); |
|
|
| useEffect(() => { |
| if (isGenerating && uploadedFile?.fileId) { |
| setSequences([]); |
| setContacts([]); |
| setProgress(0); |
| setIsComplete(false); |
| |
| |
| const eventSource = new EventSource(`/api/generate-sequences?file_id=${uploadedFile.fileId}`, { |
| withCredentials: false |
| }); |
| |
| eventSource.onmessage = (event) => { |
| try { |
| const data = JSON.parse(event.data); |
| |
| if (data.type === 'sequence') { |
| const sequence = data.sequence; |
| setSequences(prev => [...prev, sequence]); |
| |
| |
| setContacts(prev => { |
| const existingContact = prev.find(c => |
| c.firstName === sequence.firstName && |
| c.lastName === sequence.lastName && |
| c.email === sequence.email |
| ); |
| |
| let updatedContacts; |
| if (existingContact) { |
| |
| existingContact.emails.push({ |
| emailNumber: sequence.emailNumber || existingContact.emails.length + 1, |
| subject: sequence.subject, |
| emailContent: sequence.emailContent |
| }); |
| updatedContacts = [...prev]; |
| } else { |
| |
| updatedContacts = [...prev, { |
| id: sequence.id, |
| firstName: sequence.firstName, |
| lastName: sequence.lastName, |
| email: sequence.email, |
| company: sequence.company, |
| title: sequence.title, |
| product: sequence.product, |
| emails: [{ |
| emailNumber: sequence.emailNumber || 1, |
| subject: sequence.subject, |
| emailContent: sequence.emailContent |
| }] |
| }]; |
| } |
| |
| |
| setProgress((updatedContacts.length / contactCount) * 100); |
| |
| return updatedContacts; |
| }); |
| } else if (data.type === 'progress') { |
| setProgress(data.progress); |
| } else if (data.type === 'complete') { |
| setIsComplete(true); |
| onComplete?.(); |
| eventSource.close(); |
| } else if (data.type === 'error') { |
| console.error('Generation error:', data.error); |
| alert('Error generating sequences: ' + data.error); |
| eventSource.close(); |
| } |
| } catch (error) { |
| console.error('Error parsing SSE data:', error); |
| } |
| }; |
| |
| eventSource.onerror = (error) => { |
| console.error('SSE error:', error); |
| eventSource.close(); |
| if (!isComplete) { |
| alert('Connection error. Please try again.'); |
| } |
| }; |
| |
| return () => { |
| eventSource.close(); |
| }; |
| } |
| }, [isGenerating, uploadedFile, contactCount, selectedProducts, prompts, onComplete, isComplete]); |
|
|
| const handleDownload = async () => { |
| try { |
| const response = await fetch(`/api/download-sequences?file_id=${uploadedFile.fileId}`); |
| if (response.ok) { |
| const blob = await response.blob(); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'email_sequences.csv'; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } else { |
| alert('Failed to download CSV. Please try again.'); |
| } |
| } catch (error) { |
| console.error('Download error:', error); |
| alert('Error downloading CSV. Please try again.'); |
| } |
| }; |
|
|
| const filteredContacts = contacts.filter(contact => { |
| const matchesSearch = searchQuery === '' || |
| contact.firstName?.toLowerCase().includes(searchQuery.toLowerCase()) || |
| contact.lastName?.toLowerCase().includes(searchQuery.toLowerCase()) || |
| contact.company?.toLowerCase().includes(searchQuery.toLowerCase()) || |
| contact.email?.toLowerCase().includes(searchQuery.toLowerCase()); |
| |
| const matchesFilter = filterProduct === 'all' || contact.product === filterProduct; |
| |
| return matchesSearch && matchesFilter; |
| }); |
|
|
| return ( |
| <div className="w-full"> |
| {/* Progress Header */} |
| <div className="rounded-2xl border border-slate-200 bg-white p-6 mb-6"> |
| <div className="flex items-center justify-between mb-4"> |
| <div className="flex items-center gap-3"> |
| {isComplete ? ( |
| <div className="rounded-xl bg-green-100 p-3"> |
| <CheckCircle2 className="h-6 w-6 text-green-600" /> |
| </div> |
| ) : ( |
| <div className="rounded-xl bg-violet-100 p-3"> |
| <Loader2 className="h-6 w-6 text-violet-600 animate-spin" /> |
| </div> |
| )} |
| <div> |
| <h3 className="font-semibold text-slate-800"> |
| {isComplete ? 'Generation Complete!' : 'Generating Email Sequences...'} |
| </h3> |
| <p className="text-sm text-slate-500"> |
| {contacts.length} of {contactCount} contacts, {sequences.length} total emails generated |
| </p> |
| </div> |
| </div> |
| {isComplete && ( |
| <Button |
| onClick={handleDownload} |
| className="bg-green-600 hover:bg-green-700" |
| > |
| <Download className="h-4 w-4 mr-2" /> |
| Download CSV for Klenty |
| </Button> |
| )} |
| </div> |
| <Progress value={progress} className="h-2" /> |
| </div> |
| |
| {/* Filters */} |
| {sequences.length > 0 && ( |
| <motion.div |
| initial={{ opacity: 0, y: -10 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="flex flex-col sm:flex-row gap-3 mb-6" |
| > |
| <div className="relative flex-1"> |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" /> |
| <Input |
| placeholder="Search contacts..." |
| value={searchQuery} |
| onChange={(e) => setSearchQuery(e.target.value)} |
| className="pl-10" |
| /> |
| </div> |
| <Select value={filterProduct} onValueChange={setFilterProduct}> |
| <SelectTrigger className="w-full sm:w-48"> |
| <Filter className="h-4 w-4 mr-2 text-slate-400" /> |
| <SelectValue placeholder="Filter by product" /> |
| </SelectTrigger> |
| <SelectContent> |
| <SelectItem value="all">All Products</SelectItem> |
| {selectedProducts.map(product => ( |
| <SelectItem key={product.id} value={product.name}> |
| {product.name} |
| </SelectItem> |
| ))} |
| </SelectContent> |
| </Select> |
| </motion.div> |
| )} |
| |
| {/* Sequence List */} |
| <div className="space-y-3 max-h-[600px] overflow-y-auto pr-2 custom-scrollbar"> |
| <AnimatePresence> |
| {filteredContacts.map((contact, index) => ( |
| <SequenceCard key={contact.id || `${contact.firstName}-${contact.lastName}-${contact.email}`} contact={contact} index={index} /> |
| ))} |
| </AnimatePresence> |
| </div> |
| |
| {/* Empty State */} |
| {!isGenerating && contacts.length === 0 && ( |
| <div className="text-center py-16"> |
| <div className="mx-auto w-16 h-16 rounded-2xl bg-slate-100 flex items-center justify-center mb-4"> |
| <Mail className="h-8 w-8 text-slate-300" /> |
| </div> |
| <h3 className="text-lg font-semibold text-slate-400 mb-2">No sequences yet</h3> |
| <p className="text-sm text-slate-400">Click "Generate Sequences" to start</p> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|