Download frontend/src/components/sequences/SequenceViewer.jsx from Seth0330/qualifier: direct link, hf CLI and curl.
- Browser
- Download file 8.25 kB
-
https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/sequences/SequenceViewer.jsx
- Command line
-
hf download hf://spaces/Seth0330/qualifier/frontend/src/components/sequences/SequenceViewer.jsx
-
curl -L -o SequenceViewer.jsx https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/sequences/SequenceViewer.jsx
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 "Generate Sequences" to start</p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |