import { useState, useEffect} from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { PageContainer, Container, SegmentInput, Spinner, Button } from '@ifrc-go/ui'; import { DeleteBinLineIcon } from '@ifrc-go/icons'; import { useFilterContext } from '../../hooks/useFilterContext'; import { useAdmin } from '../../hooks/useAdmin'; import FilterBar from '../../components/FilterBar'; import Paginator from '../../components/Paginator'; import styles from './ExplorePage.module.css'; import ExportModal from '../../components/ExportModal'; interface ImageWithCaptionOut { image_id: string; title: string; prompt: string; model: string; schema_id: string; raw_json: Record; generated: string; edited?: string; accuracy?: number; context?: number; usability?: number; starred: boolean; created_at?: string; updated_at?: string; file_key: string; image_url: string; thumbnail_url?: string; // URL to smallest version (300x200px) detail_url?: string; // URL to medium quality version (800x600px) source: string; event_type: string; epsg: string; image_type: string; countries: {c_code: string, label: string, r_code: string}[]; // Multi-upload fields all_image_ids?: string[]; image_count?: number; } export default function ExplorePage() { const navigate = useNavigate(); const location = useLocation(); const { isAuthenticated } = useAdmin(); const [view, setView] = useState<'explore' | 'mapDetails'>('explore'); const [captions, setCaptions] = useState([]); const { search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, setShowReferenceExamples } = useFilterContext(); const [sources, setSources] = useState<{s_code: string, label: string}[]>([]); const [types, setTypes] = useState<{t_code: string, label: string}[]>([]); const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]); const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]); const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]); const [isLoadingFilters, setIsLoadingFilters] = useState(true); const [isLoadingContent, setIsLoadingContent] = useState(true); const [showExportModal, setShowExportModal] = useState(false); const [isExporting, setIsExporting] = useState(false); const [exportSuccess, setExportSuccess] = useState(false); // Delete state management const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [imageToDelete, setImageToDelete] = useState(''); const [isDeleting, setIsDeleting] = useState(false); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage] = useState(10); const [totalItems, setTotalItems] = useState(0); const [totalPages, setTotalPages] = useState(0); const viewOptions = [ { key: 'explore' as const, label: 'List' }, { key: 'mapDetails' as const, label: 'Carousel' } ]; const fetchCaptions = () => { setIsLoadingContent(true); // Build query parameters for server-side filtering and pagination const params = new URLSearchParams({ page: currentPage.toString(), limit: itemsPerPage.toString() }); if (search) params.append('search', search); if (srcFilter) params.append('source', srcFilter); if (catFilter) params.append('event_type', catFilter); if (regionFilter) params.append('region', regionFilter); if (countryFilter) params.append('country', countryFilter); if (imageTypeFilter) params.append('image_type', imageTypeFilter); if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter); if (showReferenceExamples) params.append('starred_only', 'true'); fetch(`/api/images/grouped?${params.toString()}`) .then(r => { if (!r.ok) { console.error('ExplorePage: Grouped endpoint failed, trying legacy endpoint'); // Fallback to legacy endpoint for backward compatibility return fetch('/api/captions/legacy').then(r2 => { if (!r2.ok) { console.error('ExplorePage: Legacy endpoint failed, trying regular images endpoint'); return fetch('/api/images').then(r3 => { if (!r3.ok) { throw new Error(`HTTP ${r3.status}: ${r3.statusText}`); } return r3.json(); }); } return r2.json(); }); } return r.json(); }) .then(data => { console.log('ExplorePage: Fetched captions:', data); setCaptions(data); }) .catch(error => { console.error('ExplorePage: Error fetching captions:', error); setCaptions([]); }) .finally(() => { setIsLoadingContent(false); }); }; const fetchTotalCount = () => { // Build query parameters for count endpoint const params = new URLSearchParams(); if (search) params.append('search', search); if (srcFilter) params.append('source', srcFilter); if (catFilter) params.append('event_type', catFilter); if (regionFilter) params.append('region', regionFilter); if (countryFilter) params.append('country', countryFilter); if (imageTypeFilter) params.append('image_type', imageTypeFilter); if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter); if (showReferenceExamples) params.append('starred_only', 'true'); fetch(`/api/images/grouped/count?${params.toString()}`) .then(r => { if (!r.ok) { console.error('ExplorePage: Count endpoint failed'); return { total_count: 0 }; } return r.json(); }) .then(data => { console.log('ExplorePage: Total count:', data.total_count); setTotalItems(data.total_count); setTotalPages(Math.ceil(data.total_count / itemsPerPage)); }) .catch(error => { console.error('ExplorePage: Error fetching total count:', error); setTotalItems(0); setTotalPages(0); }); }; // Fetch data when component mounts or filters change useEffect(() => { fetchCaptions(); fetchTotalCount(); }, [currentPage, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]); // Reset to first page when filters change (but not when currentPage changes) useEffect(() => { if (currentPage !== 1) { setCurrentPage(1); } }, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]); useEffect(() => { const handleVisibilityChange = () => { if (!document.hidden) { fetchCaptions(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); useEffect(() => { const searchParams = new URLSearchParams(location.search); const exportParam = searchParams.get('export'); if (exportParam === 'true') { setShowExportModal(true); if (search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) { } else { } // Clean up the URL navigate('/explore', { replace: true }); } }, [location.search, navigate, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples]); useEffect(() => { setIsLoadingFilters(true); Promise.all([ fetch('/api/sources').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`); return r.json(); }), fetch('/api/types').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`); return r.json(); }), fetch('/api/regions').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`); return r.json(); }), fetch('/api/countries').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`); return r.json(); }), fetch('/api/image-types').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`); return r.json(); }), ]).then(([sourcesData, typesData, regionsData, countriesData, imageTypesData]) => { setSources(sourcesData); setTypes(typesData); setRegions(regionsData); setCountries(countriesData); setImageTypes(imageTypesData); }).catch(() => { }).finally(() => { setIsLoadingFilters(false); }); }, []); // Server-side pagination - no client-side filtering needed const paginatedResults = captions; const exportDataset = async (images: ImageWithCaptionOut[], mode: 'standard' | 'fine-tuning' = 'fine-tuning') => { if (images.length === 0) { alert('No images to export'); return; } setIsExporting(true); setExportSuccess(false); try { const JSZip = (await import('jszip')).default; const zip = new JSZip(); // Separate images by type const crisisMaps = images.filter(img => img.image_type === 'crisis_map'); const droneImages = images.filter(img => img.image_type === 'drone_image'); if (crisisMaps.length > 0) { const crisisFolder = zip.folder('crisis_maps_dataset'); const crisisImagesFolder = crisisFolder?.folder('images'); if (crisisImagesFolder) { // Process each caption (which may contain multiple images) let jsonIndex = 1; for (const caption of crisisMaps) { try { // Get all image IDs for this caption const imageIds = caption.image_count && caption.image_count > 1 ? caption.all_image_ids || [caption.image_id] : [caption.image_id]; // Fetch all images for this caption const imagePromises = imageIds.map(async (imageId, imgIndex) => { try { const response = await fetch(`/api/images/${imageId}/file`); if (!response.ok) throw new Error(`Failed to fetch image ${imageId}`); const blob = await response.blob(); const fileExtension = caption.file_key.split('.').pop() || 'jpg'; const fileName = `${String(jsonIndex).padStart(4, '0')}_${String(imgIndex + 1).padStart(2, '0')}.${fileExtension}`; crisisImagesFolder.file(fileName, blob); return { success: true, fileName, imageId }; } catch (error) { console.error(`Failed to process image ${imageId}:`, error); return { success: false, fileName: '', imageId }; } }); const imageResults = await Promise.all(imagePromises); const successfulImages = imageResults.filter(result => result.success); if (successfulImages.length > 0) { if (mode === 'fine-tuning') { // For fine-tuning, create one entry per caption with all images const imageFiles = successfulImages.map(result => `images/${result.fileName}`); const random = Math.random(); const entry = { image: imageFiles.length === 1 ? imageFiles[0] : imageFiles, caption: caption.edited || caption.generated || '', metadata: { image_id: imageIds, title: caption.title, source: caption.source, event_type: caption.event_type, image_type: caption.image_type, countries: caption.countries, starred: caption.starred, image_count: caption.image_count || 1 } }; // Store the entry for later processing if (!crisisFolder) continue; if (random < 0.8) { // Add to train data const trainFile = crisisFolder.file('train.jsonl'); if (trainFile) { const existingData = await trainFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []); existingData.push(entry); crisisFolder.file('train.jsonl', JSON.stringify(existingData, null, 2)); } else { crisisFolder.file('train.jsonl', JSON.stringify([entry], null, 2)); } } else if (random < 0.9) { // Add to test data const testFile = crisisFolder.file('test.jsonl'); if (testFile) { const existingData = await testFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []); existingData.push(entry); crisisFolder.file('test.jsonl', JSON.stringify(existingData, null, 2)); } else { crisisFolder.file('test.jsonl', JSON.stringify([entry], null, 2)); } } else { // Add to validation data const valFile = crisisFolder.file('val.jsonl'); if (valFile) { const existingData = await valFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []); existingData.push(entry); crisisFolder.file('val.jsonl', JSON.stringify(existingData, null, 2)); } else { crisisFolder.file('val.jsonl', JSON.stringify([entry], null, 2)); } } } else { // For standard mode, create one JSON file per caption const imageFiles = successfulImages.map(result => `images/${result.fileName}`); const jsonData = { image: imageFiles.length === 1 ? imageFiles[0] : imageFiles, caption: caption.edited || caption.generated || '', metadata: { image_id: imageIds, title: caption.title, source: caption.source, event_type: caption.event_type, image_type: caption.image_type, countries: caption.countries, starred: caption.starred, image_count: caption.image_count || 1 } }; if (crisisFolder) { crisisFolder.file(`${String(jsonIndex).padStart(4, '0')}.json`, JSON.stringify(jsonData, null, 2)); } } jsonIndex++; } } catch (error) { console.error(`Failed to process caption ${caption.image_id}:`, error); } } } } // Create drone_images dataset if (droneImages.length > 0) { const droneFolder = zip.folder('drone_images_dataset'); const droneImagesFolder = droneFolder?.folder('images'); if (droneImagesFolder) { // Process each caption (which may contain multiple images) let jsonIndex = 1; for (const caption of droneImages) { try { // Get all image IDs for this caption const imageIds = caption.image_count && caption.image_count > 1 ? caption.all_image_ids || [caption.image_id] : [caption.image_id]; // Fetch all images for this caption const imagePromises = imageIds.map(async (imageId, imgIndex) => { try { const response = await fetch(`/api/images/${imageId}/file`); if (!response.ok) throw new Error(`Failed to fetch image ${imageId}`); const blob = await response.blob(); const fileExtension = caption.file_key.split('.').pop() || 'jpg'; const fileName = `${String(jsonIndex).padStart(4, '0')}_${String(imgIndex + 1).padStart(2, '0')}.${fileExtension}`; droneImagesFolder.file(fileName, blob); return { success: true, fileName, imageId }; } catch (error) { console.error(`Failed to process image ${imageId}:`, error); return { success: false, fileName: '', imageId }; } }); const imageResults = await Promise.all(imagePromises); const successfulImages = imageResults.filter(result => result.success); if (successfulImages.length > 0) { if (mode === 'fine-tuning') { // For fine-tuning, create one entry per caption with all images const imageFiles = successfulImages.map(result => `images/${result.fileName}`); const random = Math.random(); const entry = { image: imageFiles.length === 1 ? imageFiles[0] : imageFiles, caption: caption.edited || caption.generated || '', metadata: { image_id: imageIds, title: caption.title, source: caption.source, event_type: caption.event_type, image_type: caption.image_type, countries: caption.countries, starred: caption.starred, image_count: caption.image_count || 1 } }; // Store the entry for later processing if (!droneFolder) continue; if (random < 0.8) { // Add to train data const trainFile = droneFolder.file('train.jsonl'); if (trainFile) { const existingData = await trainFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []); existingData.push(entry); droneFolder.file('train.jsonl', JSON.stringify(existingData, null, 2)); } else { droneFolder.file('train.jsonl', JSON.stringify([entry], null, 2)); } } else if (random < 0.9) { // Add to test data const testFile = droneFolder.file('test.jsonl'); if (testFile) { const existingData = await testFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []); existingData.push(entry); droneFolder.file('test.jsonl', JSON.stringify(existingData, null, 2)); } else { droneFolder.file('test.jsonl', JSON.stringify([entry], null, 2)); } } else { // Add to validation data const valFile = droneFolder.file('val.jsonl'); if (valFile) { const existingData = await valFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []); existingData.push(entry); droneFolder.file('val.jsonl', JSON.stringify(existingData, null, 2)); } else { droneFolder.file('val.jsonl', JSON.stringify([entry], null, 2)); } } } else { // For standard mode, create one JSON file per caption const imageFiles = successfulImages.map(result => `images/${result.fileName}`); const jsonData = { image: imageFiles.length === 1 ? imageFiles[0] : imageFiles, caption: caption.edited || caption.generated || '', metadata: { image_id: imageIds, title: caption.title, source: caption.source, event_type: caption.event_type, image_type: caption.image_type, countries: caption.countries, starred: caption.starred, image_count: caption.image_count || 1 } }; if (droneFolder) { droneFolder.file(`${String(jsonIndex).padStart(4, '0')}.json`, JSON.stringify(jsonData, null, 2)); } } jsonIndex++; } } catch (error) { console.error(`Failed to process caption ${caption.image_id}:`, error); } } } } const zipBlob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(zipBlob); const link = document.createElement('a'); link.href = url; link.download = `datasets_${mode}_${new Date().toISOString().split('T')[0]}.zip`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); const totalImages = (crisisMaps.length || 0) + (droneImages.length || 0); console.log(`Exported ${mode} datasets with ${totalImages} total images:`); if (crisisMaps.length > 0) console.log(`- Crisis maps: ${crisisMaps.length} images`); if (droneImages.length > 0) console.log(`- Drone images: ${droneImages.length} images`); setExportSuccess(true); } catch (error) { console.error('Export failed:', error); alert('Failed to export dataset. Please try again.'); } finally { setIsExporting(false); } }; // Delete functions const handleDelete = (imageId: string) => { setImageToDelete(imageId); setShowDeleteConfirm(true); }; const confirmDelete = async () => { if (!imageToDelete) return; setIsDeleting(true); try { console.log('Deleting image with ID:', imageToDelete); const response = await fetch(`/api/images/${imageToDelete}`, { method: 'DELETE', }); if (response.ok) { // Remove the deleted image from the captions list setCaptions(prev => prev.filter(img => img.image_id !== imageToDelete)); setShowDeleteConfirm(false); setImageToDelete(''); } else { console.error('Delete failed'); alert('Failed to delete image. Please try again.'); } } catch (error) { console.error('Delete failed:', error); alert('Failed to delete image. Please try again.'); } finally { setIsDeleting(false); } }; return ( {isLoadingContent ? (
Loading examples...
) : (
{ if (value === 'explore' || value === 'mapDetails') { setView(value); if (value === 'mapDetails' && captions.length > 0) { if (captions[0]?.image_id && captions[0].image_id !== 'undefined' && captions[0].image_id !== 'null') { navigate(`/map/${captions[0].image_id}`); } else { console.error('Invalid image_id for navigation:', captions[0]?.image_id); } } } }} options={viewOptions} keySelector={(o) => o.key} labelSelector={(o) => o.label} /> {/* Action Buttons - Right Aligned */}
{/* Reference Examples Filter - Available to all users */} {/* Export Dataset Button */}
{view === 'explore' ? (
{/* Search and Filters */}
{/* Layer 1: Search, Reference Examples, Clear Filters */}
{/* Results Section */}

{paginatedResults.length} of {totalItems} examples

{/* Loading State */} {isLoadingContent && (
Loading examples...
)} {/* Content */} {!isLoadingContent && (
{paginatedResults.map(c => (
{/* Card Content */}
{ console.log('ExplorePage: Clicking on image with ID:', c.image_id); console.log('ExplorePage: Image data:', c); if (c.image_id && c.image_id !== 'undefined' && c.image_id !== 'null') { console.log('ExplorePage: Navigating to:', `/map/${c.image_id}`); console.log('ExplorePage: Full navigation URL:', `/#/map/${c.image_id}`); navigate(`/map/${c.image_id}`); } else { console.error('Invalid image_id for navigation:', c.image_id); console.error('Full item data:', JSON.stringify(c, null, 2)); // Show a visual error in production alert(`Cannot navigate: Invalid image ID (${c.image_id})`); } }}>
{/* Explore Page: Prioritize thumbnails for faster loading */} {c.thumbnail_url ? ( <> {console.log('ExplorePage: Using thumbnail for fast loading:', c.thumbnail_url)} {c.file_key} { console.error('ExplorePage: Thumbnail failed to load, falling back to original:', c.thumbnail_url); // Fallback to original image const target = e.target as HTMLImageElement; if (c.image_url) { target.src = c.image_url; } else { target.style.display = 'none'; target.parentElement!.innerHTML = 'Img'; } }} onLoad={() => console.log('ExplorePage: Thumbnail loaded successfully:', c.thumbnail_url)} /> ) : c.image_url ? ( <> {console.log('ExplorePage: No thumbnail available, using original image:', c.image_url)} {c.file_key} { console.error('ExplorePage: Original image failed to load:', c.image_url); const target = e.target as HTMLImageElement; target.style.display = 'none'; target.parentElement!.innerHTML = 'Img'; }} onLoad={() => console.log('ExplorePage: Original image loaded successfully:', c.image_url)} /> ) : ( <> {console.log('ExplorePage: No image_url or thumbnail provided for item:', c)} 'Img' )}

{c.title || 'Untitled'} {c.starred && ( )}

{c.image_type !== 'drone_image' && ( {c.source && c.source.includes(', ') ? c.source.split(', ').map(s => sources.find(src => src.s_code === s.trim())?.label || s.trim()).join(', ') : sources.find(s => s.s_code === c.source)?.label || c.source } )} {c.event_type && c.event_type.includes(', ') ? c.event_type.split(', ').map(e => types.find(t => t.t_code === e.trim())?.label || e.trim()).join(', ') : types.find(t => t.t_code === c.event_type)?.label || c.event_type } {imageTypes.find(it => it.image_type === c.image_type)?.label || c.image_type} {c.image_count && c.image_count > 1 && ( 📷 {c.image_count} )} {(!c.image_count || c.image_count <= 1) && ( Single )} {c.countries && c.countries.length > 0 && ( <> {regions.find(r => r.r_code === c.countries[0].r_code)?.label || 'Unknown Region'} {c.countries.map(country => country.label).join(', ')} )}
{/* Delete Button - Admin Only */} {isAuthenticated && ( )}
))} {!paginatedResults.length && (

No examples found.

)} {/* Enhanced Paginator Component */} {!isLoadingContent && paginatedResults.length > 0 && ( )}
)}
) : (

Map Details view coming soon...

This will show detailed information about individual maps

)}
)} {/* Delete Confirmation Modal */} {showDeleteConfirm && (
setShowDeleteConfirm(false)}>
e.stopPropagation()}>

Delete Image?

This action cannot be undone. Are you sure you want to delete this saved image and all related data?

)} {/* Export Selection Modal */} { setShowExportModal(false); setExportSuccess(false); setIsExporting(false); }} onExport={(mode, selectedTypes) => { const filteredByType = paginatedResults.filter((img: ImageWithCaptionOut) => selectedTypes.includes(img.image_type)); exportDataset(filteredByType, mode); }} filteredCount={paginatedResults.length} totalCount={totalItems} hasFilters={!!(search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || uploadTypeFilter || showReferenceExamples)} crisisMapsCount={paginatedResults.filter((img: ImageWithCaptionOut) => img.image_type === 'crisis_map').length} droneImagesCount={paginatedResults.filter((img: ImageWithCaptionOut) => img.image_type === 'drone_image').length} isLoading={isExporting} exportSuccess={exportSuccess} />
); }