import React, { useState, useEffect, useCallback } from 'react'; import { adminBinaryRequest, adminData, adminRequest } from '@/lib/adminApi'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card } from '@/components/ui/card'; import { PaginationControls } from '@/components/ui/pagination'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { CheckCircle, Eye, RefreshCw, MessageSquare, Sparkles, Flag, AlertTriangle, XCircle } from 'lucide-react'; import { toast } from 'sonner'; import { format } from 'date-fns'; import { describeImage } from '@/lib/ai/analyzers/media'; interface FlaggedMessage { id: string; created_at: string; user_id: string; match_id: string | null; moderation_status: 'flagged' | 'safe' | 'unmoderated'; flag_reason: string | null; content?: string; // Encrypted usually media_path?: string; media_type?: string; } interface UserReport { id: string; message_id: string; reporter_id: string; reason: string; status: 'pending' | 'reviewed' | 'dismissed'; created_at: string; message: { id: string; user_id: string; match_id: string | null; content: string | null; media_path: string | null; media_type: string | null; } | null; reporter_profile: { name: string | null; email: string | null; } | null; } const REASON_LABELS: Record = { harassment: 'Harassment', spam: 'Spam', inappropriate_content: 'Inappropriate Content', other: 'Other', }; export function FlaggedMessagesPage() { const navigate = useNavigate(); // AI Flagged state const [aiMessages, setAiMessages] = useState([]); const [aiLoading, setAiLoading] = useState(true); const [aiPage, setAiPage] = useState(1); const [aiPageSize, setAiPageSize] = useState(25); const [aiTotal, setAiTotal] = useState(0); // User Reports state const [userReports, setUserReports] = useState([]); const [reportsLoading, setReportsLoading] = useState(true); const [reportsPage, setReportsPage] = useState(1); const [reportsPageSize, setReportsPageSize] = useState(25); const [reportsTotal, setReportsTotal] = useState(0); // Shared state const [selectedMessage, setSelectedMessage] = useState(null); const [selectedReport, setSelectedReport] = useState(null); const [decryptedContent, setDecryptedContent] = useState<{ text: string | null; mediaUrl: string | null }>({ text: null, mediaUrl: null }); const [decrypting, setDecrypting] = useState(false); const [processing, setProcessing] = useState(null); const [imageDescription, setImageDescription] = useState(null); const [describing, setDescribing] = useState(false); const [inlineDescriptions, setInlineDescriptions] = useState>({}); const [describingInline, setDescribingInline] = useState(null); // Fetch AI flagged messages const fetchAiMessages = useCallback(async () => { setAiLoading(true); try { const from = (aiPage - 1) * aiPageSize; const to = from + aiPageSize - 1; const { data, error, count } = await adminData .from('messages') .select('id, created_at, user_id, match_id, moderation_status, flag_reason, content, media_path, media_type', { count: 'exact' }) .eq('moderation_status', 'flagged') .order('created_at', { ascending: false }) .range(from, to); if (error) throw error; setAiMessages(data || []); setAiTotal(count || 0); } catch (error) { console.error('Failed to load flagged messages:', error); toast.error('Failed to load messages'); } finally { setAiLoading(false); } }, [aiPage, aiPageSize]); // Fetch user reports const fetchUserReports = useCallback(async () => { setReportsLoading(true); try { const from = (reportsPage - 1) * reportsPageSize; const to = from + reportsPageSize - 1; const { data, error, count } = await adminData .from('message_reports') .select(` id, message_id, reporter_id, reason, status, created_at, message:messages(id, user_id, match_id, content, media_path, media_type), reporter_profile:profiles!reporter_id(name, email) `, { count: 'exact' }) .eq('status', 'pending') .order('created_at', { ascending: false }) .range(from, to); if (error) throw error; setUserReports((data || []) as unknown as UserReport[]); setReportsTotal(count || 0); } catch (error) { console.error('Failed to load user reports:', error); toast.error('Failed to load reports'); } finally { setReportsLoading(false); } }, [reportsPage, reportsPageSize]); useEffect(() => { fetchAiMessages(); fetchUserReports(); }, [fetchAiMessages, fetchUserReports]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(aiTotal / aiPageSize)); if (aiPage > totalPages) { setAiPage(totalPages); } }, [aiPage, aiPageSize, aiTotal]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(reportsTotal / reportsPageSize)); if (reportsPage > totalPages) { setReportsPage(totalPages); } }, [reportsPage, reportsPageSize, reportsTotal]); const fetchDecryptedMedia = async (messageId: string) => { try { const blob = await adminBinaryRequest('/v1/admin/actions/decrypt-media', { method: 'POST', body: { messageId }, }); return URL.createObjectURL(blob); } catch (e) { console.error(e); return null; } }; const handleViewAiMessage = async (message: FlaggedMessage) => { setSelectedMessage(message); setSelectedReport(null); setDecrypting(true); setDecryptedContent({ text: null, mediaUrl: null }); setImageDescription(null); try { const textData = await adminRequest<{ content: string | null }>('/v1/admin/actions/decrypt-message', { method: 'POST', body: { messageId: message.id }, }); let mediaUrl = null; if (message.media_path) { mediaUrl = await fetchDecryptedMedia(message.id); } setDecryptedContent({ text: textData.content, mediaUrl }); } catch (e) { toast.error("Error loading content"); } finally { setDecrypting(false); } }; const handleViewReport = async (report: UserReport) => { if (!report.message) { toast.error('Message no longer exists'); return; } setSelectedReport(report); setSelectedMessage(null); setDecrypting(true); setDecryptedContent({ text: null, mediaUrl: null }); setImageDescription(null); try { const textData = await adminRequest<{ content: string | null }>('/v1/admin/actions/decrypt-message', { method: 'POST', body: { messageId: report.message.id }, }); let mediaUrl = null; if (report.message.media_path) { mediaUrl = await fetchDecryptedMedia(report.message.id); } setDecryptedContent({ text: textData.content, mediaUrl }); } catch (e) { toast.error("Error loading content"); } finally { setDecrypting(false); } }; const handleDescribeImage = async () => { if (!decryptedContent.mediaUrl) return; setDescribing(true); try { const description = await describeImage(decryptedContent.mediaUrl); setImageDescription(description); } catch (error) { console.error('Failed to describe image:', error); toast.error('Failed to describe image'); } finally { setDescribing(false); } }; const handleDescribeInline = async (message: FlaggedMessage) => { if (!message.media_path || message.media_type === 'video') return; setDescribingInline(message.id); try { const blob = await adminBinaryRequest('/v1/admin/actions/decrypt-media', { method: 'POST', body: { messageId: message.id }, }); const base64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const result = reader.result as string; const base64Data = result.split(',')[1]; resolve(base64Data); }; reader.onerror = reject; reader.readAsDataURL(blob); }); const description = await describeImage(base64); setInlineDescriptions(prev => ({ ...prev, [message.id]: description })); } catch (error) { console.error('Failed to describe image:', error); toast.error('Failed to describe image'); } finally { setDescribingInline(null); } }; const handleMarkSafe = async (messageId: string) => { setProcessing(messageId); try { const { error } = await adminData .from('messages') .update({ moderation_status: 'safe' }) .eq('id', messageId); if (error) throw error; toast.success('Message marked as safe'); if (selectedMessage?.id === messageId) setSelectedMessage(null); fetchAiMessages(); } catch (error) { toast.error('Failed to update status'); } finally { setProcessing(null); } }; const handleDismissReport = async (reportId: string) => { setProcessing(reportId); try { const { error } = await adminData .from('message_reports') .update({ status: 'dismissed', reviewed_at: new Date().toISOString() }) .eq('id', reportId); if (error) throw error; toast.success('Report dismissed'); if (selectedReport?.id === reportId) setSelectedReport(null); fetchUserReports(); } catch (error) { toast.error('Failed to dismiss report'); } finally { setProcessing(null); } }; const handleReviewReport = async (reportId: string) => { setProcessing(reportId); try { const { error } = await adminData .from('message_reports') .update({ status: 'reviewed', reviewed_at: new Date().toISOString() }) .eq('id', reportId); if (error) throw error; toast.success('Report marked as reviewed'); if (selectedReport?.id === reportId) setSelectedReport(null); fetchUserReports(); } catch (error) { toast.error('Failed to update report'); } finally { setProcessing(null); } }; const handleJumpToChat = () => { const matchId = selectedMessage?.match_id || selectedReport?.message?.match_id; const userId = selectedMessage?.user_id || selectedReport?.message?.user_id; if (!matchId) { toast.error("No match context available"); return; } navigate(`/users/${userId}/matches/${matchId}`); }; const closeDialogs = () => { setSelectedMessage(null); setSelectedReport(null); }; return (

Content Moderation

Review AI-flagged content and user reports

AI Flagged {aiMessages.length > 0 && ( {aiMessages.length} )} User Reports {userReports.length > 0 && ( {userReports.length} )}
Time Reason Type Actions {aiMessages.length === 0 ? (

No AI-flagged messages found. Good job!

) : ( aiMessages.map((message) => ( {format(new Date(message.created_at), 'MMM d, HH:mm')} {message.flag_reason || 'Unknown'}
{message.media_path ? ( {message.media_type || 'Media'} ) : ( Text )}
{message.media_path ? ( {message.media_type || 'Media'} ) : ( Text )}
{message.media_path && message.media_type !== 'video' && ( )}
{inlineDescriptions[message.id] && (

{inlineDescriptions[message.id]}

)}
)) )}
{aiTotal > 0 && ( { setAiPage(1); setAiPageSize(size); }} /> )}
Time Reporter Reason Type Actions {userReports.length === 0 ? (

No pending user reports. All clear!

) : ( userReports.map((report) => ( {format(new Date(report.created_at), 'MMM d, HH:mm')} {report.reporter_profile?.name || report.reporter_profile?.email || 'Unknown'} {REASON_LABELS[report.reason] || report.reason}
{report.reporter_profile?.name || report.reporter_profile?.email || 'Unknown'} {report.message?.media_path ? ( {report.message.media_type || 'Media'} ) : ( Text )}
{report.message?.media_path ? ( {report.message.media_type || 'Media'} ) : ( Text )}
)) )}
{reportsTotal > 0 && ( { setReportsPage(1); setReportsPageSize(size); }} /> )}
{/* Review Dialog - AI Flagged Messages */} !open && closeDialogs()}> Review AI Flagged Content Decrypted content for review. Flagged for: {selectedMessage?.flag_reason}
{decrypting ? (

Decrypting secure content...

) : (
{decryptedContent.text && (

Message Text

{decryptedContent.text}
)} {decryptedContent.mediaUrl && (

Media Content

{selectedMessage?.media_type !== 'video' && ( )}
{selectedMessage?.media_type === 'video' ? (
{imageDescription && (
AI Description

{imageDescription}

)}
)} {!decryptedContent.text && !decryptedContent.mediaUrl && (

No content displayed

)}
)}
{/* Review Dialog - User Reports */} !open && closeDialogs()}> Review User Report Reported by: {selectedReport?.reporter_profile?.name || selectedReport?.reporter_profile?.email || 'Unknown'}
Reason: {selectedReport?.reason ? (REASON_LABELS[selectedReport.reason] || selectedReport.reason) : 'Unknown'}
{decrypting ? (

Decrypting secure content...

) : (
{decryptedContent.text && (

Message Text

{decryptedContent.text}
)} {decryptedContent.mediaUrl && (

Media Content

{selectedReport?.message?.media_type !== 'video' && ( )}
{selectedReport?.message?.media_type === 'video' ? (
{imageDescription && (
AI Description

{imageDescription}

)}
)} {!decryptedContent.text && !decryptedContent.mediaUrl && (

No content displayed

)}
)}
); }