import React, { useState, useEffect, useRef } 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'; function applySequenceToContacts(prev, sequence, contactCount, setProgress) { 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 }] }]; } const progressValue = contactCount > 0 ? Math.min(100, Math.max(0, (updatedContacts.length / contactCount) * 100)) : 0; setProgress(progressValue); return updatedContacts; } export default function SequenceViewer({ isGenerating, generationRunId, 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); const [displayedCount, setDisplayedCount] = useState(50); const [reconnectKey, setReconnectKey] = useState(0); const prevRunIdRef = useRef(null); useEffect(() => { if (!isGenerating || !uploadedFile?.fileId) return; const isNewRun = prevRunIdRef.current !== generationRunId; if (isNewRun) { prevRunIdRef.current = generationRunId; setSequences([]); setContacts([]); setProgress(0); setIsComplete(false); } const reset = isNewRun ? 1 : 0; const url = `/api/generate-sequences?file_id=${encodeURIComponent(uploadedFile.fileId)}&reset=${reset}`; const eventSource = new EventSource(url, { withCredentials: false }); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'sequence') { const seq = data.sequence; setSequences(prev => { if (prev.some(s => s.id === seq.id && s.emailNumber === seq.emailNumber)) return prev; return [...prev, seq]; }); setContacts(prev => { const existing = prev.find(c => c.email === seq.email); if (existing?.emails.some(e => e.emailNumber === seq.emailNumber)) return prev; return applySequenceToContacts(prev, seq, contactCount, setProgress); }); } 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 (err) { console.error('Error parsing SSE data:', err); } }; eventSource.onerror = () => { eventSource.close(); if (!isComplete) setReconnectKey(k => k + 1); }; return () => eventSource.close(); }, [isGenerating, uploadedFile?.fileId, generationRunId, contactCount, reconnectKey, onComplete, isComplete]); useEffect(() => { if (!isGenerating || !uploadedFile?.fileId || reconnectKey === 0) return; let cancelled = false; (async () => { try { const [statusRes, seqRes] = await Promise.all([ fetch(`/api/generation-status?file_id=${encodeURIComponent(uploadedFile.fileId)}`), fetch(`/api/sequences?file_id=${encodeURIComponent(uploadedFile.fileId)}`) ]); if (cancelled) return; if (statusRes.ok && seqRes.ok) { const status = await statusRes.json(); const { sequences: list } = await seqRes.json(); if (status.is_complete) { setIsComplete(true); onComplete?.(); } if (list?.length > 0) { const byContact = new Map(); list.forEach(seq => { const key = seq.email; if (!byContact.has(key)) { byContact.set(key, { id: seq.id, firstName: seq.firstName, lastName: seq.lastName, email: seq.email, company: seq.company, title: seq.title, product: seq.product, emails: [] }); } byContact.get(key).emails.push({ emailNumber: seq.emailNumber, subject: seq.subject, emailContent: seq.emailContent }); }); const arr = [...byContact.values()]; arr.sort((a, b) => (a.id || 0) - (b.id || 0)); setSequences(list); setContacts(arr); const p = status.total_contacts > 0 ? Math.min(100, (arr.length / status.total_contacts) * 100) : 0; setProgress(p); } } } catch (e) { if (!cancelled) console.error('Reconnect fetch error:', e); } })(); return () => { cancelled = true; }; }, [reconnectKey, isGenerating, uploadedFile?.fileId, contactCount, onComplete]); useEffect(() => { if (!isGenerating || !uploadedFile?.fileId) return; const onVisible = () => { if (document.visibilityState === 'visible') setReconnectKey(k => k + 1); }; document.addEventListener('visibilitychange', onVisible); return () => document.removeEventListener('visibilitychange', onVisible); }, [isGenerating, uploadedFile?.fileId]); 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_fixed.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; }); // Reset pagination when search/filter changes useEffect(() => { setDisplayedCount(50); }, [searchQuery, filterProduct]); // Pagination: only show first N contacts to avoid browser performance issues const displayedContacts = filteredContacts.slice(0, displayedCount); const hasMore = filteredContacts.length > displayedCount; const loadMore = () => { setDisplayedCount(prev => Math.min(prev + 50, filteredContacts.length)); }; return (
{/* Progress Header */}
{isComplete ? (
) : (
)}

{isComplete ? 'Generation Complete!' : 'Generating Email Sequences...'}

{contacts.length} of {contactCount} contacts, {sequences.length} total emails generated

{isComplete && ( )}
{/* Filters */} {sequences.length > 0 && (
setSearchQuery(e.target.value)} className="pl-10" />
)} {/* Sequence List - Optimized for high volume with pagination */}
{displayedContacts.map((contact, index) => ( ))} {hasMore && (
)} {filteredContacts.length > 0 && (
Showing {displayedContacts.length} of {filteredContacts.length} contacts
)}
{/* Empty State */} {!isGenerating && contacts.length === 0 && (

No sequences yet

Click "Generate Sequences" to start

)}
); }