qualifier / frontend /src /components /sequences /SequenceViewer.jsx
Sethu0330's picture
Add FTL RFQ Qualifier agent + full SALES CRM app for HF Spaces testing
005df22 unverified
Raw History Blame Contribute Delete
8.25 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';
import { apiFetch } from '@/lib/api';
import { useGeneratorWorkflow } from '@/context/GeneratorWorkflowContext';
export default function SequenceViewer() {
const {
isGenerating,
generationComplete,
selectedProducts,
uploadedFile,
includeLinkedin,
genPhase,
contacts,
sequences,
progress,
contactCount,
} = useGeneratorWorkflow();
const [searchQuery, setSearchQuery] = useState('');
const [filterProduct, setFilterProduct] = useState('all');
const [displayedCount, setDisplayedCount] = useState(50);
useEffect(() => {
setDisplayedCount(50);
}, [searchQuery, filterProduct]);
const handleDownload = async () => {
if (!uploadedFile?.fileId) return;
try {
const response = await apiFetch(`/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;
});
const displayedContacts = filteredContacts.slice(0, displayedCount);
const hasMore = filteredContacts.length > displayedCount;
const loadMore = () => {
setDisplayedCount((prev) => Math.min(prev + 50, filteredContacts.length));
};
const showProgress = isGenerating || !generationComplete;
const phaseLabel =
includeLinkedin && genPhase === 'linkedin' && isGenerating
? 'LinkedIn sequences'
: 'Email sequences';
return (
<div className="w-full">
<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">
{generationComplete ? (
<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">
{generationComplete ? 'Generation complete!' : `Generating ${phaseLabel}…`}
</h3>
<p className="text-sm text-slate-500">
{contacts.length} contacts · {sequences.length} generated rows
{contactCount ? ` · ~${contactCount} contacts in file` : ''}
</p>
</div>
</div>
{generationComplete && (
<Button onClick={handleDownload} className="bg-green-600 hover:bg-green-700">
<Download className="h-4 w-4 mr-2" />
Download CSV
</Button>
)}
</div>
{showProgress ? <Progress value={progress} className="h-2" /> : null}
</div>
{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>
)}
<div className="space-y-3 max-h-[600px] overflow-y-auto pr-2 custom-scrollbar">
<AnimatePresence>
{displayedContacts.map((contact, index) => (
<SequenceCard
key={contact.id || `${contact.firstName}-${contact.lastName}-${contact.email}`}
contact={contact}
index={index}
/>
))}
</AnimatePresence>
{hasMore && (
<div className="text-center py-4">
<Button variant="outline" onClick={loadMore} className="mx-auto">
Load More ({filteredContacts.length - displayedCount} remaining)
</Button>
</div>
)}
{filteredContacts.length > 0 && (
<div className="text-center py-2 text-sm text-slate-500">
Showing {displayedContacts.length} of {filteredContacts.length} contacts
</div>
)}
</div>
{!isGenerating && contacts.length === 0 && sequences.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 &quot;Generate Sequences&quot; to start</p>
</div>
)}
</div>
);
}