Review.jsx
1 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 2 import { fetchReview } from '../api'; 3 import MetricCard from '../components/MetricCard'; 4 import DataTable from '../components/DataTable'; 5 6 const REVIEW_COLS = [ 7 { accessorKey: 'domain', header: 'Domain' }, 8 { accessorKey: 'task_type', header: 'Task' }, 9 { 10 accessorKey: 'priority', 11 header: 'Priority', 12 cell: i => { 13 const v = i.getValue(); 14 const color = 15 v === 'high' ? 'text-red-400' : v === 'medium' ? 'text-amber-400' : 'text-slate-400'; 16 return <span className={color}>{v}</span>; 17 }, 18 }, 19 { 20 accessorKey: 'created_at', 21 header: 'Created', 22 cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? 'β', 23 }, 24 ]; 25 26 const OUTREACH_COLS = [ 27 { accessorKey: 'domain', header: 'Domain' }, 28 { 29 accessorKey: 'contact_method', 30 header: 'Channel', 31 cell: i => { 32 const icons = { email: 'βοΈ', sms: 'π±', form: 'π', x: 'π', linkedin: 'πΌ' }; 33 return ( 34 <span> 35 {icons[i.getValue()] ?? ''} {i.getValue()} 36 </span> 37 ); 38 }, 39 }, 40 { accessorKey: 'contact_uri', header: 'Contact' }, 41 { 42 accessorKey: 'grade', 43 header: 'Grade', 44 cell: i => { 45 const g = i.getValue(); 46 const color = !g 47 ? 'text-slate-400' 48 : g.startsWith('A') 49 ? 'text-emerald-400' 50 : g.startsWith('B') 51 ? 'text-sky-400' 52 : g.startsWith('C') 53 ? 'text-amber-400' 54 : 'text-red-400'; 55 return <span className={`font-bold ${color}`}>{g ?? 'β'}</span>; 56 }, 57 }, 58 { accessorKey: 'score', header: 'Score', cell: i => i.getValue()?.toFixed(0) ?? 'β' }, 59 ]; 60 61 const FAILING_COLS = [ 62 { accessorKey: 'domain', header: 'Domain' }, 63 { accessorKey: 'status', header: 'Stage' }, 64 { 65 accessorKey: 'error_message', 66 header: 'Error', 67 cell: i => ( 68 <span className="text-red-400 text-xs truncate max-w-xs block" title={i.getValue() ?? ''}> 69 {i.getValue() ?? 'β'} 70 </span> 71 ), 72 }, 73 { 74 accessorKey: 'recapture_at', 75 header: 'Retry At', 76 cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? 'β', 77 }, 78 ]; 79 80 export default function Review() { 81 const qc = useQueryClient(); 82 const { data, isLoading, error } = useQuery({ queryKey: ['review'], queryFn: fetchReview }); 83 84 if (isLoading) return <div className="text-slate-400 p-8">Loadingβ¦</div>; 85 if (error) return <div className="text-red-400 p-8">Error: {error.message}</div>; 86 87 const d = data ?? {}; 88 const qs = d.queue_stats ?? {}; 89 90 return ( 91 <div className="space-y-6"> 92 {/* Queue metrics */} 93 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 94 <MetricCard 95 label="Pending Reviews" 96 value={qs.pending_reviews ?? 0} 97 variant={(qs.pending_reviews ?? 0) > 10 ? 'warning' : 'default'} 98 /> 99 <MetricCard 100 label="Pending Outreaches" 101 value={qs.pending_outreaches ?? 0} 102 variant={(qs.pending_outreaches ?? 0) > 50 ? 'warning' : 'default'} 103 /> 104 <MetricCard 105 label="Failing Sites" 106 value={qs.failing_sites ?? 0} 107 variant={(qs.failing_sites ?? 0) > 0 ? 'error' : 'success'} 108 /> 109 <MetricCard label="Unread Replies" value={qs.unread_conversations ?? 0} variant="warning" /> 110 </div> 111 112 {/* Human review queue */} 113 {(d.pending_reviews ?? []).length > 0 && ( 114 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 115 <h3 className="text-sm font-medium text-slate-400 mb-3"> 116 Human Review Queue ({(d.pending_reviews ?? []).length}) 117 </h3> 118 <DataTable columns={REVIEW_COLS} data={d.pending_reviews ?? []} maxRows={20} /> 119 </div> 120 )} 121 122 {/* Pending outreaches */} 123 {(d.pending_outreaches ?? []).length > 0 && ( 124 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 125 <div className="flex items-center justify-between mb-3"> 126 <h3 className="text-sm font-medium text-slate-400"> 127 Pending Outreaches ({(d.pending_outreaches ?? []).length}) 128 </h3> 129 <div className="flex gap-2 text-xs text-slate-500"> 130 <span> 131 Use CLI: <code className="bg-slate-900 px-1 rounded">npm run outreach:export</code> 132 </span> 133 </div> 134 </div> 135 <DataTable columns={OUTREACH_COLS} data={d.pending_outreaches ?? []} maxRows={30} /> 136 </div> 137 )} 138 139 {/* Failing sites */} 140 {(d.failing_sites ?? []).length > 0 && ( 141 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 142 <h3 className="text-sm font-medium text-slate-400 mb-3"> 143 Failing Sites ({(d.failing_sites ?? []).length}) 144 </h3> 145 <DataTable columns={FAILING_COLS} data={d.failing_sites ?? []} maxRows={20} /> 146 </div> 147 )} 148 149 {/* Empty state */} 150 {(d.pending_reviews ?? []).length === 0 && 151 (d.pending_outreaches ?? []).length === 0 && 152 (d.failing_sites ?? []).length === 0 && ( 153 <div className="bg-slate-800 rounded-xl p-12 border border-slate-700 text-center"> 154 <p className="text-emerald-400 text-lg font-medium">All clear!</p> 155 <p className="text-slate-500 text-sm mt-1">No items require manual review.</p> 156 </div> 157 )} 158 </div> 159 ); 160 }