'use client'; import React, { useState, useTransition } from 'react'; import { Card, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { toast } from 'sonner'; import { approveDetection } from '../actions/approve-detection'; import { rejectDetection } from '../actions/reject-detection'; import { bulkApprove } from '../actions/bulk-approve'; import { bulkReject } from '../actions/bulk-reject'; import { triggerBulkMarketplaceMatchAction } from '@/features/marketplace/actions/trigger-bulk-marketplace-match'; import { resetDetection } from '../actions/edit-detection'; import { EditDetectionDialog } from './edit-detection-dialog'; import { AddProductDialog } from './add-product-dialog'; import { Check, X, ShieldCheck, ShoppingCart, Filter, CheckCircle2, Clock, CheckCircle, ShieldAlert, MousePointerClick, Heart, Sparkles } from 'lucide-react'; import Image from 'next/image'; interface Detection { detection: { id: string; objectName: string; category: string; confidenceScore: number; moderationStatus: 'PENDING' | 'APPROVED' | 'REJECTED'; thumbnailUrl?: string | null; createdAt: Date; }; video: { id: string; title: string; thumbnailUrl: string | null; }; marketplaceMatches: any[]; clickCount: number; interestPledgeCount: number; } interface ModerationQueueProps { initialDetections: Detection[]; userId: string; videos: { id: string; title: string }[]; stats: { pending: number; approved: number; rejected: number }; initialStatus?: 'PENDING' | 'APPROVED' | 'REJECTED'; } export function ModerationQueue({ initialDetections, userId, videos, stats, initialStatus = 'PENDING' }: ModerationQueueProps) { const [detections, setDetections] = useState(initialDetections); const [selectedIds, setSelectedIds] = useState([]); const [filterStatus, setFilterStatus] = useState<'PENDING' | 'APPROVED' | 'REJECTED'>(initialStatus); const [minConfidence, setMinConfidence] = useState(0); const [filterVideo, setFilterVideo] = useState('all'); const [filterHasLink, setFilterHasLink] = useState('all'); // 'all' | 'with-link' | 'without-link' const [sortBy, setSortBy] = useState('date'); // 'date' | 'clicks' | 'interest' | 'confidence' const [isPending, startTransition] = useTransition(); let filteredDetections = detections.filter(d => { // Status and confidence filter if (d.detection.moderationStatus !== filterStatus) return false; if (d.detection.confidenceScore < minConfidence) return false; // Video filter if (filterVideo !== 'all' && d.video.id !== filterVideo) return false; // Affiliate link filter if (filterHasLink === 'with-link' && d.marketplaceMatches.length === 0) return false; if (filterHasLink === 'without-link' && d.marketplaceMatches.length > 0) return false; return true; }); // Sorting filteredDetections = [...filteredDetections].sort((a, b) => { switch (sortBy) { case 'clicks': return b.clickCount - a.clickCount; case 'interest': return b.interestPledgeCount - a.interestPledgeCount; case 'confidence': return b.detection.confidenceScore - a.detection.confidenceScore; case 'date': default: return b.detection.createdAt.getTime() - a.detection.createdAt.getTime(); } }); const toggleSelection = (id: string) => { setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] ); }; const toggleSelectAll = () => { if (selectedIds.length === filteredDetections.length) { setSelectedIds([]); } else { setSelectedIds(filteredDetections.map(d => d.detection.id)); } }; const handleApprove = async (id: string) => { startTransition(async () => { const result = await approveDetection(id); if (result.success) { setDetections(prev => prev.map(d => d.detection.id === id ? { ...d, detection: { ...d.detection, moderationStatus: 'APPROVED' } } : d )); toast.success('Detection approved'); } else { toast.error(result.error); } }); }; const handleReject = async (id: string) => { startTransition(async () => { const result = await rejectDetection(id); if (result.success) { setDetections(prev => prev.map(d => d.detection.id === id ? { ...d, detection: { ...d.detection, moderationStatus: 'REJECTED' } } : d )); toast.success('Detection rejected'); } else { toast.error(result.error); } }); }; const handleBulkApprove = async () => { startTransition(async () => { const result = await bulkApprove(selectedIds); if (result.success) { setDetections(prev => prev.map(d => selectedIds.includes(d.detection.id) ? { ...d, detection: { ...d.detection, moderationStatus: 'APPROVED' } } : d )); setSelectedIds([]); toast.success(`Approved ${result.count} items`); } else { toast.error(result.error); } }); }; const handleBulkReject = async () => { startTransition(async () => { const result = await bulkReject(selectedIds); if (result.success) { setDetections(prev => prev.map(d => selectedIds.includes(d.detection.id) ? { ...d, detection: { ...d.detection, moderationStatus: 'REJECTED' } } : d )); setSelectedIds([]); toast.success(`Rejected ${result.count} items`); } else { toast.error(result.error); } }); }; const handleBulkGenerateLinks = async () => { startTransition(async () => { const result = await triggerBulkMarketplaceMatchAction(selectedIds); if (result.success) { toast.success(`Generation triggered for ${result.count} items. Results will appear in a few moments.`); setSelectedIds([]); } else { toast.error(result.error || 'Failed to trigger link generation'); } }); }; const handleReset = async (id: string) => { startTransition(async () => { const result = await resetDetection(id); if (result.success) { setDetections(prev => prev.map(d => d.detection.id === id ? { ...d, detection: { ...d.detection, moderationStatus: 'PENDING' } } : d )); toast.success('Detection reset to pending'); } else { toast.error(result.error); } }); }; const statCards = [ { status: 'PENDING' as const, title: 'Pending Review', count: stats.pending, icon: , description: 'Items awaiting your approval', activeClasses: 'ring-2 ring-amber-500 bg-amber-500/10 border-amber-500/50' }, { status: 'APPROVED' as const, title: 'Approved', count: stats.approved, icon: , description: 'Successfully added to Vault', activeClasses: 'ring-2 ring-emerald-500 bg-emerald-500/10 border-emerald-500/50' }, { status: 'REJECTED' as const, title: 'Rejected', count: stats.rejected, icon: , description: 'Hidden from your Vault', activeClasses: 'ring-2 ring-rose-500 bg-rose-500/10 border-rose-500/50' }, ]; return (
{statCards.map(({ status, title, count, icon, description, activeClasses }) => ( ))}
setMinConfidence(parseFloat(e.target.value))} className="w-[150px]" />
Showing {filteredDetections.length} of {detections.filter(d => d.detection.moderationStatus === filterStatus).length} items
{ setDetections(prev => [{ detection: { id: newItem.id, objectName: newItem.objectName, category: newItem.category, confidenceScore: 1, moderationStatus: 'PENDING', thumbnailUrl: newItem.thumbnailUrl ?? null, createdAt: new Date(), }, video: { id: newItem.videoId, title: newItem.videoTitle, thumbnailUrl: '', }, marketplaceMatches: newItem.hasMarketplaceMatch ? [{ id: 'new', marketplace: 'amazon', productName: '', affiliateUrl: '' }] : [], clickCount: 0, interestPledgeCount: 0, }, ...prev]); }} />
{selectedIds.length > 0 && (
{selectedIds.length} selected
)}
0} onCheckedChange={toggleSelectAll} />
{filteredDetections.length > 0 ? ( filteredDetections.map((d) => { const primaryMatch = d.marketplaceMatches[0]; const productImage = primaryMatch?.imageUrl; const snapshotImage = d.detection.thumbnailUrl; // Main image priority: Product Image -> Snapshot const mainImage = productImage || snapshotImage; // PiP Logic: Show Snapshot if we have BOTH images and they are different // (i.e. snapshot is not just a fallback copy of product image) const isSnapshotFallback = snapshotImage === productImage; const showPip = snapshotImage && productImage && !isSnapshotFallback; return (
{mainImage ? ( {d.detection.objectName} ) : (
)} {/* Video Snapshot Inset (PiP) */} {showPip && (
In video
IN VIDEO
)}
toggleSelection(d.detection.id)} className="bg-background/80 backdrop-blur-sm" />
{Math.round(d.detection.confidenceScore * 100)}% {d.clickCount > 0 && ( {d.clickCount} )} {d.interestPledgeCount > 0 && ( {d.interestPledgeCount} )}
{d.marketplaceMatches.length > 0 && (
{d.marketplaceMatches.length} matches
)}
{d.detection.objectName} { setDetections(prev => prev.map(item => item.detection.id === d.detection.id ? { ...item, detection: { ...item.detection, ...updated } } : item )); }} />
{d.detection.category} • {d.video.title}
{filterStatus === 'PENDING' ? ( <> ) : (
{filterStatus === 'APPROVED' ? ( <> Approved ) : ( <> Rejected )}
)}
); }) ) : (

Queue Clear!

No {filterStatus.toLowerCase()} items found with current filters.

)}
); }