| import React, { useState, useEffect } from 'react'; | |
| import { History, CheckCircle2, XCircle, Clock, ExternalLink, Filter } from 'lucide-react'; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { motion } from 'framer-motion'; | |
| export default function RunHistory() { | |
| const [runs, setRuns] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| const [filterStatus, setFilterStatus] = useState('all'); | |
| const [searchQuery, setSearchQuery] = useState(''); | |
| useEffect(() => { | |
| fetchRuns(); | |
| }, []); | |
| const fetchRuns = async () => { | |
| try { | |
| setLoading(true); | |
| const response = await fetch('/api/smartlead-runs'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| setRuns(data); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching runs:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const filteredRuns = runs.filter(run => { | |
| const matchesStatus = filterStatus === 'all' || run.status === filterStatus; | |
| const matchesSearch = searchQuery === '' || | |
| run.campaign_name?.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| run.campaign_id?.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| run.run_id?.toLowerCase().includes(searchQuery.toLowerCase()); | |
| return matchesStatus && matchesSearch; | |
| }); | |
| const getStatusBadge = (status) => { | |
| const variants = { | |
| 'completed': 'bg-green-100 text-green-700', | |
| 'failed': 'bg-red-100 text-red-700', | |
| 'pending': 'bg-yellow-100 text-yellow-700', | |
| 'dry_run_completed': 'bg-blue-100 text-blue-700' | |
| }; | |
| return variants[status] || 'bg-slate-100 text-slate-700'; | |
| }; | |
| const getStatusIcon = (status) => { | |
| if (status === 'completed' || status === 'dry_run_completed') { | |
| return <CheckCircle2 className="h-4 w-4" />; | |
| } else if (status === 'failed') { | |
| return <XCircle className="h-4 w-4" />; | |
| } else { | |
| return <Clock className="h-4 w-4" />; | |
| } | |
| }; | |
| const formatDate = (dateString) => { | |
| if (!dateString) return 'N/A'; | |
| const date = new Date(dateString); | |
| return date.toLocaleString(); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50"> | |
| {/* Header */} | |
| <header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-50"> | |
| <div className="max-w-6xl mx-auto px-6 py-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="h-10 w-10 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600 | |
| flex items-center justify-center shadow-lg shadow-violet-200"> | |
| <History className="h-5 w-5 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="font-bold text-slate-800 text-lg">Run History</h1> | |
| <p className="text-xs text-slate-500">Smartlead campaign push history</p> | |
| </div> | |
| </div> | |
| <Button | |
| variant="outline" | |
| onClick={() => window.location.href = '/'} | |
| className="text-slate-500 hover:text-slate-700" | |
| > | |
| Back to Generator | |
| </Button> | |
| </div> | |
| </div> | |
| </header> | |
| <main className="max-w-6xl mx-auto px-6 py-8"> | |
| {/* Filters */} | |
| <div className="mb-6 flex flex-col sm:flex-row gap-3"> | |
| <div className="relative flex-1"> | |
| <Input | |
| placeholder="Search by campaign name, ID, or run ID..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-10" | |
| /> | |
| </div> | |
| <Select value={filterStatus} onValueChange={setFilterStatus}> | |
| <SelectTrigger className="w-full sm:w-48"> | |
| <Filter className="h-4 w-4 mr-2 text-slate-400" /> | |
| <SelectValue placeholder="Filter by status" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="all">All Status</SelectItem> | |
| <SelectItem value="completed">Completed</SelectItem> | |
| <SelectItem value="failed">Failed</SelectItem> | |
| <SelectItem value="pending">Pending</SelectItem> | |
| <SelectItem value="dry_run_completed">Dry Run</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| {/* Runs List */} | |
| {loading ? ( | |
| <div className="text-center py-16"> | |
| <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-violet-600"></div> | |
| <p className="text-sm text-slate-500 mt-2">Loading runs...</p> | |
| </div> | |
| ) : filteredRuns.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"> | |
| <History className="h-8 w-8 text-slate-300" /> | |
| </div> | |
| <h3 className="text-lg font-semibold text-slate-400 mb-2">No runs found</h3> | |
| <p className="text-sm text-slate-400">Push sequences to Smartlead to see history here</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| {filteredRuns.map((run, index) => ( | |
| <motion.div | |
| key={run.run_id} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.3, delay: index * 0.05 }} | |
| className="rounded-xl border border-slate-200 bg-white p-5 hover:shadow-md transition-shadow" | |
| > | |
| <div className="flex items-start justify-between mb-4"> | |
| <div className="flex items-start gap-4 flex-1"> | |
| <div className={`rounded-lg p-2 ${getStatusBadge(run.status)}`}> | |
| {getStatusIcon(run.status)} | |
| </div> | |
| <div className="flex-1"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <h4 className="font-semibold text-slate-800"> | |
| {run.campaign_name || 'Unnamed Campaign'} | |
| </h4> | |
| <Badge className={getStatusBadge(run.status)}> | |
| {run.status} | |
| </Badge> | |
| {run.dry_run && ( | |
| <Badge variant="outline" className="text-xs"> | |
| Dry Run | |
| </Badge> | |
| )} | |
| </div> | |
| <div className="flex flex-wrap items-center gap-4 text-sm text-slate-500"> | |
| {run.campaign_id && ( | |
| <span className="font-mono text-xs"> | |
| Campaign: {run.campaign_id} | |
| </span> | |
| )} | |
| <span> | |
| {run.mode === 'new' ? 'New Campaign' : 'Existing Campaign'} | |
| </span> | |
| <span> | |
| {run.steps_count} {run.steps_count === 1 ? 'step' : 'steps'} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="text-right text-xs text-slate-500"> | |
| <div>{formatDate(run.created_at)}</div> | |
| {run.completed_at && ( | |
| <div className="text-slate-400 mt-1"> | |
| Completed: {formatDate(run.completed_at)} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Stats */} | |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 pt-4 border-t border-slate-100"> | |
| <div> | |
| <div className="text-xs text-slate-500 mb-1">Total Leads</div> | |
| <div className="text-lg font-semibold text-slate-800">{run.total_leads}</div> | |
| </div> | |
| <div> | |
| <div className="text-xs text-slate-500 mb-1">Added</div> | |
| <div className="text-lg font-semibold text-green-600">{run.added_leads}</div> | |
| </div> | |
| {run.skipped_leads > 0 && ( | |
| <div> | |
| <div className="text-xs text-slate-500 mb-1">Skipped</div> | |
| <div className="text-lg font-semibold text-yellow-600">{run.skipped_leads}</div> | |
| </div> | |
| )} | |
| {run.failed_leads > 0 && ( | |
| <div> | |
| <div className="text-xs text-slate-500 mb-1">Failed</div> | |
| <div className="text-lg font-semibold text-red-600">{run.failed_leads}</div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Run ID */} | |
| <div className="mt-3 pt-3 border-t border-slate-100"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs text-slate-400 font-mono"> | |
| Run ID: {run.run_id} | |
| </span> | |
| {run.campaign_id && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => { | |
| // TODO: Open Smartlead campaign URL if available | |
| window.open(`https://app.smartlead.ai/campaigns/${run.campaign_id}`, '_blank'); | |
| }} | |
| className="h-7 text-xs" | |
| > | |
| <ExternalLink className="h-3 w-3 mr-1" /> | |
| View in Smartlead | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </div> | |
| )} | |
| </main> | |
| </div> | |
| ); | |
| } | |