EMAILOUT / frontend /src /components /sequences /SequenceViewer.jsx
Seth
update
5940a39
raw
history blame
11.1 kB
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);
// Start streaming sequences from API
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]);
// Group sequences by contact (sequence.id)
setContacts(prev => {
const existingContact = prev.find(c =>
c.firstName === sequence.firstName &&
c.lastName === sequence.lastName &&
c.email === sequence.email
);
let updatedContacts;
if (existingContact) {
// Add email to existing contact
existingContact.emails.push({
emailNumber: sequence.emailNumber || existingContact.emails.length + 1,
subject: sequence.subject,
emailContent: sequence.emailContent
});
updatedContacts = [...prev];
} else {
// Create new contact
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
}]
}];
}
// Update progress based on unique contacts
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>
);
}