import { useState, useEffect, useRef, useCallback } from 'react'; import { PageContainer, Heading, Button, Spinner, IconButton } from '@ifrc-go/ui'; import { DeleteBinLineIcon} 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'; export default function UploadPage() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [step, setStep] = useState<1 | '2a' | '2b' | 3>(1); const [isLoading, setIsLoading] = useState(false); const [isLoadingContribution, setIsLoadingContribution] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const stepRef = useRef(step); const uploadedImageIdRef = useRef(null); const [preview, setPreview] = useState(null); const [file, setFile] = useState(null); const [files, setFiles] = useState([]); const [source, setSource] = useState(''); const [eventType, setEventType] = useState(''); const [epsg, setEpsg] = useState(''); const [imageType, setImageType] = useState('crisis_map'); const [countries, setCountries] = useState([]); const [title, setTitle] = useState(''); // Drone metadata fields const [centerLon, setCenterLon] = useState(''); const [centerLat, setCenterLat] = useState(''); const [amslM, setAmslM] = useState(''); const [aglM, setAglM] = useState(''); const [headingDeg, setHeadingDeg] = useState(''); const [yawDeg, setYawDeg] = useState(''); const [pitchDeg, setPitchDeg] = useState(''); const [rollDeg, setRollDeg] = useState(''); const [rtkFix, setRtkFix] = useState(false); 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}[]>([]); const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]); const [countriesOptions, setCountriesOptions] = useState<{c_code: string, label: string, r_code: string}[]>([]); const [uploadedImageId, setUploadedImageId] = useState(null); 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(''); 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); const [showNavigationConfirm, setShowNavigationConfirm] = useState(false); const [pendingNavigation, setPendingNavigation] = useState(null); const [showFallbackNotification, setShowFallbackNotification] = useState(false); const [fallbackInfo, setFallbackInfo] = useState<{ originalModel: string; fallbackModel: string; reason: string; } | null>(null); const [showPreprocessingNotification, setShowPreprocessingNotification] = useState(false); const [preprocessingInfo, setPreprocessingInfo] = useState<{ original_filename: string; processed_filename: string; original_mime_type: string; processed_mime_type: string; 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(''); const [showUnsupportedFormatModal, setShowUnsupportedFormatModal] = useState(false); const [unsupportedFile, setUnsupportedFile] = useState(null); 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 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 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); } 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']; let needsPreprocess = !supportedTypes.includes(file.type); if (!needsPreprocess && file.name) { const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); needsPreprocess = !supportedExtensions.includes(fileExtension); } return needsPreprocess; }; const isCompletelyUnsupported = (file: File): boolean => { 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' ]; for (const unsupportedType of completelyUnsupportedTypes) { if (file.type.startsWith(unsupportedType)) { return true; } } if (file.name) { const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); const unsupportedExtensions = [ '.html', '.htm', '.css', '.js', '.json', '.txt', '.xml', '.zip', '.rar', '.7z', '.csv', '.xlsx', '.xls', '.pptx', '.ppt', '.docx', '.doc', '.mp3', '.mp4', '.avi', '.mov' ]; if (unsupportedExtensions.includes(fileExtension)) { return true; } } return false; }; const onFileChange = (file: File | undefined) => { if (file) { console.log('File selected:', 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; } 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 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; } 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]); } } } }; // API functions async function readJsonSafely(res: Response): Promise> { const text = await res.text(); try { return text ? JSON.parse(text) : {}; } catch { return { error: text }; } } function handleApiError(err: unknown, operation: string) { const message = err instanceof Error ? err.message : `Failed to ${operation.toLowerCase()}`; alert(message); } async function handleGenerate() { if (files.length === 0) return; setIsLoading(true); 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', 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') { if (centerLon) fd.append('center_lon', centerLon); if (centerLat) fd.append('center_lat', centerLat); if (amslM) fd.append('amsl_m', amslM); if (aglM) fd.append('agl_m', aglM); if (headingDeg) fd.append('heading_deg', headingDeg); if (yawDeg) fd.append('yaw_deg', yawDeg); if (pitchDeg) fd.append('pitch_deg', pitchDeg); if (rollDeg) fd.append('roll_deg', rollDeg); if (rtkFix) fd.append('rtk_fix', rtkFix.toString()); if (stdHM) fd.append('std_h_m', stdHM); if (stdVM) fd.append('std_v_m', stdVM); } 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); // 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); } 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 && typeof mapJson.preprocessing_info === 'object' && 'was_preprocessed' in mapJson.preprocessing_info && mapJson.preprocessing_info.was_preprocessed === true) { setPreprocessingInfo(mapJson.preprocessing_info as any); setShowPreprocessingNotification(true); } const mapIdVal = mapJson.image_id as string; if (!mapIdVal) throw new Error('Upload failed: image_id not found'); setUploadedImageId(mapIdVal); // 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; 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 && 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') { 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 || ''); } } } } 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); } } if (capJson.generated) { setDraft(capJson.generated as string); } handleStepChange('2a'); } async function handleSubmit() { console.log('handleSubmit called with:', { uploadedImageId, title, draft }); if (!uploadedImageId) return alert("No image to submit"); if (!isPerformanceConfirmed) { setShowRatingWarning(true); return; } setIsSubmitting(true); try { // 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: metadata.countries || [], }; 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 for image ${i + 1}`); } const combinedContent = `Description: ${description}\n\nAnalysis: ${analysis}\n\nRecommended Actions: ${recommendedActions}`; const captionBody = { title: title, edited: combinedContent, accuracy: scores.accuracy, context: scores.context, usability: scores.usability, }; console.log('Updating caption:', captionBody); const captionRes = await fetch(`/api/images/${uploadedImageId}/caption`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(captionBody), }); const captionJson = await readJsonSafely(captionRes); if (!captionRes.ok) throw new Error((captionJson.error as string) || "Caption update failed"); setUploadedImageId(null); setUploadedImageIds([]); handleStepChange(3); } catch (err) { handleApiError(err, 'Submit'); } finally { setIsSubmitting(false); } } async function handleDelete() { console.log('handleDelete called with uploadedImageId:', uploadedImageId); if (!uploadedImageId) { alert('No image to delete. Please try refreshing the page.'); return; } setShowDeleteConfirm(true); } async function confirmDelete() { try { console.log('Deleting image with ID:', uploadedImageId); const res = await fetch(`/api/images/${uploadedImageId}`, { method: "DELETE", }); if (!res.ok) { const json = await readJsonSafely(res); throw new Error((json.error as string) || `Delete failed with status ${res.status}`); } setShowDeleteConfirm(false); 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 { await fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }); setShowNavigationConfirm(false); setPendingNavigation(null); navigate(pendingNavigation); } catch (error) { console.error('Failed to delete image before navigation:', error); setShowNavigationConfirm(false); setPendingNavigation(null); navigate(pendingNavigation); } } } // 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 && (
{/* 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 && (

Generating...

)} {isLoadingContribution && (

Loading contribution...

)} {/* Generate Button */} {((step === 1 && !isLoading && !isLoadingContribution) || (step === 1 && searchParams.get('contribute') === 'true' && !isLoading && !isLoadingContribution && files.length > 0)) && (
{imageUrl ? ( ) : ( )}
)} {/* Step 2A: Metadata */} {step === '2a' && (
{ setSelectedImageData(imageData || null); setIsFullSizeModalOpen(true); }} />
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} />
)} {/* Step 2B: Rating and Generated Text */} {step === '2b' && (
{/* Submit Loading State */} {isSubmitting && (

Submitting...

)}
{ setSelectedImageData(imageData || null); setIsFullSizeModalOpen(true); }} currentImageIndex={currentImageIndex} onGoToPrevious={goToPrevious} onGoToNext={goToNext} onGoToImage={goToImage} showCarousel={true} />
{!isPerformanceConfirmed && (
setScores(prev => ({ ...prev, [key]: value }))} onConfirmRatings={() => setIsPerformanceConfirmed(true)} onEditRatings={() => setIsPerformanceConfirmed(false)} />
)}
setDescription(value || '')} onAnalysisChange={(value) => setAnalysis(value || '')} onRecommendedActionsChange={(value) => setRecommendedActions(value || '')} onBack={() => handleStepChange('2a')} onDelete={handleDelete} onSubmit={handleSubmit} onEditRatings={() => setIsPerformanceConfirmed(false)} isPerformanceConfirmed={isPerformanceConfirmed} isSubmitting={isSubmitting} />
)}
)} {/* Success page */} {step === 3 && (
Saved!

{searchParams.get('contribute') === 'true' ? 'Your contribution has been successfully saved.' : 'Your caption has been successfully saved.' }

)} {/* Modals */} { setIsFullSizeModalOpen(false); setSelectedImageData(null); }} /> setShowRatingWarning(false)} /> setShowDeleteConfirm(false)} /> setShowNavigationConfirm(false)} /> setShowFallbackNotification(false)} /> setShowPreprocessingNotification(false)} /> setShowUnsupportedFormatModal(false)} /> setShowFileSizeWarningModal(false)} onCancel={() => setShowFileSizeWarningModal(false)} />
); }