import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useAxios } from '@/plugins/AxiosContext'; import { Head, usePage } from '@inertiajs/react'; import { AlertCircle, Loader2, RotateCw, Search, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; interface DuplicateItem { id: number; name: string; distance: number; url: string; group: number; } interface DuplicateGroup { original: { id: number; name: string; url: string; group: number; } | null; duplicates: DuplicateItem[]; } interface ScanResult { duplicates: DuplicateGroup[]; total_groups: number; total_duplicates: number; } export default function DuplicateManagement() { const { title } = usePage().props; const axios = useAxios(); const [threshold, setThreshold] = useState(5); const [isScanning, setIsScanning] = useState(false); const [scanResult, setScanResult] = useState(null); const [deletingIds, setDeletingIds] = useState([]); const handleScan = async () => { setIsScanning(true); try { const response = await axios.post(route('admin.duplicates.scan'), { threshold, }); if (response.data.duplicates.length === 0) { toast.info('No duplicate images were found with the current threshold.'); } setScanResult(response.data); } catch (error) { console.error('Scan error:', error); toast.error('Failed to scan for duplicates'); } finally { setIsScanning(false); } }; const handleDelete = async (id: number) => { setDeletingIds((prev) => [...prev, id]); try { const response = await axios.post(route('admin.duplicates.delete'), { id, }); if (response.data.success) { toast.success(response.data.message); // Remove the deleted item from results setScanResult((prev) => { if (!prev) return null; return { ...prev, duplicates: prev.duplicates.map((group) => { // If the original was deleted, remove it from the group if (group.original && group.original.id === id) { return { ...group, original: null, // Mark original as deleted }; } // Remove the deleted item from duplicates return { ...group, duplicates: group.duplicates.filter((item) => item.id !== id), }; }), }; }); } } catch (error) { console.error('Delete error:', error); if (error.response?.data?.message) { toast.error(error.response.data.message); } else { toast.error('Failed to delete image'); } } finally { setDeletingIds((prev) => prev.filter((deletingId) => deletingId !== id)); } }; const handleRegenerateHash = async (id: number) => { try { const response = await axios.post(route('admin.duplicates.regenerate-hash'), { id, }); if (response.data.success) { toast.success(response.data.message); } } catch (error) { console.error('Regenerate hash error:', error); toast.error('Failed to regenerate hash'); } }; return ( <>

Duplicate Management

Find and manage duplicate meme images using Hamming distance comparison

Scan for Duplicates
setThreshold(Number(e.target.value))} min="0" max="64" className="mt-1" />

Lower values = stricter matching (0-10 recommended)

{scanResult && ( Scan Results
Groups: {scanResult.total_groups} Total Duplicates: {scanResult.total_duplicates}
{scanResult.duplicates.length === 0 ? (

No duplicates found

) : (
{scanResult.duplicates.map((group, index) => (

Duplicate Group {index + 1}

{group.duplicates.length} duplicates
{/* Original */} {group.original && (
Original Group {group.original.group}
{group.original.name}

{group.original.name}

ID: {group.original.id}

)} {/* Duplicates */} {group.duplicates.map((duplicate) => (
Distance: {duplicate.distance} Group {duplicate.group}
{duplicate.name}

{duplicate.name}

ID: {duplicate.id}

))}
))}
)}
)}
); }