diff --git "a/frontend/src/pages/UploadPage/UploadPage.tsx" "b/frontend/src/pages/UploadPage/UploadPage.tsx" --- "a/frontend/src/pages/UploadPage/UploadPage.tsx" +++ "b/frontend/src/pages/UploadPage/UploadPage.tsx" @@ -1,16 +1,25 @@ import { useState, useEffect, useRef, useCallback } from 'react'; -import type { DragEvent } from 'react'; -import { - PageContainer, Heading, Button, - SelectInput, MultiSelectInput, Container, IconButton, TextInput, TextArea, Spinner, SegmentInput, -} from '@ifrc-go/ui'; -import { - UploadCloudLineIcon, - ArrowRightLineIcon, - DeleteBinLineIcon, -} from '@ifrc-go/icons'; -import { Link, useSearchParams, useNavigate } from 'react-router-dom'; +import { PageContainer, Heading, Button, Spinner, IconButton } from '@ifrc-go/ui'; +import { DeleteBinLineIcon, ChevronLeftLineIcon, ChevronRightLineIcon } from '@ifrc-go/icons'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import styles from './UploadPage.module.css'; +import { + FileUploadSection, + ImagePreviewSection, + MetadataFormSection, + RatingSection, + GeneratedTextSection, + FullSizeImageModal, + RatingWarningModal, + DeleteConfirmModal, + NavigationConfirmModal, + FallbackNotificationModal, + PreprocessingNotificationModal, + PreprocessingModal, + UnsupportedFormatModal, + FileSizeWarningModal, + +} from '../../components'; const SELECTED_MODEL_KEY = 'selectedVlmModel'; @@ -25,7 +34,8 @@ export default function UploadPage() { const [preview, setPreview] = useState(null); const [file, setFile] = useState(null); - const [source, setSource] = useState(''); + const [files, setFiles] = useState([]); + const [source, setSource] = useState(''); const [eventType, setEventType] = useState(''); const [epsg, setEpsg] = useState(''); const [imageType, setImageType] = useState('crisis_map'); @@ -45,6 +55,25 @@ export default function UploadPage() { const [stdHM, setStdHM] = useState(''); const [stdVM, setStdVM] = useState(''); + // Multi-image metadata arrays + const [metadataArray, setMetadataArray] = useState>([]); + const [sources, setSources] = useState<{s_code: string, label: string}[]>([]); const [types, setTypes] = useState<{t_code: string, label: string}[]>([]); const [spatialReferences, setSpatialReferences] = useState<{epsg: string, srid: string, proj4: string, wkt: string}[]>([]); @@ -52,277 +81,17 @@ export default function UploadPage() { const [countriesOptions, setCountriesOptions] = useState<{c_code: string, label: string, r_code: string}[]>([]); const [uploadedImageId, setUploadedImageId] = useState(null); - - stepRef.current = step; - uploadedImageIdRef.current = uploadedImageId; - - const handleSourceChange = (value: string | undefined) => setSource(value || ''); - const handleEventTypeChange = (value: string | undefined) => setEventType(value || ''); - const handleEpsgChange = (value: string | undefined) => setEpsg(value || ''); - const handleImageTypeChange = (value: string | undefined) => setImageType(value || ''); - const handleCountriesChange = (value: string[] | undefined) => setCountries(Array.isArray(value) ? value : []); - - // Drone metadata handlers - const handleCenterLonChange = (value: string | undefined) => setCenterLon(value || ''); - const handleCenterLatChange = (value: string | undefined) => setCenterLat(value || ''); - const handleAmslMChange = (value: string | undefined) => setAmslM(value || ''); - const handleAglMChange = (value: string | undefined) => setAglM(value || ''); - const handleHeadingDegChange = (value: string | undefined) => setHeadingDeg(value || ''); - const handleYawDegChange = (value: string | undefined) => setYawDeg(value || ''); - const handlePitchDegChange = (value: string | undefined) => setPitchDeg(value || ''); - const handleRollDegChange = (value: string | undefined) => setRollDeg(value || ''); - const handleRtkFixChange = (value: boolean | undefined) => setRtkFix(value || false); - const handleStdHMChange = (value: string | undefined) => setStdHM(value || ''); - const handleStdVMChange = (value: string | undefined) => setStdVM(value || ''); - - const handleStepChange = (newStep: 1 | '2a' | '2b' | 3) => setStep(newStep); - - useEffect(() => { - Promise.all([ - fetch('/api/sources').then(r => r.json()), - fetch('/api/types').then(r => r.json()), - fetch('/api/spatial-references').then(r => r.json()), - fetch('/api/image-types').then(r => r.json()), - fetch('/api/countries').then(r => r.json()), - fetch('/api/models').then(r => r.json()) - ]).then(([sourcesData, typesData, spatialData, imageTypesData, countriesData, modelsData]) => { - if (!localStorage.getItem(SELECTED_MODEL_KEY) && modelsData?.length) { - localStorage.setItem(SELECTED_MODEL_KEY, modelsData[0].m_code); - } - setSources(sourcesData); - setTypes(typesData); - setSpatialReferences(spatialData); - setImageTypes(imageTypesData); - setCountriesOptions(countriesData); - - if (sourcesData.length > 0) setSource(sourcesData[0].s_code); - setEventType('OTHER'); - setEpsg('OTHER'); - if (imageTypesData.length > 0 && !searchParams.get('imageType') && !imageType) { - setImageType(imageTypesData[0].image_type); - } - }); - }, [searchParams, imageType]); - - const handleNavigation = useCallback((to: string) => { - if (to === '/upload' || to === '/') { - return; - } - - if (uploadedImageIdRef.current) { - setPendingNavigation(to); - setShowNavigationConfirm(true); - } else { - navigate(to); - } - }, [navigate]); - - useEffect(() => { - window.confirmNavigationIfNeeded = (to: string) => { - handleNavigation(to); - }; - - return () => { - delete window.confirmNavigationIfNeeded; - }; - }, [handleNavigation]); - - useEffect(() => { - const handleBeforeUnload = (event: BeforeUnloadEvent) => { - if (uploadedImageIdRef.current) { - const message = 'You have an uploaded image that will be deleted if you leave this page. Are you sure you want to leave?'; - event.preventDefault(); - event.returnValue = message; - return message; - } - }; - - const handleCleanup = () => { - if (uploadedImageIdRef.current) { - fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error); - } - }; - - const handleGlobalClick = (event: MouseEvent) => { - const target = event.target as HTMLElement; - const link = target.closest('a[href]') || target.closest('[data-navigate]'); - - if (link && uploadedImageIdRef.current) { - const href = link.getAttribute('href') || link.getAttribute('data-navigate'); - if (href && href !== '#' && !href.startsWith('javascript:') && !href.startsWith('mailto:')) { - event.preventDefault(); - event.stopPropagation(); - handleNavigation(href); - } - } - }; - - window.addEventListener('beforeunload', handleBeforeUnload); - document.addEventListener('click', handleGlobalClick, true); - - return () => { - window.removeEventListener('beforeunload', handleBeforeUnload); - document.removeEventListener('click', handleGlobalClick, true); - handleCleanup(); - }; - }, [handleNavigation]); - + const [uploadedImageIds, setUploadedImageIds] = useState([]); const [imageUrl, setImageUrl] = useState(null); const [draft, setDraft] = useState(''); const [description, setDescription] = useState(''); const [analysis, setAnalysis] = useState(''); const [recommendedActions, setRecommendedActions] = useState(''); - - useEffect(() => { - const imageUrlParam = searchParams.get('imageUrl'); - const stepParam = searchParams.get('step'); - const imageIdParam = searchParams.get('imageId'); - const imageTypeParam = searchParams.get('imageType'); - - if (imageUrlParam) { - setImageUrl(imageUrlParam); - - if (stepParam === '2a' && imageIdParam) { - setIsLoadingContribution(true); - setUploadedImageId(imageIdParam); - - if (imageTypeParam) { - console.log('Setting imageType from URL parameter:', imageTypeParam); - setImageType(imageTypeParam); - } - - fetch(`/api/images/${imageIdParam}`) - .then(res => res.json()) - .then(data => { - console.log('API response data.image_type:', data.image_type); - if (data.image_type && !imageTypeParam) { - console.log('Setting imageType from API response:', data.image_type); - setImageType(data.image_type); - } - - if (data.generated) { - // Extract the three parts from raw_json.metadata (same as regular upload flow) - const extractedMetadataForParts = data.raw_json?.metadata; - if (extractedMetadataForParts) { - if (extractedMetadataForParts.description) { - setDescription(extractedMetadataForParts.description); - } - if (extractedMetadataForParts.analysis) { - setAnalysis(extractedMetadataForParts.analysis); - } - if (extractedMetadataForParts.recommended_actions) { - setRecommendedActions(extractedMetadataForParts.recommended_actions); - } - } - - // Set draft with the generated content for backward compatibility - setDraft(data.generated); - } - - let extractedMetadata = data.raw_json?.metadata; - console.log('Raw metadata:', extractedMetadata); - - if (!extractedMetadata && data.generated) { - try { - const parsedGenerated = JSON.parse(data.generated); - console.log('Parsed generated field:', parsedGenerated); - if (parsedGenerated.metadata) { - extractedMetadata = parsedGenerated; - console.log('Using metadata from generated field'); - } - } catch (e) { - console.log('Could not parse generated field as JSON:', e); - } - } - - if (extractedMetadata) { - const metadata = extractedMetadata.metadata || extractedMetadata; - console.log('Final metadata to apply:', metadata); - if (metadata.title) { - console.log('Setting title to:', metadata.title); - setTitle(metadata.title); - } - if (metadata.source) { - console.log('Setting source to:', metadata.source); - setSource(metadata.source); - } - if (metadata.type) { - console.log('Setting event type to:', metadata.type); - setEventType(metadata.type); - } - if (metadata.epsg) { - console.log('Setting EPSG to:', metadata.epsg); - setEpsg(metadata.epsg); - } - if (metadata.countries && Array.isArray(metadata.countries)) { - console.log('Setting countries to:', metadata.countries); - setCountries(metadata.countries); - } - } else { - console.log('No metadata found to extract'); - } - - setStep('2a'); - setIsLoadingContribution(false); - }) - .catch(console.error) - .finally(() => setIsLoadingContribution(false)); - } - } - }, [searchParams]); - - useEffect(() => { - console.log('imageType changed to:', imageType); - }, [imageType]); - - const resetToStep1 = () => { - setIsPerformanceConfirmed(false); - setStep(1); - setFile(null); - setPreview(null); - setUploadedImageId(null); - setImageUrl(null); - setTitle(''); - setSource(''); - setEventType(''); - setEpsg(''); - setCountries([]); - setCenterLon(''); - setCenterLat(''); - setAmslM(''); - setAglM(''); - setHeadingDeg(''); - setYawDeg(''); - setPitchDeg(''); - setRollDeg(''); - setRtkFix(false); - setStdHM(''); - setStdVM(''); - setScores({ accuracy: 50, context: 50, usability: 50 }); - setDraft(''); - setDescription(''); - setAnalysis(''); - setRecommendedActions(''); - setShowFallbackNotification(false); - setFallbackInfo(null); - setShowPreprocessingNotification(false); - setPreprocessingInfo(null); - setShowPreprocessingModal(false); - setPreprocessingFile(null); - setIsPreprocessing(false); - setPreprocessingProgress(''); - setShowUnsupportedFormatModal(false); - setUnsupportedFile(null); - setShowFileSizeWarningModal(false); - setOversizedFile(null); - }; - const [scores, setScores] = useState({ - accuracy: 50, - context: 50, - usability: 50, - }); + const [scores, setScores] = useState({ accuracy: 50, context: 50, usability: 50 }); + // Modal states const [isFullSizeModalOpen, setIsFullSizeModalOpen] = useState(false); + const [selectedImageData, setSelectedImageData] = useState<{ file: File; index: number } | null>(null); const [isPerformanceConfirmed, setIsPerformanceConfirmed] = useState(false); const [showRatingWarning, setShowRatingWarning] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -334,7 +103,6 @@ export default function UploadPage() { fallbackModel: string; reason: string; } | null>(null); - const [showPreprocessingNotification, setShowPreprocessingNotification] = useState(false); const [preprocessingInfo, setPreprocessingInfo] = useState<{ original_filename: string; @@ -344,62 +112,107 @@ export default function UploadPage() { was_preprocessed: boolean; error?: string; } | null>(null); - const [showPreprocessingModal, setShowPreprocessingModal] = useState(false); const [preprocessingFile, setPreprocessingFile] = useState(null); const [isPreprocessing, setIsPreprocessing] = useState(false); const [preprocessingProgress, setPreprocessingProgress] = useState(''); - - // unsupported format popup const [showUnsupportedFormatModal, setShowUnsupportedFormatModal] = useState(false); const [unsupportedFile, setUnsupportedFile] = useState(null); - - // New state for file size warning popup const [showFileSizeWarningModal, setShowFileSizeWarningModal] = useState(false); const [oversizedFile, setOversizedFile] = useState(null); + // Carousel state for multi-upload in step 2b + const [currentImageIndex, setCurrentImageIndex] = useState(0); + + stepRef.current = step; + uploadedImageIdRef.current = uploadedImageId; + + // Event handlers + const handleSourceChange = (value: string | undefined) => setSource(value || ''); + const handleEventTypeChange = (value: string | undefined) => setEventType(value || ''); + const handleEpsgChange = (value: string | undefined) => setEpsg(value || ''); + const handleImageTypeChange = (value: string | undefined) => setImageType(value || ''); + const handleCountriesChange = (value: string[] | undefined) => setCountries(Array.isArray(value) ? value : []); + const handleCenterLonChange = (value: string | undefined) => setCenterLon(value || ''); + const handleCenterLatChange = (value: string | undefined) => setCenterLat(value || ''); + const handleAmslMChange = (value: string | undefined) => setAmslM(value || ''); + const handleAglMChange = (value: string | undefined) => setAglM(value || ''); + const handleHeadingDegChange = (value: string | undefined) => setHeadingDeg(value || ''); + const handleYawDegChange = (value: string | undefined) => setYawDeg(value || ''); + const handlePitchDegChange = (value: string | undefined) => setPitchDeg(value || ''); + const handleRollDegChange = (value: string | undefined) => setRollDeg(value || ''); + const handleRtkFixChange = (value: boolean | undefined) => setRtkFix(value || false); + const handleStdHMChange = (value: string | undefined) => setStdHM(value || ''); + const handleStdVMChange = (value: string | undefined) => setStdVM(value || ''); + const handleStepChange = (newStep: 1 | '2a' | '2b' | 3) => setStep(newStep); + + // Carousel navigation functions for step 2b + const goToPrevious = useCallback(() => { + if (files.length > 1) { + setCurrentImageIndex((prev: number) => (prev > 0 ? prev - 1 : files.length - 1)); + } + }, [files.length]); + + const goToNext = useCallback(() => { + if (files.length > 1) { + setCurrentImageIndex((prev: number) => (prev < files.length - 1 ? prev + 1 : 0)); + } + }, [files.length]); - const onDrop = (e: DragEvent) => { - e.preventDefault(); - const dropped = e.dataTransfer.files?.[0]; - if (dropped) { - onFileChange(dropped); + const goToImage = useCallback((index: number) => { + if (index >= 0 && index < files.length) { + setCurrentImageIndex(index); + } + }, [files.length]); + + // Multi-image functions + const addImage = () => { + if (files.length < 5) { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.jpg,.jpeg,.png,.tiff,.tif,.heic,.heif,.webp,.gif,.pdf'; + input.onchange = (e) => { + const target = e.target as HTMLInputElement; + if (target.files && target.files[0]) { + const newFile = target.files[0]; + onFileChange(newFile); + } + }; + input.click(); } }; - const onFileChange = (file: File | undefined) => { - if (file) { - console.log('File selected:', file.name, 'Type:', file.type, 'Size:', file.size); - - // Check if file is too large (5MB limit) - show warning but don't block - const fileSizeMB = file.size / (1024 * 1024); - if (fileSizeMB > 5) { - console.log('File too large, showing size warning modal'); - setOversizedFile(file); - setShowFileSizeWarningModal(true); - // Don't return - continue with normal processing - } - - // Check if file is completely unsupported - if (isCompletelyUnsupported(file)) { - console.log('File format not supported at all, showing unsupported format modal'); - setUnsupportedFile(file); - setShowUnsupportedFormatModal(true); - return; + const removeImage = (index: number) => { + setFiles(prev => { + const newFiles = prev.filter((_, i) => i !== index); + // If we're back to single file, update the single file state + if (newFiles.length === 1) { + setFile(newFiles[0]); + } else if (newFiles.length === 0) { + setFile(null); } - - // Check if file needs preprocessing - if (needsPreprocessing(file)) { - console.log('File needs preprocessing, showing modal'); - setPreprocessingFile(file); - setShowPreprocessingModal(true); - } else { - console.log('File does not need preprocessing, setting directly'); - setFile(file); + return newFiles; + }); + setMetadataArray(prev => prev.filter((_, i) => i !== index)); + }; + + const updateMetadataForImage = (index: number, field: string, value: any) => { + setMetadataArray(prev => { + const newArray = [...prev]; + if (!newArray[index]) { + newArray[index] = { + source: '', eventType: '', epsg: '', countries: [], + centerLon: '', centerLat: '', amslM: '', aglM: '', + headingDeg: '', yawDeg: '', pitchDeg: '', rollDeg: '', + rtkFix: false, stdHM: '', stdVM: '' + }; } - } + newArray[index] = { ...newArray[index], [field]: value }; + return newArray; + }); }; + // File handling functions const needsPreprocessing = (file: File): boolean => { const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png']; const supportedExtensions = ['.jpg', '.jpeg', '.png']; @@ -415,38 +228,22 @@ export default function UploadPage() { }; const isCompletelyUnsupported = (file: File): boolean => { - // List of formats that are completely unsupported (cannot be converted) const completelyUnsupportedTypes = [ - 'text/html', - 'text/css', - 'application/javascript', - 'application/json', - 'text/plain', - 'application/xml', - 'text/xml', - 'application/zip', - 'application/x-zip-compressed', - 'application/x-rar-compressed', - 'application/x-7z-compressed', - 'audio/', - 'video/', - 'text/csv', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + 'text/html', 'text/css', 'application/javascript', 'application/json', + 'text/plain', 'application/xml', 'text/xml', 'application/zip', + 'application/x-zip-compressed', 'application/x-rar-compressed', + 'application/x-7z-compressed', 'audio/', 'video/', 'text/csv', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ]; - // Check if the MIME type starts with any of the unsupported types for (const unsupportedType of completelyUnsupportedTypes) { if (file.type.startsWith(unsupportedType)) { return true; } } - // Check file extension for additional unsupported formats if (file.name) { const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); const unsupportedExtensions = [ @@ -463,88 +260,87 @@ export default function UploadPage() { return false; }; - const handlePreprocessingConfirm = async () => { - if (!preprocessingFile) return; - - setIsPreprocessing(true); - setPreprocessingProgress('Starting file conversion...'); - - try { - const formData = new FormData(); - formData.append('file', preprocessingFile); - formData.append('preprocess_only', 'true'); - - setPreprocessingProgress('Converting file format...'); - - const response = await fetch('/api/images/preprocess', { - method: 'POST', - body: formData - }); + const onFileChange = (file: File | undefined) => { + if (file) { + console.log('File selected:', file.name, 'Type:', file.type, 'Size:', file.size); - if (!response.ok) { - throw new Error('Preprocessing failed'); + const fileSizeMB = file.size / (1024 * 1024); + if (fileSizeMB > 5) { + console.log('File too large, showing size warning modal'); + setOversizedFile(file); + setShowFileSizeWarningModal(true); } - const result = await response.json(); - - setPreprocessingProgress('Finalizing conversion...'); - - const processedContent = atob(result.processed_content); - const processedBytes = new Uint8Array(processedContent.length); - for (let i = 0; i < processedContent.length; i++) { - processedBytes[i] = processedContent.charCodeAt(i); + if (isCompletelyUnsupported(file)) { + console.log('File format not supported at all, showing unsupported format modal'); + setUnsupportedFile(file); + setShowUnsupportedFormatModal(true); + return; + } + + if (needsPreprocessing(file)) { + console.log('File needs preprocessing, showing modal'); + setPreprocessingFile(file); + setShowPreprocessingModal(true); + } else { + console.log('File does not need preprocessing, setting directly'); + // If this is the first file, set it as the single file + if (files.length === 0) { + setFile(file); + setFiles([file]); + } else { + // If files already exist, add to the array (multi-upload mode) + setFiles(prev => [...prev, file]); + } } - - const processedFile = new File( - [processedBytes], - result.processed_filename, - { type: result.processed_mime_type } - ); - - const previewUrl = URL.createObjectURL(processedFile); - - setFile(processedFile); - setPreview(previewUrl); - - setPreprocessingProgress('Conversion complete!'); - - setTimeout(() => { - setShowPreprocessingModal(false); - setPreprocessingFile(null); - setIsPreprocessing(false); - setPreprocessingProgress(''); - }, 1000); - - } catch (error) { - console.error('Preprocessing error:', error); - setPreprocessingProgress('Conversion failed. Please try again.'); - setTimeout(() => { - setShowPreprocessingModal(false); - setPreprocessingFile(null); - setIsPreprocessing(false); - setPreprocessingProgress(''); - }, 2000); } }; - const handlePreprocessingCancel = () => { - setShowPreprocessingModal(false); - setPreprocessingFile(null); - setIsPreprocessing(false); - setPreprocessingProgress(''); - }; + const onChangeFile = (file: File | undefined) => { + if (file) { + console.log('File changed:', file.name, 'Type:', file.type, 'Size:', file.size); + + const fileSizeMB = file.size / (1024 * 1024); + if (fileSizeMB > 5) { + console.log('File too large, showing size warning modal'); + setOversizedFile(file); + setShowFileSizeWarningModal(true); + } + + if (isCompletelyUnsupported(file)) { + console.log('File format not supported at all, showing unsupported format modal'); + setUnsupportedFile(file); + setShowUnsupportedFormatModal(true); + return; + } - useEffect(() => { - if (!file) { - setPreview(null); - return; + if (needsPreprocessing(file)) { + console.log('File needs preprocessing, showing modal'); + setPreprocessingFile(file); + setShowPreprocessingModal(true); + } else { + console.log('File does not need preprocessing, replacing last file'); + // Replace only the last file in the array + if (files.length > 1) { + setFiles(prev => { + const newFiles = [...prev]; + newFiles[newFiles.length - 1] = file; + return newFiles; + }); + // Update single file state if it's a single upload + if (files.length === 1) { + setFile(file); + } + } else { + // If only one file, replace it normally + setFile(file); + setFiles([file]); + } + } } - const url = URL.createObjectURL(file); - setPreview(url); - return () => URL.revokeObjectURL(url); - }, [file]); - + }; + // API functions async function readJsonSafely(res: Response): Promise> { const text = await res.text(); try { @@ -560,18 +356,39 @@ export default function UploadPage() { } async function handleGenerate() { - if (!file) return; + if (files.length === 0) return; setIsLoading(true); - console.log('DEBUG: handleGenerate called - starting regular upload flow'); + try { + if (files.length === 1) { + await handleSingleUpload(); + } else { + await handleMultiUpload(); + } + } catch (err) { + handleApiError(err, 'Upload'); + } finally { + setIsLoading(false); + } + } + + async function handleSingleUpload() { + console.log('DEBUG: Starting single image upload'); const fd = new FormData(); - fd.append('file', file); - + fd.append('file', files[0]); + fd.append('title', title); + fd.append('image_type', imageType); + + // Add metadata for single image + if (source) fd.append('source', source); + if (eventType) fd.append('event_type', eventType); + if (epsg) fd.append('epsg', epsg); + if (countries.length > 0) { + countries.forEach(c => fd.append('countries', c)); + } if (imageType === 'drone_image') { - fd.append('event_type', eventType || 'OTHER'); - fd.append('epsg', epsg || 'OTHER'); if (centerLon) fd.append('center_lon', centerLon); if (centerLat) fd.append('center_lat', centerLat); if (amslM) fd.append('amsl_m', amslM); @@ -583,24 +400,66 @@ export default function UploadPage() { if (rtkFix) fd.append('rtk_fix', rtkFix.toString()); if (stdHM) fd.append('std_h_m', stdHM); if (stdVM) fd.append('std_v_m', stdVM); - } else { - fd.append('source', source || 'OTHER'); - fd.append('event_type', eventType || 'OTHER'); - fd.append('epsg', epsg || 'OTHER'); - } - + } + + const modelName = localStorage.getItem(SELECTED_MODEL_KEY); + if (modelName) { + fd.append('model_name', modelName); + } + + const mapRes = await fetch('/api/images/', { method: 'POST', body: fd }); + const mapJson = await readJsonSafely(mapRes); + if (!mapRes.ok) throw new Error((mapJson.error as string) || 'Upload failed'); + console.log('DEBUG: Single upload response:', mapJson); + + await processUploadResponse(mapJson, false); + } + + async function handleMultiUpload() { + console.log('DEBUG: Starting multi-image upload'); + + const fd = new FormData(); + files.forEach(file => fd.append('files', file)); + fd.append('title', title); fd.append('image_type', imageType); - countries.forEach((c) => fd.append('countries', c)); + + // Add metadata for each image + metadataArray.forEach((metadata, index) => { + if (metadata.source) fd.append(`source_${index}`, metadata.source); + if (metadata.eventType) fd.append(`event_type_${index}`, metadata.eventType); + if (metadata.epsg) fd.append(`epsg_${index}`, metadata.epsg); + if (metadata.countries.length > 0) { + metadata.countries.forEach(c => fd.append(`countries_${index}`, c)); + } + if (imageType === 'drone_image') { + if (metadata.centerLon) fd.append(`center_lon_${index}`, metadata.centerLon); + if (metadata.centerLat) fd.append(`center_lat_${index}`, metadata.centerLat); + if (metadata.amslM) fd.append(`amsl_m_${index}`, metadata.amslM); + if (metadata.aglM) fd.append(`agl_m_${index}`, metadata.aglM); + if (metadata.headingDeg) fd.append(`heading_deg_${index}`, metadata.headingDeg); + if (metadata.yawDeg) fd.append(`yaw_deg_${index}`, metadata.yawDeg); + if (metadata.pitchDeg) fd.append(`pitch_deg_${index}`, metadata.pitchDeg); + if (metadata.rollDeg) fd.append(`roll_deg_${index}`, metadata.rollDeg); + if (metadata.rtkFix) fd.append(`rtk_fix_${index}`, metadata.rtkFix.toString()); + if (metadata.stdHM) fd.append(`std_h_m_${index}`, metadata.stdHM); + if (metadata.stdVM) fd.append(`std_v_m_${index}`, metadata.stdVM); + } + }); const modelName = localStorage.getItem(SELECTED_MODEL_KEY); if (modelName) { fd.append('model_name', modelName); } - try { - const mapRes = await fetch('/api/images/', { method: 'POST', body: fd }); + const mapRes = await fetch('/api/images/multi', { method: 'POST', body: fd }); const mapJson = await readJsonSafely(mapRes); if (!mapRes.ok) throw new Error((mapJson.error as string) || 'Upload failed'); + console.log('DEBUG: Multi upload response:', mapJson); + + await processUploadResponse(mapJson, true); + } + + async function processUploadResponse(mapJson: Record, isMultiUpload: boolean) { setImageUrl(mapJson.image_url as string); if (mapJson.preprocessing_info && @@ -615,142 +474,23 @@ export default function UploadPage() { if (!mapIdVal) throw new Error('Upload failed: image_id not found'); setUploadedImageId(mapIdVal); - const capRes = await fetch( - `/api/images/${mapIdVal}/caption`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - title: 'Generated Caption', - ...(modelName && { model_name: modelName }) - }) - }, - ); - const capJson = await readJsonSafely(capRes); - if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed'); - setUploadedImageId(mapIdVal); - - const fallbackInfo = (capJson.raw_json as Record)?.fallback_info; - if (fallbackInfo) { - setFallbackInfo({ - originalModel: (fallbackInfo as Record).original_model as string, - fallbackModel: (fallbackInfo as Record).fallback_model as string, - reason: (fallbackInfo as Record).reason as string - }); - setShowFallbackNotification(true); - } - - const extractedMetadata = (capJson.raw_json as Record)?.metadata; - if (extractedMetadata) { - const metadata = (extractedMetadata as Record).metadata || extractedMetadata; - if ((metadata as Record).title) setTitle((metadata as Record).title as string); - if ((metadata as Record).source) setSource((metadata as Record).source as string); - if ((metadata as Record).type) setEventType((metadata as Record).type as string); - if ((metadata as Record).epsg) setEpsg((metadata as Record).epsg as string); - if ((metadata as Record).countries && Array.isArray((metadata as Record).countries)) { - setCountries((metadata as Record).countries as string[]); - } - // Extract drone metadata if available - if (imageType === 'drone_image') { - if ((metadata as Record).center_lon) setCenterLon((metadata as Record).center_lon as string); - if ((metadata as Record).center_lat) setCenterLat((metadata as Record).center_lat as string); - if ((metadata as Record).amsl_m) setAmslM((metadata as Record).amsl_m as string); - if ((metadata as Record).agl_m) setAglM((metadata as Record).agl_m as string); - if ((metadata as Record).heading_deg) setHeadingDeg((metadata as Record).heading_deg as string); - if ((metadata as Record).yaw_deg) setYawDeg((metadata as Record).yaw_deg as string); - if ((metadata as Record).pitch_deg) setPitchDeg((metadata as Record).pitch_deg as string); - if ((metadata as Record).roll_deg) setRollDeg((metadata as Record).roll_deg as string); - if ((metadata as Record).rtk_fix !== undefined) setRtkFix((metadata as Record).rtk_fix as boolean); - if ((metadata as Record).std_h_m) setStdHM((metadata as Record).std_h_m as string); - if ((metadata as Record).std_v_m) setStdVM((metadata as Record).std_v_m as string); - } - } - - // Extract the three parts from raw_json.metadata - const extractedMetadataForParts = (capJson.raw_json as Record)?.metadata; - if (extractedMetadataForParts) { - if ((extractedMetadataForParts as Record).description) { - setDescription((extractedMetadataForParts as Record).description as string); - } - if ((extractedMetadataForParts as Record).analysis) { - setAnalysis((extractedMetadataForParts as Record).analysis as string); - } - if ((extractedMetadataForParts as Record).recommended_actions) { - setRecommendedActions((extractedMetadataForParts as Record).recommended_actions as string); - } - } - - // Set draft with the generated content for backward compatibility - if (capJson.generated) { - setDraft(capJson.generated as string); - } - handleStepChange('2a'); - } catch (err) { - handleApiError(err, 'Upload'); - } finally { - setIsLoading(false); - } - } + // Store image IDs + if (isMultiUpload) { + if (mapJson.image_ids && Array.isArray(mapJson.image_ids)) { + const imageIds = mapJson.image_ids as string[]; + console.log('DEBUG: Storing image IDs for multi-upload:', imageIds); + setUploadedImageIds(imageIds); + } else { + console.log('DEBUG: Multi-upload but no image_ids found, using single ID'); + setUploadedImageIds([mapIdVal]); + } + } else { + console.log('DEBUG: Storing single image ID:', mapIdVal); + setUploadedImageIds([mapIdVal]); + } + + const capJson = mapJson; - async function handleGenerateFromUrl() { - if (!imageUrl) return; - setIsLoading(true); - try { - const res = await fetch('/api/contribute/from-url', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - url: imageUrl, - source: imageType === 'drone_image' ? undefined : (source || 'OTHER'), - event_type: eventType || 'OTHER', - epsg: epsg || 'OTHER', - image_type: imageType, - countries, - ...(imageType === 'drone_image' && { - center_lon: centerLon || undefined, - center_lat: centerLat || undefined, - amsl_m: amslM || undefined, - agl_m: aglM || undefined, - heading_deg: headingDeg || undefined, - yaw_deg: yawDeg || undefined, - pitch_deg: pitchDeg || undefined, - roll_deg: rollDeg || undefined, - rtk_fix: rtkFix || undefined, - std_h_m: stdHM || undefined, - std_v_m: stdVM || undefined, - }), - }), - }); - const json = await readJsonSafely(res); - if (!res.ok) throw new Error((json.error as string) || 'Upload failed'); - - if (json.preprocessing_info && - typeof json.preprocessing_info === 'object' && - 'was_preprocessed' in json.preprocessing_info && - json.preprocessing_info.was_preprocessed === true) { - setPreprocessingInfo(json.preprocessing_info as any); - setShowPreprocessingNotification(true); - } - - const newId = json.image_id as string; - setUploadedImageId(newId); - setImageUrl(json.image_url as string); - - const modelName = localStorage.getItem(SELECTED_MODEL_KEY) || undefined; - const capRes = await fetch(`/api/images/${newId}/caption`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - title: 'Generated Caption', - // No prompt specified - backend will use active prompt for image type - ...(modelName && { model_name: modelName }), - }), - }); - const capJson = await readJsonSafely(capRes); - if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed'); - const fallbackInfo = (capJson.raw_json as Record)?.fallback_info; if (fallbackInfo) { setFallbackInfo({ @@ -764,29 +504,107 @@ export default function UploadPage() { const extractedMetadata = (capJson.raw_json as Record)?.metadata; if (extractedMetadata) { const metadata = (extractedMetadata as Record).metadata || extractedMetadata; - if ((metadata as Record).title) setTitle((metadata as Record).title as string); - if ((metadata as Record).source) setSource((metadata as Record).source as string); - if ((metadata as Record).type) setEventType((metadata as Record).type as string); - if ((metadata as Record).epsg) setEpsg((metadata as Record).epsg as string); - if ((metadata as Record).countries && Array.isArray((metadata as Record).countries)) { - setCountries((metadata as Record).countries as string[]); - } + + if (metadata && typeof metadata === 'object') { + const newMetadataArray = []; + + if (isMultiUpload) { + // Try to get individual image metadata first + const metadataImages = (metadata as Record).metadata_images; + if (metadataImages && typeof metadataImages === 'object') { + // Parse individual image metadata + for (let i = 1; i <= files.length; i++) { + const imageKey = `image${i}`; + const imageMetadata = (metadataImages as Record)[imageKey]; + + if (imageMetadata && typeof imageMetadata === 'object') { + const imgMeta = imageMetadata as Record; + newMetadataArray.push({ + source: imgMeta.source as string || '', + eventType: imgMeta.type as string || '', + epsg: imgMeta.epsg as string || '', + countries: Array.isArray(imgMeta.countries) ? imgMeta.countries as string[] : [], + centerLon: '', centerLat: '', amslM: '', aglM: '', + headingDeg: '', yawDeg: '', pitchDeg: '', rollDeg: '', + rtkFix: false, stdHM: '', stdVM: '' + }); + } else { + // Fallback to empty metadata for this image + newMetadataArray.push({ + source: '', eventType: '', epsg: '', countries: [], + centerLon: '', centerLat: '', amslM: '', aglM: '', + headingDeg: '', yawDeg: '', pitchDeg: '', rollDeg: '', + rtkFix: false, stdHM: '', stdVM: '' + }); + } + } + } else { + // Fallback to shared metadata if no individual metadata found + const sharedMetadata = { + source: (metadata as Record).source as string || '', + eventType: (metadata as Record).type as string || '', + epsg: (metadata as Record).epsg as string || '', + countries: Array.isArray((metadata as Record).countries) + ? (metadata as Record).countries as string[] + : [], + centerLon: '', centerLat: '', amslM: '', aglM: '', + headingDeg: '', yawDeg: '', pitchDeg: '', rollDeg: '', + rtkFix: false, stdHM: '', stdVM: '' + }; + + // Create metadata array with shared data for all images + for (let i = 0; i < files.length; i++) { + newMetadataArray.push({ ...sharedMetadata }); + } + } + } else { + // Single upload: use shared metadata + const sharedMetadata = { + source: (metadata as Record).source as string || '', + eventType: (metadata as Record).type as string || '', + epsg: (metadata as Record).epsg as string || '', + countries: Array.isArray((metadata as Record).countries) + ? (metadata as Record).countries as string[] + : [], + centerLon: '', centerLat: '', amslM: '', aglM: '', + headingDeg: '', yawDeg: '', pitchDeg: '', rollDeg: '', + rtkFix: false, stdHM: '', stdVM: '' + }; + newMetadataArray.push(sharedMetadata); + } + + setMetadataArray(newMetadataArray); + + if (newMetadataArray.length > 0) { + const firstMeta = newMetadataArray[0]; + // Set shared title from metadata + if (metadata && typeof metadata === 'object') { + const sharedTitle = (metadata as Record).title; + if (sharedTitle) { + setTitle(sharedTitle as string || ''); + } + } + setSource(firstMeta.source || ''); + setEventType(firstMeta.eventType || ''); + setEpsg(firstMeta.epsg || ''); + setCountries(firstMeta.countries || []); if (imageType === 'drone_image') { - if ((metadata as Record).center_lon) setCenterLon((metadata as Record).center_lon as string); - if ((metadata as Record).center_lat) setCenterLat((metadata as Record).center_lat as string); - if ((metadata as Record).amsl_m) setAmslM((metadata as Record).amsl_m as string); - if ((metadata as Record).agl_m) setAglM((metadata as Record).agl_m as string); - if ((metadata as Record).heading_deg) setHeadingDeg((metadata as Record).heading_deg as string); - if ((metadata as Record).yaw_deg) setYawDeg((metadata as Record).yaw_deg as string); - if ((metadata as Record).pitch_deg) setPitchDeg((metadata as Record).pitch_deg as string); - if ((metadata as Record).roll_deg) setRollDeg((metadata as Record).roll_deg as string); - if ((metadata as Record).rtk_fix !== undefined) setRtkFix((metadata as Record).rtk_fix as boolean); - if ((metadata as Record).std_h_m) setStdHM((metadata as Record).std_h_m as string); - if ((metadata as Record).std_v_m) setStdVM((metadata as Record).std_v_m as string); - } - } + setCenterLon(firstMeta.centerLon || ''); + setCenterLat(firstMeta.centerLat || ''); + setAmslM(firstMeta.amslM || ''); + setAglM(firstMeta.aglM || ''); + setHeadingDeg(firstMeta.headingDeg || ''); + setYawDeg(firstMeta.yawDeg || ''); + setPitchDeg(firstMeta.pitchDeg || ''); + setRollDeg(firstMeta.rollDeg || ''); + setRtkFix(firstMeta.rtkFix || false); + setStdHM(firstMeta.stdHM || ''); + setStdVM(firstMeta.stdVM || ''); + } + } + } + } - // Extract the three parts from raw_json.metadata const extractedMetadataForParts = (capJson.raw_json as Record)?.metadata; if (extractedMetadataForParts) { if ((extractedMetadataForParts as Record).description) { @@ -800,18 +618,11 @@ export default function UploadPage() { } } - // Set draft with the generated content for backward compatibility if (capJson.generated) { setDraft(capJson.generated as string); } handleStepChange('2a'); - } catch (err) { - handleApiError(err, 'Upload'); - } finally { - setIsLoading(false); - } } - async function handleSubmit() { console.log('handleSubmit called with:', { uploadedImageId, title, draft }); @@ -823,23 +634,40 @@ export default function UploadPage() { } try { - const metadataBody = { - source: imageType === 'drone_image' ? undefined : (source || 'OTHER'), - event_type: eventType || 'OTHER', + // Use stored image IDs for multi-image uploads + const imageIds = uploadedImageIds.length > 0 ? uploadedImageIds : [uploadedImageId!]; + console.log('DEBUG: Submit - Using image IDs:', imageIds); + console.log('DEBUG: Submit - uploadedImageIds:', uploadedImageIds); + console.log('DEBUG: Submit - uploadedImageId:', uploadedImageId); + + // Update metadata for each image + for (let i = 0; i < imageIds.length; i++) { + const imageId = imageIds[i]; + const metadata = metadataArray[i] || { + source: source || 'OTHER', + eventType: eventType || 'OTHER', epsg: epsg || 'OTHER', + countries: countries || [] + }; + + const metadataBody = { + source: imageType === 'drone_image' ? undefined : (metadata.source || 'OTHER'), + event_type: metadata.eventType || 'OTHER', + epsg: metadata.epsg || 'OTHER', image_type: imageType, - countries: countries, + countries: metadata.countries || [], }; - console.log('Updating metadata:', metadataBody); - const metadataRes = await fetch(`/api/images/${uploadedImageId}`, { + + console.log(`Updating metadata for image ${i + 1}:`, metadataBody); + const metadataRes = await fetch(`/api/images/${imageId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(metadataBody), }); const metadataJson = await readJsonSafely(metadataRes); - if (!metadataRes.ok) throw new Error((metadataJson.error as string) || "Metadata update failed"); + if (!metadataRes.ok) throw new Error((metadataJson.error as string) || `Metadata update failed for image ${i + 1}`); + } - // Combine the three parts for submission const combinedContent = `Description: ${description}\n\nAnalysis: ${analysis}\n\nRecommended Actions: ${recommendedActions}`; const captionBody = { @@ -859,6 +687,7 @@ export default function UploadPage() { if (!captionRes.ok) throw new Error((captionJson.error as string) || "Caption update failed"); setUploadedImageId(null); + setUploadedImageIds([]); handleStepChange(3); } catch (err) { handleApiError(err, 'Submit'); @@ -888,16 +717,74 @@ export default function UploadPage() { } setShowDeleteConfirm(false); - if (searchParams.get('isContribution') === 'true') { - navigate('/explore'); - } else { resetToStep1(); - } } catch (err) { handleApiError(err, 'Delete'); } } + const resetToStep1 = () => { + setIsPerformanceConfirmed(false); + setStep(1); + setFile(null); + setFiles([]); + setPreview(null); + setUploadedImageId(null); + setUploadedImageIds([]); + setImageUrl(null); + setTitle(''); + setSource(''); + setEventType(''); + setEpsg(''); + setCountries([]); + setCenterLon(''); + setCenterLat(''); + setAmslM(''); + setAglM(''); + setHeadingDeg(''); + setYawDeg(''); + setPitchDeg(''); + setRollDeg(''); + setRtkFix(false); + setStdHM(''); + setStdVM(''); + setScores({ accuracy: 50, context: 50, usability: 50 }); + setDraft(''); + setDescription(''); + setAnalysis(''); + setRecommendedActions(''); + setMetadataArray([]); + setShowFallbackNotification(false); + setFallbackInfo(null); + setShowPreprocessingNotification(false); + setPreprocessingInfo(null); + setShowPreprocessingModal(false); + setPreprocessingFile(null); + setIsPreprocessing(false); + setPreprocessingProgress(''); + setShowUnsupportedFormatModal(false); + setUnsupportedFile(null); + setShowFileSizeWarningModal(false); + setOversizedFile(null); + + // Clear URL parameters to prevent re-triggering contribute workflow + navigate('/upload', { replace: true }); + }; + + // Navigation handling + const handleNavigation = useCallback((to: string) => { + if (to === '/upload' || to === '/') { + return; + } + + if (uploadedImageIdRef.current) { + setPendingNavigation(to); + setShowNavigationConfirm(true); + } else { + navigate(to); + } + }, [navigate]); + async function confirmNavigation() { if (pendingNavigation && uploadedImageIdRef.current) { try { @@ -914,97 +801,290 @@ export default function UploadPage() { } } + // Preprocessing handlers + const handlePreprocessingConfirm = async () => { + if (!preprocessingFile) return; + + setIsPreprocessing(true); + setPreprocessingProgress('Starting file conversion...'); + + try { + const formData = new FormData(); + formData.append('file', preprocessingFile); + formData.append('preprocess_only', 'true'); + + setPreprocessingProgress('Converting file format...'); + + const response = await fetch('/api/images/preprocess', { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error('Preprocessing failed'); + } + + const result = await response.json(); + + setPreprocessingProgress('Finalizing conversion...'); + + const processedContent = atob(result.processed_content); + const processedBytes = new Uint8Array(processedContent.length); + for (let i = 0; i < processedContent.length; i++) { + processedBytes[i] = processedContent.charCodeAt(i); + } + + const processedFile = new File( + [processedBytes], + result.processed_filename, + { type: result.processed_mime_type } + ); + + const previewUrl = URL.createObjectURL(processedFile); + + // If this is the first file, set it as the single file + if (files.length === 0) { + setFile(processedFile); + setFiles([processedFile]); + } else { + // If files already exist, add to the array (multi-upload mode) + setFiles(prev => [...prev, processedFile]); + } + setPreview(previewUrl); + + setPreprocessingProgress('Conversion complete!'); + + setTimeout(() => { + setShowPreprocessingModal(false); + setPreprocessingFile(null); + setIsPreprocessing(false); + setPreprocessingProgress(''); + }, 1000); + + } catch (error) { + console.error('Preprocessing error:', error); + setPreprocessingProgress('Conversion failed. Please try again.'); + setTimeout(() => { + setShowPreprocessingModal(false); + setPreprocessingFile(null); + setIsPreprocessing(false); + setPreprocessingProgress(''); + }, 2000); + } + }; + + const handlePreprocessingCancel = () => { + setShowPreprocessingModal(false); + setPreprocessingFile(null); + setIsPreprocessing(false); + setPreprocessingProgress(''); + }; + + // Fetch contributed images from database and convert to File objects + const fetchContributedImages = async (imageIds: string[]) => { + setIsLoadingContribution(true); + try { + const filePromises = imageIds.map(async (imageId) => { + // Fetch image data from the API + const response = await fetch(`/api/images/${imageId}`); + if (!response.ok) { + throw new Error(`Failed to fetch image ${imageId}`); + } + const imageData = await response.json(); + + // Fetch the actual image file + const fileResponse = await fetch(`/api/images/${imageId}/file`); + if (!fileResponse.ok) { + throw new Error(`Failed to fetch image file ${imageId}`); + } + const blob = await fileResponse.blob(); + + // Create a File object from the blob + const fileName = imageData.file_key.split('/').pop() || `contributed_${imageId}.png`; + const file = new File([blob], fileName, { type: blob.type }); + + return { file, imageData }; + }); + + const contributedResults = await Promise.all(filePromises); + const contributedFiles = contributedResults.map(result => result.file); + const firstImageData = contributedResults[0]?.imageData; + + setFiles(contributedFiles); + + // Set the image IDs for submit process + setUploadedImageIds(imageIds); + if (imageIds.length === 1) { + setUploadedImageId(imageIds[0]); + } + + // Set the first file as the main file for single upload compatibility + if (contributedFiles.length >= 1) { + setFile(contributedFiles[0]); + } + + // Set the image type based on the contributed image's type + if (firstImageData?.image_type) { + setImageType(firstImageData.image_type); + } + + // Stay on step 1 to show the images in the file upload section + + } catch (error) { + console.error('Failed to fetch contributed images:', error); + alert(`Failed to load contributed images: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + setIsLoadingContribution(false); + } + }; + + // Effects + useEffect(() => { + Promise.all([ + fetch('/api/sources').then(r => r.json()), + fetch('/api/types').then(r => r.json()), + fetch('/api/spatial-references').then(r => r.json()), + fetch('/api/image-types').then(r => r.json()), + fetch('/api/countries').then(r => r.json()), + fetch('/api/models').then(r => r.json()) + ]).then(([sourcesData, typesData, spatialData, imageTypesData, countriesData, modelsData]) => { + if (!localStorage.getItem(SELECTED_MODEL_KEY) && modelsData?.length) { + localStorage.setItem(SELECTED_MODEL_KEY, modelsData[0].m_code); + } + setSources(sourcesData); + setTypes(typesData); + setSpatialReferences(spatialData); + setImageTypes(imageTypesData); + setCountriesOptions(countriesData); + + if (sourcesData.length > 0) setSource(sourcesData[0].s_code); + setEventType('OTHER'); + setEpsg('OTHER'); + if (imageTypesData.length > 0 && !searchParams.get('imageType') && !imageType) { + setImageType(imageTypesData[0].image_type); + } + }); + }, [searchParams, imageType]); + + useEffect(() => { + window.confirmNavigationIfNeeded = (to: string) => { + handleNavigation(to); + }; + + return () => { + delete window.confirmNavigationIfNeeded; + }; + }, [handleNavigation]); + + useEffect(() => { + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + if (uploadedImageIdRef.current) { + const message = 'You have an uploaded image that will be deleted if you leave this page. Are you sure you want to leave?'; + event.preventDefault(); + event.returnValue = message; + return message; + } + }; + + const handleCleanup = () => { + if (uploadedImageIdRef.current) { + fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error); + } + }; + + const handleGlobalClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const link = target.closest('a[href]') || target.closest('[data-navigate]'); + + if (link && uploadedImageIdRef.current) { + const href = link.getAttribute('href') || link.getAttribute('data-navigate'); + if (href && href !== '#' && !href.startsWith('javascript:') && !href.startsWith('mailto:')) { + event.preventDefault(); + event.stopPropagation(); + handleNavigation(href); + } + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + document.addEventListener('click', handleGlobalClick, true); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + document.removeEventListener('click', handleGlobalClick, true); + handleCleanup(); + }; + }, [handleNavigation]); + + useEffect(() => { + if (!file) { + setPreview(null); + return; + } + const url = URL.createObjectURL(file); + setPreview(url); + return () => URL.revokeObjectURL(url); + }, [file]); + + // Handle contribute parameter - fetch images from database + useEffect(() => { + const contribute = searchParams.get('contribute'); + const imageIds = searchParams.get('imageIds'); + + if (contribute === 'true' && imageIds) { + const ids = imageIds.split(',').filter(id => id.trim()); + if (ids.length > 0) { + fetchContributedImages(ids); + } + } + }, [searchParams]); + + // Reset carousel index when entering step 2b + useEffect(() => { + if (step === '2b') { + setCurrentImageIndex(0); + } + }, [step]); + + // Render return ( {step !== 3 && (
- {/* Drop-zone */} - {step === 1 && !searchParams.get('step') && ( -
-

- This app evaluates how well multimodal AI models analyze and describe - crisis maps and drone imagery. Upload an image and the AI will generate a description. - Then you can review and rate the result based on your expertise. -

- - {/* "More »" link */} -
- - More - -
- - {/* Image Type Selection */} -
- - handleImageTypeChange(value as string)} - options={[ - { key: 'crisis_map', label: 'Crisis Maps' }, - { key: 'drone_image', label: 'Drone Imagery' } - ]} - keySelector={(o) => o.key} - labelSelector={(o) => o.label} - /> - -
- -
e.preventDefault()} - onDrop={onDrop} - > - {file && preview ? ( -
-
- File preview -
-

- {file.name} -

-

- {(file.size / 1024 / 1024).toFixed(2)} MB -

-
- ) : ( - <> - -

Drag & Drop any file here

-

or

- - )} - - -
-
- )} + {/* Step 1: File Upload */} + {step === 1 && !searchParams.get('step') && !isLoadingContribution && ( + + )} + + {/* Step 1: Contributed Images Display */} + {step === 1 && searchParams.get('contribute') === 'true' && !isLoadingContribution && files.length > 0 && ( + + )} + {/* Loading States */} {isLoading && (
@@ -1019,19 +1099,20 @@ export default function UploadPage() {
)} - {step === 1 && !isLoading && ( + {/* Generate Button */} + {((step === 1 && !isLoading && !isLoadingContribution) || (step === 1 && searchParams.get('contribute') === 'true' && !isLoading && !isLoadingContribution && files.length > 0)) && (
{imageUrl ? ( ) : (
)} + {/* Step 2A: Metadata */} {step === '2a' && (
- -
-
- Uploaded image preview -
-
- -
-
-
+ { + setSelectedImageData(imageData || null); + setIsFullSizeModalOpen(true); + }} + />
- -
-
- setTitle(value || '')} - placeholder="Enter a title for this map..." - required - /> -
- {imageType !== 'drone_image' && ( - o.s_code} - labelSelector={(o) => o.label} - required - /> - )} - o.t_code} - labelSelector={(o) => o.label} - required={imageType !== 'drone_image'} - /> - o.epsg} - labelSelector={(o) => `${o.srid} (EPSG:${o.epsg})`} - placeholder="EPSG" - required={imageType !== 'drone_image'} - /> - o.image_type} - labelSelector={(o) => o.label} - required - /> - o.c_code} - labelSelector={(o) => o.label} - placeholder="Select one or more" + setTitle(value || '')} + onSourceChange={handleSourceChange} + onEventTypeChange={handleEventTypeChange} + onEpsgChange={handleEpsgChange} + onCountriesChange={handleCountriesChange} + onCenterLonChange={handleCenterLonChange} + onCenterLatChange={handleCenterLatChange} + onAmslMChange={handleAmslMChange} + onAglMChange={handleAglMChange} + onHeadingDegChange={handleHeadingDegChange} + onYawDegChange={handleYawDegChange} + onPitchDegChange={handlePitchDegChange} + onRollDegChange={handleRollDegChange} + onRtkFixChange={handleRtkFixChange} + onStdHMChange={handleStdHMChange} + onStdVMChange={handleStdVMChange} + onImageTypeChange={handleImageTypeChange} + updateMetadataForImage={updateMetadataForImage} /> - - {imageType === 'drone_image' && ( - <> -
-

Drone Flight Data

-
- - - - - - - - -
- -
- - -
-
- - )} -
-
)} + {/* Step 2B: Rating and Generated Text */} {step === '2b' && (
- {/* Top Row - Image and Rating horizontally aligned */} -
+
- -
-
- Uploaded image preview -
-
- -
-
-
-
- - {/* Rating Section */} -
- -
- {!isPerformanceConfirmed && ( - <> -

How well did the AI perform on the task?

- {(['accuracy', 'context', 'usability'] as const).map((k) => ( -
- - - setScores((s) => ({ ...s, [k]: Number(e.target.value) })) - } - className={styles.ratingInput} - /> - {scores[k]} -
- ))} -
- -
- - )} - {isPerformanceConfirmed && ( -
- -
- )} -
-
-
-
- - {/* Bottom Row - Generated Text spanning full width */} -
- -
-
-