Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	Merge pull request #1 from SCGR-1/Oct_1
Browse files- README.md +87 -47
- frontend/README.md +0 -69
- frontend/src/pages/MapDetailsPage/MapDetailPage.tsx +148 -152
- py_backend/alembic/versions/0010_update_prompts_to_three_parts.py +3 -3
- py_backend/app/crud.py +10 -7
- py_backend/app/database.py +3 -1
- py_backend/app/main.py +98 -68
- py_backend/app/routers/caption.py +29 -27
- py_backend/app/routers/images.py +28 -26
- py_backend/app/routers/images_files.py +196 -0
- py_backend/app/routers/images_listing.py +129 -0
- py_backend/app/routers/images_metadata.py +150 -0
- py_backend/app/routers/images_upload.py +135 -0
- py_backend/app/routers/prompts.py +6 -4
- py_backend/app/routers/upload.py +36 -34
- py_backend/app/services/gpt4v_service.py +20 -17
- py_backend/app/services/image_preprocessor.py +11 -8
- py_backend/app/services/schema_validator.py +2 -2
- py_backend/app/services/thumbnail_service.py +9 -6
- py_backend/app/services/upload_service.py +293 -0
- py_backend/app/utils/__init__.py +1 -0
- py_backend/app/utils/image_utils.py +109 -0
- py_backend/requirements.txt +41 -26
- py_backend/static/assets/{AdminPage-0qyl8wHV.js β AdminPage-DLWomD88.js} +1 -1
- py_backend/static/assets/{ExportModal-BI_JlHOZ.js β ExportModal-C5df6JIW.js} +1 -1
- py_backend/static/assets/index-BTevbmbp.js +2 -0
- py_backend/static/assets/{index-C4_t-iUv.js β index-D3L5y9N7.js} +1 -1
- py_backend/static/assets/index-D5zar1LU.js +0 -2
- py_backend/static/assets/{index-CWQiqOU3.js β index-hD4RFL5O.js} +2 -2
- py_backend/static/assets/{index-B0L3qvtx.js β index-w0OOMPwN.js} +2 -2
- py_backend/static/assets/{jszip.min-uuN5m5dK.js β jszip.min-zGid42TK.js} +1 -1
- py_backend/static/assets/{useAdmin-DN0iYnPm.js β useAdmin-avReXN15.js} +1 -1
- py_backend/static/index.html +1 -1
    	
        README.md
    CHANGED
    
    | @@ -1,66 +1,106 @@ | |
| 1 | 
            -
            ---
         | 
| 2 | 
            -
            title: PromptAid Vision
         | 
| 3 | 
            -
            emoji: π
         | 
| 4 | 
            -
            colorFrom: blue
         | 
| 5 | 
            -
            colorTo: red
         | 
| 6 | 
            -
            sdk: docker
         | 
| 7 | 
            -
            app_port: 7860
         | 
| 8 | 
            -
            pinned: false
         | 
| 9 | 
            -
            ---
         | 
| 10 | 
            -
             | 
| 11 | 
             
            # PromptAid Vision
         | 
| 12 |  | 
| 13 | 
            -
             | 
| 14 |  | 
| 15 | 
            -
            ##  | 
| 16 |  | 
| 17 | 
            -
             | 
| 18 | 
            -
            - **Unit Tests**: `frontend/src/test/unit_tests/` - Component and hook testing with Vitest
         | 
| 19 | 
            -
            - **Integration Tests**: `frontend/src/test/integration/` - Component interaction testing
         | 
| 20 |  | 
| 21 | 
            -
             | 
| 22 | 
            -
            - **Unit Tests**: `py_backend/tests/unit_tests/` - Individual service testing
         | 
| 23 | 
            -
            - **Integration Tests**: `py_backend/tests/integration_tests/` - API and workflow testing
         | 
| 24 |  | 
| 25 | 
            -
             | 
| 26 | 
            -
            - ** | 
| 27 | 
            -
            - ** | 
|  | |
|  | |
| 28 |  | 
| 29 | 
             
            ## Quick Start
         | 
| 30 |  | 
| 31 | 
            -
            ###  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 32 | 
             
            ```bash
         | 
| 33 | 
            -
             | 
| 34 | 
            -
            cd  | 
| 35 | 
            -
             | 
| 36 | 
            -
            npm run dev
         | 
| 37 |  | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 42 | 
             
            ```
         | 
| 43 |  | 
| 44 | 
            -
             | 
|  | |
| 45 | 
             
            ```bash
         | 
| 46 | 
            -
            #  | 
| 47 | 
            -
            cd  | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
            # E2E tests
         | 
| 56 | 
            -
            cd e2e
         | 
| 57 | 
            -
            ./run_e2e_tests.sh
         | 
| 58 | 
             
            ```
         | 
| 59 |  | 
| 60 | 
             
            ## Project Structure
         | 
|  | |
| 61 | 
             
            ```
         | 
| 62 | 
            -
            βββ frontend/ | 
| 63 | 
            -
            βββ py_backend/ | 
| 64 | 
            -
            βββ e2e/ | 
| 65 | 
            -
            βββ .github/workflows/ | 
| 66 | 
             
            ```
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1 | 
             
            # PromptAid Vision
         | 
| 2 |  | 
| 3 | 
            +
            An AI-powered platform for crisis mapping and drone image analysis.
         | 
| 4 |  | 
| 5 | 
            +
            ## Overview
         | 
| 6 |  | 
| 7 | 
            +
            PromptAid Vision combines multiple AI vision models (GPT-4V, Gemini, Hugging Face) to analyze imagery for crisis response and humanitarian mapping. The platform provides automated image analysis, metadata extraction, and comprehensive reporting capabilities.
         | 
|  | |
|  | |
| 8 |  | 
| 9 | 
            +
            ## Architecture
         | 
|  | |
|  | |
| 10 |  | 
| 11 | 
            +
            - **Frontend**: React + TypeScript with Tailwind CSS
         | 
| 12 | 
            +
            - **Backend**: FastAPI + Python with PostgreSQL
         | 
| 13 | 
            +
            - **AI Models**: Multi-model support with intelligent fallbacks
         | 
| 14 | 
            +
            - **Storage**: Flexible local/S3 storage with image processing
         | 
| 15 | 
            +
            - **Testing**: Unit, integration, and E2E test coverage
         | 
| 16 |  | 
| 17 | 
             
            ## Quick Start
         | 
| 18 |  | 
| 19 | 
            +
            ### Prerequisites
         | 
| 20 | 
            +
            - Node.js 20+
         | 
| 21 | 
            +
            - Python 3.11+
         | 
| 22 | 
            +
            - PostgreSQL 16+
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            ### Development Setup
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            1. **Backend**
         | 
| 27 | 
            +
               ```bash
         | 
| 28 | 
            +
               cd py_backend
         | 
| 29 | 
            +
               python -m venv .venv
         | 
| 30 | 
            +
               source .venv/bin/activate  # Windows: .venv\Scripts\activate
         | 
| 31 | 
            +
               pip install -r requirements.txt
         | 
| 32 | 
            +
               alembic upgrade head
         | 
| 33 | 
            +
               uvicorn app.main:app --reload --port 7860
         | 
| 34 | 
            +
               ```
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            2. **Frontend**
         | 
| 37 | 
            +
               ```bash
         | 
| 38 | 
            +
               cd frontend
         | 
| 39 | 
            +
               npm install
         | 
| 40 | 
            +
               npm run dev
         | 
| 41 | 
            +
               ```
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            3. **Access**
         | 
| 44 | 
            +
               - Application: http://localhost:5173
         | 
| 45 | 
            +
               - API: http://localhost:7860
         | 
| 46 | 
            +
               - Documentation: http://localhost:7860/docs
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            ## Deployment
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            ### Local Production
         | 
| 51 | 
             
            ```bash
         | 
| 52 | 
            +
            cd frontend && npm run build
         | 
| 53 | 
            +
            cd py_backend && uvicorn app.main:app --host 0.0.0.0 --port 7860
         | 
| 54 | 
            +
            ```
         | 
|  | |
| 55 |  | 
| 56 | 
            +
            ### Docker
         | 
| 57 | 
            +
            ```bash
         | 
| 58 | 
            +
            docker-compose up --build
         | 
| 59 | 
            +
            ```
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            ### Hugging Face Spaces
         | 
| 62 | 
            +
            Automatically deployed via GitHub Actions workflow. Configure environment variables in Space settings.
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            ## Environment Variables
         | 
| 65 | 
            +
             | 
| 66 | 
            +
            ```bash
         | 
| 67 | 
            +
            DATABASE_URL=postgresql://user:password@localhost:5432/promptaid
         | 
| 68 | 
            +
            STORAGE_PROVIDER=local  # or s3
         | 
| 69 | 
            +
            OPENAI_API_KEY=your-key
         | 
| 70 | 
            +
            ANTHROPIC_API_KEY=your-key
         | 
| 71 | 
            +
            GOOGLE_API_KEY=your-key
         | 
| 72 | 
            +
            HUGGINGFACE_API_KEY=your-key
         | 
| 73 | 
             
            ```
         | 
| 74 |  | 
| 75 | 
            +
            ## Testing
         | 
| 76 | 
            +
             | 
| 77 | 
             
            ```bash
         | 
| 78 | 
            +
            # Backend
         | 
| 79 | 
            +
            cd py_backend && python -m pytest tests/ -v
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            # Frontend
         | 
| 82 | 
            +
            cd frontend && npm run test:unit
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            # E2E
         | 
| 85 | 
            +
            cd e2e && ./run_e2e_tests.sh
         | 
|  | |
|  | |
|  | |
|  | |
| 86 | 
             
            ```
         | 
| 87 |  | 
| 88 | 
             
            ## Project Structure
         | 
| 89 | 
            +
             | 
| 90 | 
             
            ```
         | 
| 91 | 
            +
            βββ frontend/          # React frontend
         | 
| 92 | 
            +
            βββ py_backend/        # FastAPI backend
         | 
| 93 | 
            +
            βββ e2e/              # End-to-end tests
         | 
| 94 | 
            +
            βββ .github/workflows/ # CI/CD pipelines
         | 
| 95 | 
             
            ```
         | 
| 96 | 
            +
             | 
| 97 | 
            +
            ## Contributing
         | 
| 98 | 
            +
             | 
| 99 | 
            +
            1. Fork the repository
         | 
| 100 | 
            +
            2. Create a feature branch
         | 
| 101 | 
            +
            3. Add tests for new functionality
         | 
| 102 | 
            +
            4. Submit a pull request
         | 
| 103 | 
            +
             | 
| 104 | 
            +
            ## License
         | 
| 105 | 
            +
             | 
| 106 | 
            +
            MIT License
         | 
    	
        frontend/README.md
    DELETED
    
    | @@ -1,69 +0,0 @@ | |
| 1 | 
            -
            # React + TypeScript + Vite
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
         | 
| 4 | 
            -
             | 
| 5 | 
            -
            Currently, two official plugins are available:
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
         | 
| 8 | 
            -
            - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
         | 
| 9 | 
            -
             | 
| 10 | 
            -
            ## Expanding the ESLint configuration
         | 
| 11 | 
            -
             | 
| 12 | 
            -
            If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
         | 
| 13 | 
            -
             | 
| 14 | 
            -
            ```js
         | 
| 15 | 
            -
            export default tseslint.config([
         | 
| 16 | 
            -
              globalIgnores(['dist']),
         | 
| 17 | 
            -
              {
         | 
| 18 | 
            -
                files: ['**/*.{ts,tsx}'],
         | 
| 19 | 
            -
                extends: [
         | 
| 20 | 
            -
                  // Other configs...
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                  // Remove tseslint.configs.recommended and replace with this
         | 
| 23 | 
            -
                  ...tseslint.configs.recommendedTypeChecked,
         | 
| 24 | 
            -
                  // Alternatively, use this for stricter rules
         | 
| 25 | 
            -
                  ...tseslint.configs.strictTypeChecked,
         | 
| 26 | 
            -
                  // Optionally, add this for stylistic rules
         | 
| 27 | 
            -
                  ...tseslint.configs.stylisticTypeChecked,
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                  // Other configs...
         | 
| 30 | 
            -
                ],
         | 
| 31 | 
            -
                languageOptions: {
         | 
| 32 | 
            -
                  parserOptions: {
         | 
| 33 | 
            -
                    project: ['./tsconfig.node.json', './tsconfig.app.json'],
         | 
| 34 | 
            -
                    tsconfigRootDir: import.meta.dirname,
         | 
| 35 | 
            -
                  },
         | 
| 36 | 
            -
                  // other options...
         | 
| 37 | 
            -
                },
         | 
| 38 | 
            -
              },
         | 
| 39 | 
            -
            ])
         | 
| 40 | 
            -
            ```
         | 
| 41 | 
            -
             | 
| 42 | 
            -
            You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
         | 
| 43 | 
            -
             | 
| 44 | 
            -
            ```js
         | 
| 45 | 
            -
            // eslint.config.js
         | 
| 46 | 
            -
            import reactX from 'eslint-plugin-react-x'
         | 
| 47 | 
            -
            import reactDom from 'eslint-plugin-react-dom'
         | 
| 48 | 
            -
             | 
| 49 | 
            -
            export default tseslint.config([
         | 
| 50 | 
            -
              globalIgnores(['dist']),
         | 
| 51 | 
            -
              {
         | 
| 52 | 
            -
                files: ['**/*.{ts,tsx}'],
         | 
| 53 | 
            -
                extends: [
         | 
| 54 | 
            -
                  // Other configs...
         | 
| 55 | 
            -
                  // Enable lint rules for React
         | 
| 56 | 
            -
                  reactX.configs['recommended-typescript'],
         | 
| 57 | 
            -
                  // Enable lint rules for React DOM
         | 
| 58 | 
            -
                  reactDom.configs.recommended,
         | 
| 59 | 
            -
                ],
         | 
| 60 | 
            -
                languageOptions: {
         | 
| 61 | 
            -
                  parserOptions: {
         | 
| 62 | 
            -
                    project: ['./tsconfig.node.json', './tsconfig.app.json'],
         | 
| 63 | 
            -
                    tsconfigRootDir: import.meta.dirname,
         | 
| 64 | 
            -
                  },
         | 
| 65 | 
            -
                  // other options...
         | 
| 66 | 
            -
                },
         | 
| 67 | 
            -
              },
         | 
| 68 | 
            -
            ])
         | 
| 69 | 
            -
            ```
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        frontend/src/pages/MapDetailsPage/MapDetailPage.tsx
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            import { PageContainer, Container, Button, Spinner, SegmentInput | 
| 2 | 
             
            import { useParams, useNavigate } from 'react-router-dom';
         | 
| 3 | 
             
            import { useState, useEffect, useMemo, useCallback } from 'react';
         | 
| 4 | 
             
            import { ChevronLeftLineIcon, ChevronRightLineIcon, DeleteBinLineIcon } from '@ifrc-go/icons';
         | 
| @@ -56,37 +56,7 @@ export default function MapDetailPage() { | |
| 56 | 
             
              const navigate = useNavigate();
         | 
| 57 | 
             
              const { isAuthenticated } = useAdmin();
         | 
| 58 |  | 
| 59 | 
            -
              //  | 
| 60 | 
            -
              console.log('MapDetailsPage: Current URL:', window.location.href);
         | 
| 61 | 
            -
              console.log('MapDetailsPage: Hash:', window.location.hash);
         | 
| 62 | 
            -
              console.log('MapDetailsPage: mapId from useParams:', mapId);
         | 
| 63 | 
            -
              console.log('MapDetailsPage: mapId type:', typeof mapId);
         | 
| 64 | 
            -
              console.log('MapDetailsPage: mapId length:', mapId?.length);
         | 
| 65 | 
            -
              console.log('MapDetailsPage: mapId value:', JSON.stringify(mapId));
         | 
| 66 | 
            -
              
         | 
| 67 | 
            -
              // Early validation - if mapId is invalid, show error immediately
         | 
| 68 | 
            -
              const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
         | 
| 69 | 
            -
              if (!mapId || mapId === 'undefined' || mapId === 'null' || mapId.trim() === '' || !uuidRegex.test(mapId)) {
         | 
| 70 | 
            -
                return (
         | 
| 71 | 
            -
                  <PageContainer>
         | 
| 72 | 
            -
                    <div className="flex flex-col items-center gap-4 text-center py-12">
         | 
| 73 | 
            -
                      <div className="text-4xl">β οΈ</div>
         | 
| 74 | 
            -
                      <div className="text-xl font-semibold">Invalid Map ID</div>
         | 
| 75 | 
            -
                      <div>The map ID provided is not valid.</div>
         | 
| 76 | 
            -
                      <div className="text-sm text-gray-500 mt-2">
         | 
| 77 | 
            -
                        Debug Info: mapId = "{mapId}" (type: {typeof mapId})
         | 
| 78 | 
            -
                      </div>
         | 
| 79 | 
            -
                      <Button
         | 
| 80 | 
            -
                        name="back-to-explore"
         | 
| 81 | 
            -
                        variant="secondary"
         | 
| 82 | 
            -
                        onClick={() => navigate('/explore')}
         | 
| 83 | 
            -
                      >
         | 
| 84 | 
            -
                        Return to Explore
         | 
| 85 | 
            -
                      </Button>
         | 
| 86 | 
            -
                    </div>
         | 
| 87 | 
            -
                  </PageContainer>
         | 
| 88 | 
            -
                );
         | 
| 89 | 
            -
              }
         | 
| 90 | 
             
              const [view, setView] = useState<'explore' | 'mapDetails'>('mapDetails');
         | 
| 91 | 
             
              const [map, setMap] = useState<MapOut | null>(null);
         | 
| 92 | 
             
              const [loading, setLoading] = useState(true);
         | 
| @@ -142,6 +112,92 @@ export default function MapDetailPage() { | |
| 142 | 
             
                { key: 'mapDetails' as const, label: 'Carousel' }
         | 
| 143 | 
             
              ];
         | 
| 144 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 145 | 
             
              const fetchMapData = useCallback(async (id: string) => {
         | 
| 146 | 
             
                console.log('fetchMapData called with id:', id);
         | 
| 147 | 
             
                console.log('fetchMapData id type:', typeof id);
         | 
| @@ -217,31 +273,6 @@ export default function MapDetailPage() { | |
| 217 | 
             
                }
         | 
| 218 | 
             
              }, [checkNavigationAvailability, fetchAllImages]);
         | 
| 219 |  | 
| 220 | 
            -
              const fetchAllImages = useCallback(async (imageIds: string[]) => {
         | 
| 221 | 
            -
                console.log('fetchAllImages called with imageIds:', imageIds);
         | 
| 222 | 
            -
                setIsLoadingImages(true);
         | 
| 223 | 
            -
                
         | 
| 224 | 
            -
                try {
         | 
| 225 | 
            -
                  const imagePromises = imageIds.map(async (imageId) => {
         | 
| 226 | 
            -
                    const response = await fetch(`/api/images/${imageId}`);
         | 
| 227 | 
            -
                    if (!response.ok) {
         | 
| 228 | 
            -
                      throw new Error(`Failed to fetch image ${imageId}`);
         | 
| 229 | 
            -
                    }
         | 
| 230 | 
            -
                    return response.json();
         | 
| 231 | 
            -
                  });
         | 
| 232 | 
            -
                  
         | 
| 233 | 
            -
                  const images = await Promise.all(imagePromises);
         | 
| 234 | 
            -
                  setAllImages(images);
         | 
| 235 | 
            -
                  setCurrentImageIndex(0);
         | 
| 236 | 
            -
                  console.log('fetchAllImages: Loaded', images.length, 'images');
         | 
| 237 | 
            -
                } catch (err: unknown) {
         | 
| 238 | 
            -
                  console.error('fetchAllImages error:', err);
         | 
| 239 | 
            -
                  setError(err instanceof Error ? err.message : 'Failed to load all images');
         | 
| 240 | 
            -
                } finally {
         | 
| 241 | 
            -
                  setIsLoadingImages(false);
         | 
| 242 | 
            -
                }
         | 
| 243 | 
            -
              }, []);
         | 
| 244 | 
            -
             | 
| 245 | 
             
              // Carousel navigation functions
         | 
| 246 | 
             
              const goToPrevious = useCallback(() => {
         | 
| 247 | 
             
                if (allImages.length > 1) {
         | 
| @@ -431,65 +462,6 @@ export default function MapDetailPage() { | |
| 431 | 
             
                }
         | 
| 432 | 
             
              }, [map, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples, mapId, navigate, loading, isDeleting]);
         | 
| 433 |  | 
| 434 | 
            -
              const checkNavigationAvailability = useCallback(async (currentId: string) => {
         | 
| 435 | 
            -
                // Validate the ID before making the request
         | 
| 436 | 
            -
                if (!currentId || currentId === 'undefined' || currentId === 'null' || currentId.trim() === '') {
         | 
| 437 | 
            -
                  return;
         | 
| 438 | 
            -
                }
         | 
| 439 | 
            -
             | 
| 440 | 
            -
                try {
         | 
| 441 | 
            -
             | 
| 442 | 
            -
                  const params = new URLSearchParams();
         | 
| 443 | 
            -
                  if (search) params.append('search', search);
         | 
| 444 | 
            -
                  if (srcFilter) params.append('source', srcFilter);
         | 
| 445 | 
            -
                  if (catFilter) params.append('event_type', catFilter);
         | 
| 446 | 
            -
                  if (regionFilter) params.append('region', regionFilter);
         | 
| 447 | 
            -
                  if (countryFilter) params.append('country', countryFilter);
         | 
| 448 | 
            -
                  if (imageTypeFilter) params.append('image_type', imageTypeFilter);
         | 
| 449 | 
            -
                  if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter);
         | 
| 450 | 
            -
                  if (showReferenceExamples) params.append('starred_only', 'true');
         | 
| 451 | 
            -
                  
         | 
| 452 | 
            -
                  const response = await fetch(`/api/images/grouped?${params.toString()}`);
         | 
| 453 | 
            -
                  if (response.ok) {
         | 
| 454 | 
            -
                    const filteredImages = await response.json();
         | 
| 455 | 
            -
                    
         | 
| 456 | 
            -
                    console.log('Server response for upload_type=multiple:', {
         | 
| 457 | 
            -
                      url: `/api/images/grouped?${params.toString()}`,
         | 
| 458 | 
            -
                      count: filteredImages.length,
         | 
| 459 | 
            -
                      images: filteredImages.map((img: any) => ({
         | 
| 460 | 
            -
                        image_id: img.image_id,
         | 
| 461 | 
            -
                        image_count: img.image_count,
         | 
| 462 | 
            -
                        all_image_ids: img.all_image_ids,
         | 
| 463 | 
            -
                        all_image_ids_length: img.all_image_ids?.length
         | 
| 464 | 
            -
                      }))
         | 
| 465 | 
            -
                    });
         | 
| 466 | 
            -
                    
         | 
| 467 | 
            -
                    const currentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === currentId);
         | 
| 468 | 
            -
                    
         | 
| 469 | 
            -
                    // Debug logging
         | 
| 470 | 
            -
                    console.log('Navigation availability check (server-side):', {
         | 
| 471 | 
            -
                      filteredImagesCount: filteredImages.length,
         | 
| 472 | 
            -
                      currentIndex,
         | 
| 473 | 
            -
                      currentId,
         | 
| 474 | 
            -
                      uploadTypeFilter,
         | 
| 475 | 
            -
                      hasPrevious: filteredImages.length > 1 && currentIndex > 0,
         | 
| 476 | 
            -
                      hasNext: filteredImages.length > 1 && currentIndex < filteredImages.length - 1,
         | 
| 477 | 
            -
                      filteredImages: filteredImages.map((img: any) => ({
         | 
| 478 | 
            -
                        image_id: img.image_id,
         | 
| 479 | 
            -
                        image_count: img.image_count,
         | 
| 480 | 
            -
                        all_image_ids: img.all_image_ids,
         | 
| 481 | 
            -
                        image_type: img.image_type
         | 
| 482 | 
            -
                      }))
         | 
| 483 | 
            -
                    });
         | 
| 484 | 
            -
                    
         | 
| 485 | 
            -
                    setHasPrevious(filteredImages.length > 1 && currentIndex > 0);
         | 
| 486 | 
            -
                    setHasNext(filteredImages.length > 1 && currentIndex < filteredImages.length - 1);
         | 
| 487 | 
            -
                  }
         | 
| 488 | 
            -
                } catch (error) {
         | 
| 489 | 
            -
                  console.error('Failed to check navigation availability:', error);
         | 
| 490 | 
            -
                }
         | 
| 491 | 
            -
              }, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]);
         | 
| 492 | 
            -
             | 
| 493 | 
             
              const navigateToItem = async (direction: 'previous' | 'next') => {
         | 
| 494 | 
             
                if (isNavigating) return;
         | 
| 495 |  | 
| @@ -735,6 +707,42 @@ export default function MapDetailPage() { | |
| 735 | 
             
                  setIsDeleting(false);
         | 
| 736 | 
             
                }
         | 
| 737 | 
             
              };
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 738 |  | 
| 739 | 
             
              const filteredMap = useMemo(() => {
         | 
| 740 | 
             
                if (!map) return null;
         | 
| @@ -776,42 +784,6 @@ export default function MapDetailPage() { | |
| 776 | 
             
                return matches ? map : null;
         | 
| 777 | 
             
              }, [map, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, navigateToMatchingImage]);
         | 
| 778 |  | 
| 779 | 
            -
              const navigateToMatchingImage = useCallback(async () => {
         | 
| 780 | 
            -
                setLoading(true);
         | 
| 781 | 
            -
                try {
         | 
| 782 | 
            -
                  // Use server-side filtering like ExplorePage
         | 
| 783 | 
            -
                  const params = new URLSearchParams();
         | 
| 784 | 
            -
                  if (search) params.append('search', search);
         | 
| 785 | 
            -
                  if (srcFilter) params.append('source', srcFilter);
         | 
| 786 | 
            -
                  if (catFilter) params.append('event_type', catFilter);
         | 
| 787 | 
            -
                  if (regionFilter) params.append('region', regionFilter);
         | 
| 788 | 
            -
                  if (countryFilter) params.append('country', countryFilter);
         | 
| 789 | 
            -
                  if (imageTypeFilter) params.append('image_type', imageTypeFilter);
         | 
| 790 | 
            -
                  if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter);
         | 
| 791 | 
            -
                  if (showReferenceExamples) params.append('starred_only', 'true');
         | 
| 792 | 
            -
                  
         | 
| 793 | 
            -
                  const response = await fetch(`/api/images/grouped?${params.toString()}`);
         | 
| 794 | 
            -
                  if (response.ok) {
         | 
| 795 | 
            -
                    const filteredImages = await response.json();
         | 
| 796 | 
            -
                    
         | 
| 797 | 
            -
                    if (filteredImages.length > 0) {
         | 
| 798 | 
            -
                      const firstMatchingImage = filteredImages[0];
         | 
| 799 | 
            -
                      if (firstMatchingImage && firstMatchingImage.image_id) {
         | 
| 800 | 
            -
                        navigate(`/map/${firstMatchingImage.image_id}`);
         | 
| 801 | 
            -
                      }
         | 
| 802 | 
            -
                    } else {
         | 
| 803 | 
            -
                      // No matching images, go back to explore
         | 
| 804 | 
            -
                      navigate('/explore');
         | 
| 805 | 
            -
                    }
         | 
| 806 | 
            -
                  }
         | 
| 807 | 
            -
                } catch (error) {
         | 
| 808 | 
            -
                  console.error('Failed to navigate to matching image:', error);
         | 
| 809 | 
            -
                  navigate('/explore');
         | 
| 810 | 
            -
                } finally {
         | 
| 811 | 
            -
                  setLoading(false);
         | 
| 812 | 
            -
                }
         | 
| 813 | 
            -
              }, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, navigate]);
         | 
| 814 | 
            -
             | 
| 815 | 
             
              const handleContribute = () => {
         | 
| 816 | 
             
                if (!map) return;
         | 
| 817 |  | 
| @@ -1122,6 +1094,30 @@ export default function MapDetailPage() { | |
| 1122 | 
             
                }
         | 
| 1123 | 
             
              };
         | 
| 1124 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1125 | 
             
              if (loading) {
         | 
| 1126 | 
             
                return (
         | 
| 1127 | 
             
                  <PageContainer>
         | 
|  | |
| 1 | 
            +
            import { PageContainer, Container, Button, Spinner, SegmentInput } from '@ifrc-go/ui';
         | 
| 2 | 
             
            import { useParams, useNavigate } from 'react-router-dom';
         | 
| 3 | 
             
            import { useState, useEffect, useMemo, useCallback } from 'react';
         | 
| 4 | 
             
            import { ChevronLeftLineIcon, ChevronRightLineIcon, DeleteBinLineIcon } from '@ifrc-go/icons';
         | 
|  | |
| 56 | 
             
              const navigate = useNavigate();
         | 
| 57 | 
             
              const { isAuthenticated } = useAdmin();
         | 
| 58 |  | 
| 59 | 
            +
              // All React Hooks must be called before any early returns
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 60 | 
             
              const [view, setView] = useState<'explore' | 'mapDetails'>('mapDetails');
         | 
| 61 | 
             
              const [map, setMap] = useState<MapOut | null>(null);
         | 
| 62 | 
             
              const [loading, setLoading] = useState(true);
         | 
|  | |
| 112 | 
             
                { key: 'mapDetails' as const, label: 'Carousel' }
         | 
| 113 | 
             
              ];
         | 
| 114 |  | 
| 115 | 
            +
              // Early validation will be moved after all hooks
         | 
| 116 | 
            +
             | 
| 117 | 
            +
              const checkNavigationAvailability = useCallback(async (currentId: string) => {
         | 
| 118 | 
            +
                // Validate the ID before making the request
         | 
| 119 | 
            +
                if (!currentId || currentId === 'undefined' || currentId === 'null' || currentId.trim() === '') {
         | 
| 120 | 
            +
                  return;
         | 
| 121 | 
            +
                }
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                try {
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  const params = new URLSearchParams();
         | 
| 126 | 
            +
                  if (search) params.append('search', search);
         | 
| 127 | 
            +
                  if (srcFilter) params.append('source', srcFilter);
         | 
| 128 | 
            +
                  if (catFilter) params.append('event_type', catFilter);
         | 
| 129 | 
            +
                  if (regionFilter) params.append('region', regionFilter);
         | 
| 130 | 
            +
                  if (countryFilter) params.append('country', countryFilter);
         | 
| 131 | 
            +
                  if (imageTypeFilter) params.append('image_type', imageTypeFilter);
         | 
| 132 | 
            +
                  if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter);
         | 
| 133 | 
            +
                  if (showReferenceExamples) params.append('starred_only', 'true');
         | 
| 134 | 
            +
                  
         | 
| 135 | 
            +
                  const response = await fetch(`/api/images/grouped?${params.toString()}`);
         | 
| 136 | 
            +
                  if (response.ok) {
         | 
| 137 | 
            +
                    const filteredImages = await response.json();
         | 
| 138 | 
            +
                    
         | 
| 139 | 
            +
                    console.log('Server response for upload_type=multiple:', {
         | 
| 140 | 
            +
                      url: `/api/images/grouped?${params.toString()}`,
         | 
| 141 | 
            +
                      count: filteredImages.length,
         | 
| 142 | 
            +
                      images: filteredImages.map((img: any) => ({
         | 
| 143 | 
            +
                        image_id: img.image_id,
         | 
| 144 | 
            +
                        image_count: img.image_count,
         | 
| 145 | 
            +
                        all_image_ids: img.all_image_ids,
         | 
| 146 | 
            +
                        all_image_ids_length: img.all_image_ids?.length
         | 
| 147 | 
            +
                      }))
         | 
| 148 | 
            +
                    });
         | 
| 149 | 
            +
                    
         | 
| 150 | 
            +
                    const currentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === currentId);
         | 
| 151 | 
            +
                    
         | 
| 152 | 
            +
                    // Debug logging
         | 
| 153 | 
            +
                    console.log('Navigation availability check (server-side):', {
         | 
| 154 | 
            +
                      filteredImagesCount: filteredImages.length,
         | 
| 155 | 
            +
                      currentIndex,
         | 
| 156 | 
            +
                      currentId,
         | 
| 157 | 
            +
                      uploadTypeFilter,
         | 
| 158 | 
            +
                      hasPrevious: filteredImages.length > 1 && currentIndex > 0,
         | 
| 159 | 
            +
                      hasNext: filteredImages.length > 1 && currentIndex < filteredImages.length - 1,
         | 
| 160 | 
            +
                      filteredImages: filteredImages.map((img: any) => ({
         | 
| 161 | 
            +
                        image_id: img.image_id,
         | 
| 162 | 
            +
                        image_count: img.image_count,
         | 
| 163 | 
            +
                        all_image_ids: img.all_image_ids,
         | 
| 164 | 
            +
                        image_type: img.image_type
         | 
| 165 | 
            +
                      }))
         | 
| 166 | 
            +
                    });
         | 
| 167 | 
            +
                    
         | 
| 168 | 
            +
                    setHasPrevious(filteredImages.length > 1 && currentIndex > 0);
         | 
| 169 | 
            +
                    setHasNext(filteredImages.length > 1 && currentIndex < filteredImages.length - 1);
         | 
| 170 | 
            +
                  }
         | 
| 171 | 
            +
                } catch (error) {
         | 
| 172 | 
            +
                  console.error('Failed to check navigation availability:', error);
         | 
| 173 | 
            +
                }
         | 
| 174 | 
            +
              }, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]);
         | 
| 175 | 
            +
             | 
| 176 | 
            +
              const fetchAllImages = useCallback(async (imageIds: string[]) => {
         | 
| 177 | 
            +
                console.log('fetchAllImages called with imageIds:', imageIds);
         | 
| 178 | 
            +
                setIsLoadingImages(true);
         | 
| 179 | 
            +
                
         | 
| 180 | 
            +
                try {
         | 
| 181 | 
            +
                  const imagePromises = imageIds.map(async (imageId) => {
         | 
| 182 | 
            +
                    const response = await fetch(`/api/images/${imageId}`);
         | 
| 183 | 
            +
                    if (!response.ok) {
         | 
| 184 | 
            +
                      throw new Error(`Failed to fetch image ${imageId}`);
         | 
| 185 | 
            +
                    }
         | 
| 186 | 
            +
                    return response.json();
         | 
| 187 | 
            +
                  });
         | 
| 188 | 
            +
                  
         | 
| 189 | 
            +
                  const images = await Promise.all(imagePromises);
         | 
| 190 | 
            +
                  setAllImages(images);
         | 
| 191 | 
            +
                  setCurrentImageIndex(0);
         | 
| 192 | 
            +
                  console.log('fetchAllImages: Loaded', images.length, 'images');
         | 
| 193 | 
            +
                } catch (err: unknown) {
         | 
| 194 | 
            +
                  console.error('fetchAllImages error:', err);
         | 
| 195 | 
            +
                  setError(err instanceof Error ? err.message : 'Failed to load all images');
         | 
| 196 | 
            +
                } finally {
         | 
| 197 | 
            +
                  setIsLoadingImages(false);
         | 
| 198 | 
            +
                }
         | 
| 199 | 
            +
              }, []);
         | 
| 200 | 
            +
             | 
| 201 | 
             
              const fetchMapData = useCallback(async (id: string) => {
         | 
| 202 | 
             
                console.log('fetchMapData called with id:', id);
         | 
| 203 | 
             
                console.log('fetchMapData id type:', typeof id);
         | 
|  | |
| 273 | 
             
                }
         | 
| 274 | 
             
              }, [checkNavigationAvailability, fetchAllImages]);
         | 
| 275 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 276 | 
             
              // Carousel navigation functions
         | 
| 277 | 
             
              const goToPrevious = useCallback(() => {
         | 
| 278 | 
             
                if (allImages.length > 1) {
         | 
|  | |
| 462 | 
             
                }
         | 
| 463 | 
             
              }, [map, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples, mapId, navigate, loading, isDeleting]);
         | 
| 464 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 465 | 
             
              const navigateToItem = async (direction: 'previous' | 'next') => {
         | 
| 466 | 
             
                if (isNavigating) return;
         | 
| 467 |  | 
|  | |
| 707 | 
             
                  setIsDeleting(false);
         | 
| 708 | 
             
                }
         | 
| 709 | 
             
              };
         | 
| 710 | 
            +
             | 
| 711 | 
            +
              const navigateToMatchingImage = useCallback(async () => {
         | 
| 712 | 
            +
                setLoading(true);
         | 
| 713 | 
            +
                try {
         | 
| 714 | 
            +
                  // Use server-side filtering like ExplorePage
         | 
| 715 | 
            +
                  const params = new URLSearchParams();
         | 
| 716 | 
            +
                  if (search) params.append('search', search);
         | 
| 717 | 
            +
                  if (srcFilter) params.append('source', srcFilter);
         | 
| 718 | 
            +
                  if (catFilter) params.append('event_type', catFilter);
         | 
| 719 | 
            +
                  if (regionFilter) params.append('region', regionFilter);
         | 
| 720 | 
            +
                  if (countryFilter) params.append('country', countryFilter);
         | 
| 721 | 
            +
                  if (imageTypeFilter) params.append('image_type', imageTypeFilter);
         | 
| 722 | 
            +
                  if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter);
         | 
| 723 | 
            +
                  if (showReferenceExamples) params.append('starred_only', 'true');
         | 
| 724 | 
            +
                  
         | 
| 725 | 
            +
                  const response = await fetch(`/api/images/grouped?${params.toString()}`);
         | 
| 726 | 
            +
                  if (response.ok) {
         | 
| 727 | 
            +
                    const filteredImages = await response.json();
         | 
| 728 | 
            +
                    
         | 
| 729 | 
            +
                    if (filteredImages.length > 0) {
         | 
| 730 | 
            +
                      const firstMatchingImage = filteredImages[0];
         | 
| 731 | 
            +
                      if (firstMatchingImage && firstMatchingImage.image_id) {
         | 
| 732 | 
            +
                        navigate(`/map/${firstMatchingImage.image_id}`);
         | 
| 733 | 
            +
                      }
         | 
| 734 | 
            +
                    } else {
         | 
| 735 | 
            +
                      // No matching images, go back to explore
         | 
| 736 | 
            +
                      navigate('/explore');
         | 
| 737 | 
            +
                    }
         | 
| 738 | 
            +
                  }
         | 
| 739 | 
            +
                } catch (error) {
         | 
| 740 | 
            +
                  console.error('Failed to navigate to matching image:', error);
         | 
| 741 | 
            +
                  navigate('/explore');
         | 
| 742 | 
            +
                } finally {
         | 
| 743 | 
            +
                  setLoading(false);
         | 
| 744 | 
            +
                }
         | 
| 745 | 
            +
              }, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, navigate]);
         | 
| 746 |  | 
| 747 | 
             
              const filteredMap = useMemo(() => {
         | 
| 748 | 
             
                if (!map) return null;
         | 
|  | |
| 784 | 
             
                return matches ? map : null;
         | 
| 785 | 
             
              }, [map, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, navigateToMatchingImage]);
         | 
| 786 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 787 | 
             
              const handleContribute = () => {
         | 
| 788 | 
             
                if (!map) return;
         | 
| 789 |  | 
|  | |
| 1094 | 
             
                }
         | 
| 1095 | 
             
              };
         | 
| 1096 |  | 
| 1097 | 
            +
              // Early validation - if mapId is invalid, show error immediately
         | 
| 1098 | 
            +
              const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
         | 
| 1099 | 
            +
              if (!mapId || mapId === 'undefined' || mapId === 'null' || mapId.trim() === '' || !uuidRegex.test(mapId)) {
         | 
| 1100 | 
            +
                return (
         | 
| 1101 | 
            +
                  <PageContainer>
         | 
| 1102 | 
            +
                    <div className="flex flex-col items-center gap-4 text-center py-12">
         | 
| 1103 | 
            +
                      <div className="text-4xl">β οΈ</div>
         | 
| 1104 | 
            +
                      <div className="text-xl font-semibold">Invalid Map ID</div>
         | 
| 1105 | 
            +
                      <div>The map ID provided is not valid.</div>
         | 
| 1106 | 
            +
                      <div className="text-sm text-gray-500 mt-2">
         | 
| 1107 | 
            +
                        Debug Info: mapId = "{mapId}" (type: {typeof mapId})
         | 
| 1108 | 
            +
                      </div>
         | 
| 1109 | 
            +
                      <Button
         | 
| 1110 | 
            +
                        name="back-to-explore"
         | 
| 1111 | 
            +
                        variant="secondary"
         | 
| 1112 | 
            +
                        onClick={() => navigate('/explore')}
         | 
| 1113 | 
            +
                      >
         | 
| 1114 | 
            +
                        Return to Explore
         | 
| 1115 | 
            +
                      </Button>
         | 
| 1116 | 
            +
                    </div>
         | 
| 1117 | 
            +
                  </PageContainer>
         | 
| 1118 | 
            +
                );
         | 
| 1119 | 
            +
              }
         | 
| 1120 | 
            +
             | 
| 1121 | 
             
              if (loading) {
         | 
| 1122 | 
             
                return (
         | 
| 1123 | 
             
                  <PageContainer>
         | 
    	
        py_backend/alembic/versions/0010_update_prompts_to_three_parts.py
    CHANGED
    
    | @@ -95,7 +95,7 @@ Return ONLY the JSON object (no markdown) in this envelope: | |
| 95 | 
             
                # Update default_caption@1.0.0 schema
         | 
| 96 | 
             
                op.execute(sa.text("""
         | 
| 97 | 
             
                    UPDATE json_schemas 
         | 
| 98 | 
            -
                    SET schema = '{"type":"object","properties":{"description":{"type":"string"},"analysis":{"type":"string"},"recommended_actions":{"type":"string"},"metadata":{"type":"object","properties":{"title":{"type":"string"},"source":{"type":"string"},"type":{"type":"string"},"countries":{"type":"array","items":{"type":"string"}},"epsg":{"type":"string"}}}},"required":["description","analysis","recommended_actions","metadata"]}'
         | 
| 99 | 
             
                    WHERE schema_id = 'default_caption@1.0.0'
         | 
| 100 | 
             
                """))
         | 
| 101 |  | 
| @@ -133,7 +133,7 @@ Return ONLY the JSON object (no markdown) in this envelope: | |
| 133 |  | 
| 134 | 
             
                op.execute(sa.text("""
         | 
| 135 | 
             
                    UPDATE json_schemas 
         | 
| 136 | 
            -
                    SET schema = :schema
         | 
| 137 | 
             
                    WHERE schema_id = 'drone_caption@1.0.0'
         | 
| 138 | 
             
                """).bindparams(schema=json.dumps(drone_schema, separators=(",", ":"))))
         | 
| 139 |  | 
| @@ -245,6 +245,6 @@ Return ONLY the JSON object (no markdown) in this envelope: | |
| 245 |  | 
| 246 | 
             
                op.execute(sa.text("""
         | 
| 247 | 
             
                    UPDATE json_schemas 
         | 
| 248 | 
            -
                    SET schema = :schema
         | 
| 249 | 
             
                    WHERE schema_id = 'drone_caption@1.0.0'
         | 
| 250 | 
             
                """).bindparams(schema=json.dumps(drone_schema, separators=(",", ":"))))
         | 
|  | |
| 95 | 
             
                # Update default_caption@1.0.0 schema
         | 
| 96 | 
             
                op.execute(sa.text("""
         | 
| 97 | 
             
                    UPDATE json_schemas 
         | 
| 98 | 
            +
                    SET schema = '{"type":"object","properties":{"description":{"type":"string"},"analysis":{"type":"string"},"recommended_actions":{"type":"string"},"metadata":{"type":"object","properties":{"title":{"type":"string"},"source":{"type":"string"},"type":{"type":"string"},"countries":{"type":"array","items":{"type":"string"}},"epsg":{"type":"string"}},"required":["title","source","type","countries","epsg"]}},"required":["description","analysis","recommended_actions","metadata"]}'
         | 
| 99 | 
             
                    WHERE schema_id = 'default_caption@1.0.0'
         | 
| 100 | 
             
                """))
         | 
| 101 |  | 
|  | |
| 133 |  | 
| 134 | 
             
                op.execute(sa.text("""
         | 
| 135 | 
             
                    UPDATE json_schemas 
         | 
| 136 | 
            +
                    SET schema = CAST(:schema AS jsonb)
         | 
| 137 | 
             
                    WHERE schema_id = 'drone_caption@1.0.0'
         | 
| 138 | 
             
                """).bindparams(schema=json.dumps(drone_schema, separators=(",", ":"))))
         | 
| 139 |  | 
|  | |
| 245 |  | 
| 246 | 
             
                op.execute(sa.text("""
         | 
| 247 | 
             
                    UPDATE json_schemas 
         | 
| 248 | 
            +
                    SET schema = CAST(:schema AS jsonb)
         | 
| 249 | 
             
                    WHERE schema_id = 'drone_caption@1.0.0'
         | 
| 250 | 
             
                """).bindparams(schema=json.dumps(drone_schema, separators=(",", ":"))))
         | 
    	
        py_backend/app/crud.py
    CHANGED
    
    | @@ -1,9 +1,12 @@ | |
| 1 | 
             
            import io, hashlib
         | 
|  | |
| 2 | 
             
            from typing import Optional, List
         | 
| 3 | 
             
            from sqlalchemy.orm import Session, joinedload
         | 
| 4 | 
             
            from . import models, schemas
         | 
| 5 | 
             
            from fastapi import HTTPException
         | 
| 6 |  | 
|  | |
|  | |
| 7 | 
             
            def hash_bytes(data: bytes) -> str:
         | 
| 8 | 
             
                """Compute SHA-256 hex digest of the data."""
         | 
| 9 | 
             
                return hashlib.sha256(data).hexdigest()
         | 
| @@ -88,10 +91,10 @@ def get_image(db: Session, image_id: str): | |
| 88 | 
             
                )
         | 
| 89 |  | 
| 90 | 
             
            def create_caption(db: Session, image_id, title, prompt, model_code, raw_json, text, metadata=None, image_count=None):
         | 
| 91 | 
            -
                 | 
| 92 | 
            -
                 | 
| 93 | 
            -
                 | 
| 94 | 
            -
                 | 
| 95 |  | 
| 96 | 
             
                if metadata:
         | 
| 97 | 
             
                    raw_json["extracted_metadata"] = metadata
         | 
| @@ -122,11 +125,11 @@ def create_caption(db: Session, image_id, title, prompt, model_code, raw_json, t | |
| 122 | 
             
                # Link caption to image
         | 
| 123 | 
             
                img.captions.append(caption)
         | 
| 124 |  | 
| 125 | 
            -
                 | 
| 126 | 
             
                db.commit()
         | 
| 127 | 
            -
                 | 
| 128 | 
             
                db.refresh(caption)
         | 
| 129 | 
            -
                 | 
| 130 | 
             
                return caption
         | 
| 131 |  | 
| 132 | 
             
            def get_caption(db: Session, caption_id: str):
         | 
|  | |
| 1 | 
             
            import io, hashlib
         | 
| 2 | 
            +
            import logging
         | 
| 3 | 
             
            from typing import Optional, List
         | 
| 4 | 
             
            from sqlalchemy.orm import Session, joinedload
         | 
| 5 | 
             
            from . import models, schemas
         | 
| 6 | 
             
            from fastapi import HTTPException
         | 
| 7 |  | 
| 8 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 9 | 
            +
             | 
| 10 | 
             
            def hash_bytes(data: bytes) -> str:
         | 
| 11 | 
             
                """Compute SHA-256 hex digest of the data."""
         | 
| 12 | 
             
                return hashlib.sha256(data).hexdigest()
         | 
|  | |
| 91 | 
             
                )
         | 
| 92 |  | 
| 93 | 
             
            def create_caption(db: Session, image_id, title, prompt, model_code, raw_json, text, metadata=None, image_count=None):
         | 
| 94 | 
            +
                logger.debug(f"Creating caption for image_id: {image_id}")
         | 
| 95 | 
            +
                logger.debug(f"Caption data: title={title}, prompt={prompt}, model={model_code}")
         | 
| 96 | 
            +
                logger.debug(f"Database session ID: {id(db)}")
         | 
| 97 | 
            +
                logger.debug(f"Database session is active: {db.is_active}")
         | 
| 98 |  | 
| 99 | 
             
                if metadata:
         | 
| 100 | 
             
                    raw_json["extracted_metadata"] = metadata
         | 
|  | |
| 125 | 
             
                # Link caption to image
         | 
| 126 | 
             
                img.captions.append(caption)
         | 
| 127 |  | 
| 128 | 
            +
                logger.debug(f"About to commit caption to database...")
         | 
| 129 | 
             
                db.commit()
         | 
| 130 | 
            +
                logger.debug(f"Caption commit successful!")
         | 
| 131 | 
             
                db.refresh(caption)
         | 
| 132 | 
            +
                logger.info(f"Caption created successfully for image: {img.image_id}")
         | 
| 133 | 
             
                return caption
         | 
| 134 |  | 
| 135 | 
             
            def get_caption(db: Session, caption_id: str):
         | 
    	
        py_backend/app/database.py
    CHANGED
    
    | @@ -6,6 +6,8 @@ from sqlalchemy.orm import sessionmaker, declarative_base | |
| 6 |  | 
| 7 | 
             
            from .config import settings
         | 
| 8 |  | 
|  | |
|  | |
| 9 | 
             
            raw_db_url = settings.DATABASE_URL
         | 
| 10 |  | 
| 11 | 
             
            if raw_db_url.startswith("psql '") and raw_db_url.endswith("'"):
         | 
| @@ -18,7 +20,7 @@ if raw_db_url.startswith("postgresql://") and not raw_db_url.startswith("postgre | |
| 18 | 
             
            if "sslmode=" not in raw_db_url and "localhost" not in raw_db_url and "127.0.0.1" not in raw_db_url:
         | 
| 19 | 
             
                raw_db_url = f"{raw_db_url}{'&' if '?' in raw_db_url else '?'}sslmode=require"
         | 
| 20 |  | 
| 21 | 
            -
             | 
| 22 |  | 
| 23 | 
             
            engine = create_engine(
         | 
| 24 | 
             
                raw_db_url,            
         | 
|  | |
| 6 |  | 
| 7 | 
             
            from .config import settings
         | 
| 8 |  | 
| 9 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 10 | 
            +
             | 
| 11 | 
             
            raw_db_url = settings.DATABASE_URL
         | 
| 12 |  | 
| 13 | 
             
            if raw_db_url.startswith("psql '") and raw_db_url.endswith("'"):
         | 
|  | |
| 20 | 
             
            if "sslmode=" not in raw_db_url and "localhost" not in raw_db_url and "127.0.0.1" not in raw_db_url:
         | 
| 21 | 
             
                raw_db_url = f"{raw_db_url}{'&' if '?' in raw_db_url else '?'}sslmode=require"
         | 
| 22 |  | 
| 23 | 
            +
            logger.debug(f"database url: {raw_db_url}")  
         | 
| 24 |  | 
| 25 | 
             
            engine = create_engine(
         | 
| 26 | 
             
                raw_db_url,            
         | 
    	
        py_backend/app/main.py
    CHANGED
    
    | @@ -1,5 +1,6 @@ | |
| 1 | 
             
            import os
         | 
| 2 | 
             
            import subprocess
         | 
|  | |
| 3 | 
             
            from datetime import datetime
         | 
| 4 | 
             
            from pathlib import Path
         | 
| 5 |  | 
| @@ -13,11 +14,27 @@ from dotenv import load_dotenv | |
| 13 | 
             
            load_dotenv()
         | 
| 14 |  | 
| 15 | 
             
            from app.config import settings
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 16 | 
             
            from app.routers import upload, caption, metadata, models
         | 
| 17 | 
             
            from app.routers.images import router as images_router
         | 
| 18 | 
             
            from app.routers.prompts import router as prompts_router
         | 
| 19 | 
             
            from app.routers.admin import router as admin_router
         | 
| 20 | 
             
            from app.routers.schemas import router as schemas_router
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 21 |  | 
| 22 | 
             
            app = FastAPI(
         | 
| 23 | 
             
                title="PromptAid Vision",
         | 
| @@ -34,9 +51,9 @@ app.add_middleware(GZipMiddleware, minimum_size=500) | |
| 34 | 
             
            # --------------------------------------------------------------------
         | 
| 35 | 
             
            @app.middleware("http")
         | 
| 36 | 
             
            async def log_requests(request: Request, call_next):
         | 
| 37 | 
            -
                 | 
| 38 | 
             
                response = await call_next(request)
         | 
| 39 | 
            -
                 | 
| 40 | 
             
                return response
         | 
| 41 |  | 
| 42 | 
             
            # --------------------------------------------------------------------
         | 
| @@ -83,14 +100,20 @@ app.add_middleware( | |
| 83 | 
             
            # --------------------------------------------------------------------
         | 
| 84 | 
             
            # API Routers
         | 
| 85 | 
             
            # --------------------------------------------------------------------
         | 
| 86 | 
            -
            app.include_router(caption.router, | 
| 87 | 
            -
            app.include_router(metadata.router, | 
| 88 | 
            -
            app.include_router(models.router, | 
| 89 | 
            -
             | 
| 90 | 
            -
            app.include_router( | 
| 91 | 
            -
             | 
| 92 | 
            -
            app.include_router( | 
| 93 | 
            -
            app.include_router( | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 94 |  | 
| 95 | 
             
            # Handle /api/images and /api/prompts without trailing slash (avoid 307)
         | 
| 96 | 
             
            @app.get("/api/images", include_in_schema=False)
         | 
| @@ -155,7 +178,7 @@ CANDIDATES = [ | |
| 155 | 
             
                Path("/app/app") / "static",      # some containers use /app/app
         | 
| 156 | 
             
            ]
         | 
| 157 | 
             
            STATIC_DIR = next((p for p in CANDIDATES if p.is_dir()), APP_DIR / "static")
         | 
| 158 | 
            -
             | 
| 159 |  | 
| 160 | 
             
            # --------------------------------------------------------------------
         | 
| 161 | 
             
            # Explicit top-level static files
         | 
| @@ -268,19 +291,19 @@ def spa_fallback(full_path: str): | |
| 268 | 
             
            def run_migrations():
         | 
| 269 | 
             
                """Run database migrations on startup"""
         | 
| 270 | 
             
                try:
         | 
| 271 | 
            -
                     | 
| 272 | 
             
                    current_dir = os.getcwd()
         | 
| 273 | 
            -
                     | 
| 274 |  | 
| 275 | 
             
                    try:
         | 
| 276 | 
             
                        result = subprocess.run(["which", "alembic"], capture_output=True, text=True)
         | 
| 277 | 
            -
                         | 
| 278 | 
             
                    except Exception as e:
         | 
| 279 | 
            -
                         | 
| 280 |  | 
| 281 | 
            -
                     | 
| 282 | 
             
                    if os.path.exists('/app'):
         | 
| 283 | 
            -
                         | 
| 284 |  | 
| 285 | 
             
                    alembic_paths = [
         | 
| 286 | 
             
                        "alembic.ini",
         | 
| @@ -292,14 +315,14 @@ def run_migrations(): | |
| 292 | 
             
                    for path in alembic_paths:
         | 
| 293 | 
             
                        if os.path.exists(path):
         | 
| 294 | 
             
                            alembic_dir = os.path.dirname(path)
         | 
| 295 | 
            -
                             | 
| 296 | 
             
                            break
         | 
| 297 | 
             
                    if not alembic_dir:
         | 
| 298 | 
            -
                         | 
| 299 | 
             
                        alembic_dir = current_dir
         | 
| 300 |  | 
| 301 | 
             
                    try:
         | 
| 302 | 
            -
                         | 
| 303 | 
             
                        result = subprocess.run(
         | 
| 304 | 
             
                            ["alembic", "upgrade", "head"],
         | 
| 305 | 
             
                            cwd=alembic_dir,
         | 
| @@ -307,46 +330,46 @@ def run_migrations(): | |
| 307 | 
             
                            text=True,
         | 
| 308 | 
             
                            timeout=60,
         | 
| 309 | 
             
                        )
         | 
| 310 | 
            -
                         | 
| 311 | 
            -
                         | 
| 312 | 
            -
                         | 
| 313 |  | 
| 314 | 
             
                        if result.returncode == 0:
         | 
| 315 | 
            -
                             | 
| 316 | 
             
                        else:
         | 
| 317 | 
            -
                             | 
| 318 | 
            -
                             | 
| 319 | 
             
                            try:
         | 
| 320 | 
             
                                from app.database import engine
         | 
| 321 | 
             
                                from app.models import Base
         | 
| 322 | 
             
                                Base.metadata.create_all(bind=engine)
         | 
| 323 | 
            -
                                 | 
| 324 | 
             
                            except Exception as fallback_error:
         | 
| 325 | 
            -
                                 | 
| 326 | 
             
                    except Exception as e:
         | 
| 327 | 
            -
                         | 
| 328 |  | 
| 329 | 
             
                except Exception as e:
         | 
| 330 | 
            -
                     | 
| 331 |  | 
| 332 | 
             
            def ensure_storage_ready():
         | 
| 333 | 
             
                """Ensure storage is ready before starting the app"""
         | 
| 334 | 
            -
                 | 
| 335 | 
            -
                 | 
| 336 | 
            -
                 | 
| 337 | 
             
                if settings.STORAGE_PROVIDER == "s3":
         | 
| 338 | 
             
                    try:
         | 
| 339 | 
            -
                         | 
| 340 | 
             
                        from app.storage import _ensure_bucket
         | 
| 341 | 
             
                        _ensure_bucket()
         | 
| 342 | 
            -
                         | 
| 343 | 
             
                    except Exception as e:
         | 
| 344 | 
            -
                         | 
| 345 | 
            -
                         | 
| 346 | 
             
                elif settings.STORAGE_PROVIDER == "local":
         | 
| 347 | 
            -
                     | 
| 348 | 
             
                else:
         | 
| 349 | 
            -
                     | 
| 350 |  | 
| 351 | 
             
            # --------------------------------------------------------------------
         | 
| 352 | 
             
            # VLM service registration on startup
         | 
| @@ -365,36 +388,47 @@ import asyncio | |
| 365 |  | 
| 366 |  | 
| 367 | 
             
            @app.on_event("startup")
         | 
| 368 | 
            -
            async def  | 
| 369 | 
            -
                """ | 
| 370 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 371 |  | 
| 372 | 
             
                # Always have a stub as a safe fallback
         | 
| 373 | 
             
                try:
         | 
| 374 | 
             
                    vlm_manager.register_service(StubVLMService())
         | 
| 375 | 
            -
                     | 
| 376 | 
             
                except Exception as e:
         | 
| 377 | 
            -
                     | 
| 378 |  | 
| 379 | 
             
                # OpenAI GPT-4V (if configured)
         | 
| 380 | 
             
                if settings.OPENAI_API_KEY:
         | 
| 381 | 
             
                    try:
         | 
| 382 | 
             
                        vlm_manager.register_service(GPT4VService(settings.OPENAI_API_KEY))
         | 
| 383 | 
            -
                         | 
| 384 | 
             
                    except Exception as e:
         | 
| 385 | 
            -
                         | 
| 386 | 
             
                else:
         | 
| 387 | 
            -
                     | 
| 388 |  | 
| 389 | 
             
                # Google Gemini (if configured)
         | 
| 390 | 
             
                if settings.GOOGLE_API_KEY:
         | 
| 391 | 
             
                    try:
         | 
| 392 | 
             
                        vlm_manager.register_service(GeminiService(settings.GOOGLE_API_KEY))
         | 
| 393 | 
            -
                         | 
| 394 | 
             
                    except Exception as e:
         | 
| 395 | 
            -
                         | 
| 396 | 
             
                else:
         | 
| 397 | 
            -
                     | 
| 398 |  | 
| 399 | 
             
                # Hugging Face Inference Providers (if configured)
         | 
| 400 | 
             
                if settings.HF_API_KEY:
         | 
| @@ -417,39 +451,35 @@ async def register_vlm_services() -> None: | |
| 417 | 
             
                                        public_name=m.m_code,  # stable name your UI/DB uses
         | 
| 418 | 
             
                                    )
         | 
| 419 | 
             
                                    vlm_manager.register_service(svc)
         | 
| 420 | 
            -
                                     | 
| 421 | 
             
                                    registered += 1
         | 
| 422 | 
             
                                except Exception as e:
         | 
| 423 | 
            -
                                     | 
| 424 | 
             
                            else:
         | 
| 425 | 
             
                                skipped += 1
         | 
| 426 |  | 
| 427 | 
             
                        if registered:
         | 
| 428 | 
            -
                             | 
| 429 | 
             
                        else:
         | 
| 430 | 
            -
                             | 
| 431 | 
             
                        if skipped:
         | 
| 432 | 
            -
                             | 
| 433 | 
             
                    finally:
         | 
| 434 | 
             
                        db.close()
         | 
| 435 | 
             
                else:
         | 
| 436 | 
            -
                     | 
| 437 |  | 
| 438 | 
            -
                # Kick off lightweight probes in the background (don | 
| 439 | 
             
                try:
         | 
| 440 | 
             
                    asyncio.create_task(vlm_manager.probe_all())
         | 
| 441 | 
             
                except Exception as e:
         | 
| 442 | 
            -
                     | 
| 443 | 
            -
             | 
| 444 | 
            -
                print(f"β Available models now: {', '.join(vlm_manager.get_available_models())}")
         | 
| 445 | 
            -
                print(f"β Total services: {len(vlm_manager.services)}")
         | 
| 446 |  | 
|  | |
|  | |
| 447 |  | 
| 448 | 
            -
            # Run startup tasks
         | 
| 449 | 
            -
            run_migrations()
         | 
| 450 | 
            -
            ensure_storage_ready()
         | 
| 451 |  | 
| 452 | 
            -
             | 
| 453 | 
            -
             | 
| 454 | 
            -
             | 
| 455 | 
            -
             | 
|  | |
| 1 | 
             
            import os
         | 
| 2 | 
             
            import subprocess
         | 
| 3 | 
            +
            import logging
         | 
| 4 | 
             
            from datetime import datetime
         | 
| 5 | 
             
            from pathlib import Path
         | 
| 6 |  | 
|  | |
| 14 | 
             
            load_dotenv()
         | 
| 15 |  | 
| 16 | 
             
            from app.config import settings
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            # Configure logging
         | 
| 19 | 
            +
            logging.basicConfig(
         | 
| 20 | 
            +
                level=logging.INFO,
         | 
| 21 | 
            +
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
         | 
| 22 | 
            +
                handlers=[
         | 
| 23 | 
            +
                    logging.StreamHandler(),
         | 
| 24 | 
            +
                ]
         | 
| 25 | 
            +
            )
         | 
| 26 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 27 | 
            +
             | 
| 28 | 
             
            from app.routers import upload, caption, metadata, models
         | 
| 29 | 
             
            from app.routers.images import router as images_router
         | 
| 30 | 
             
            from app.routers.prompts import router as prompts_router
         | 
| 31 | 
             
            from app.routers.admin import router as admin_router
         | 
| 32 | 
             
            from app.routers.schemas import router as schemas_router
         | 
| 33 | 
            +
            # New modular image routers
         | 
| 34 | 
            +
            from app.routers.images_listing import router as images_listing_router
         | 
| 35 | 
            +
            from app.routers.images_metadata import router as images_metadata_router
         | 
| 36 | 
            +
            from app.routers.images_files import router as images_files_router
         | 
| 37 | 
            +
            from app.routers.images_upload import router as images_upload_router
         | 
| 38 |  | 
| 39 | 
             
            app = FastAPI(
         | 
| 40 | 
             
                title="PromptAid Vision",
         | 
|  | |
| 51 | 
             
            # --------------------------------------------------------------------
         | 
| 52 | 
             
            @app.middleware("http")
         | 
| 53 | 
             
            async def log_requests(request: Request, call_next):
         | 
| 54 | 
            +
                logger.debug(f"{request.method} {request.url.path}")
         | 
| 55 | 
             
                response = await call_next(request)
         | 
| 56 | 
            +
                logger.debug(f"{request.method} {request.url.path} -> {response.status_code}")
         | 
| 57 | 
             
                return response
         | 
| 58 |  | 
| 59 | 
             
            # --------------------------------------------------------------------
         | 
|  | |
| 100 | 
             
            # --------------------------------------------------------------------
         | 
| 101 | 
             
            # API Routers
         | 
| 102 | 
             
            # --------------------------------------------------------------------
         | 
| 103 | 
            +
            app.include_router(caption.router,              prefix="/api",            tags=["captions"])
         | 
| 104 | 
            +
            app.include_router(metadata.router,             prefix="/api",            tags=["metadata"])
         | 
| 105 | 
            +
            app.include_router(models.router,               prefix="/api",            tags=["models"])
         | 
| 106 | 
            +
            # Legacy upload router (to be deprecated)
         | 
| 107 | 
            +
            app.include_router(upload.router,               prefix="/api/images",     tags=["images-legacy"])
         | 
| 108 | 
            +
            # New modular image routers
         | 
| 109 | 
            +
            app.include_router(images_listing_router,       prefix="/api/images",     tags=["images-listing"])
         | 
| 110 | 
            +
            app.include_router(images_metadata_router,      prefix="/api/images",     tags=["images-metadata"])
         | 
| 111 | 
            +
            app.include_router(images_files_router,         prefix="/api/images",     tags=["images-files"])
         | 
| 112 | 
            +
            app.include_router(images_upload_router,       prefix="/api/images",     tags=["images-upload"])
         | 
| 113 | 
            +
            app.include_router(images_router,              prefix="/api/contribute", tags=["contribute"])
         | 
| 114 | 
            +
            app.include_router(prompts_router,             prefix="/api/prompts",    tags=["prompts"])
         | 
| 115 | 
            +
            app.include_router(admin_router,               prefix="/api/admin",     tags=["admin"])
         | 
| 116 | 
            +
            app.include_router(schemas_router,             prefix="/api",            tags=["schemas"])
         | 
| 117 |  | 
| 118 | 
             
            # Handle /api/images and /api/prompts without trailing slash (avoid 307)
         | 
| 119 | 
             
            @app.get("/api/images", include_in_schema=False)
         | 
|  | |
| 178 | 
             
                Path("/app/app") / "static",      # some containers use /app/app
         | 
| 179 | 
             
            ]
         | 
| 180 | 
             
            STATIC_DIR = next((p for p in CANDIDATES if p.is_dir()), APP_DIR / "static")
         | 
| 181 | 
            +
            logger.info(f"Serving static files from: {STATIC_DIR}")
         | 
| 182 |  | 
| 183 | 
             
            # --------------------------------------------------------------------
         | 
| 184 | 
             
            # Explicit top-level static files
         | 
|  | |
| 291 | 
             
            def run_migrations():
         | 
| 292 | 
             
                """Run database migrations on startup"""
         | 
| 293 | 
             
                try:
         | 
| 294 | 
            +
                    logger.info("Running database migrations...")
         | 
| 295 | 
             
                    current_dir = os.getcwd()
         | 
| 296 | 
            +
                    logger.debug(f"Current working directory: {current_dir}")
         | 
| 297 |  | 
| 298 | 
             
                    try:
         | 
| 299 | 
             
                        result = subprocess.run(["which", "alembic"], capture_output=True, text=True)
         | 
| 300 | 
            +
                        logger.debug(f"Alembic location: {result.stdout.strip() if result.stdout else 'Not found'}")
         | 
| 301 | 
             
                    except Exception as e:
         | 
| 302 | 
            +
                        logger.debug(f"Could not check alembic location: {e}")
         | 
| 303 |  | 
| 304 | 
            +
                    logger.debug(f"Checking if /app exists: {os.path.exists('/app')}")
         | 
| 305 | 
             
                    if os.path.exists('/app'):
         | 
| 306 | 
            +
                        logger.debug(f"Contents of /app: {os.listdir('/app')}")
         | 
| 307 |  | 
| 308 | 
             
                    alembic_paths = [
         | 
| 309 | 
             
                        "alembic.ini",
         | 
|  | |
| 315 | 
             
                    for path in alembic_paths:
         | 
| 316 | 
             
                        if os.path.exists(path):
         | 
| 317 | 
             
                            alembic_dir = os.path.dirname(path)
         | 
| 318 | 
            +
                            logger.debug(f"Found alembic.ini at: {path}")
         | 
| 319 | 
             
                            break
         | 
| 320 | 
             
                    if not alembic_dir:
         | 
| 321 | 
            +
                        logger.warning("Could not find alembic.ini - using current directory")
         | 
| 322 | 
             
                        alembic_dir = current_dir
         | 
| 323 |  | 
| 324 | 
             
                    try:
         | 
| 325 | 
            +
                        logger.info(f"Running alembic upgrade head from: {alembic_dir}")
         | 
| 326 | 
             
                        result = subprocess.run(
         | 
| 327 | 
             
                            ["alembic", "upgrade", "head"],
         | 
| 328 | 
             
                            cwd=alembic_dir,
         | 
|  | |
| 330 | 
             
                            text=True,
         | 
| 331 | 
             
                            timeout=60,
         | 
| 332 | 
             
                        )
         | 
| 333 | 
            +
                        logger.debug(f"Alembic return code: {result.returncode}")
         | 
| 334 | 
            +
                        logger.debug(f"Alembic stdout: {result.stdout}")
         | 
| 335 | 
            +
                        logger.debug(f"Alembic stderr: {result.stderr}")
         | 
| 336 |  | 
| 337 | 
             
                        if result.returncode == 0:
         | 
| 338 | 
            +
                            logger.info("Database migrations completed successfully")
         | 
| 339 | 
             
                        else:
         | 
| 340 | 
            +
                            logger.error("Database migrations failed")
         | 
| 341 | 
            +
                            logger.warning("Trying fallback: create tables directly...")
         | 
| 342 | 
             
                            try:
         | 
| 343 | 
             
                                from app.database import engine
         | 
| 344 | 
             
                                from app.models import Base
         | 
| 345 | 
             
                                Base.metadata.create_all(bind=engine)
         | 
| 346 | 
            +
                                logger.info("Tables created directly via SQLAlchemy")
         | 
| 347 | 
             
                            except Exception as fallback_error:
         | 
| 348 | 
            +
                                logger.error(f"Fallback also failed: {fallback_error}")
         | 
| 349 | 
             
                    except Exception as e:
         | 
| 350 | 
            +
                        logger.error(f"Error running alembic: {e}")
         | 
| 351 |  | 
| 352 | 
             
                except Exception as e:
         | 
| 353 | 
            +
                    logger.error(f"Could not run migrations: {e}")
         | 
| 354 |  | 
| 355 | 
             
            def ensure_storage_ready():
         | 
| 356 | 
             
                """Ensure storage is ready before starting the app"""
         | 
| 357 | 
            +
                logger.debug(f"Storage provider from settings: '{settings.STORAGE_PROVIDER}'")
         | 
| 358 | 
            +
                logger.debug(f"S3 endpoint from settings: '{settings.S3_ENDPOINT}'")
         | 
| 359 | 
            +
                logger.debug(f"S3 bucket from settings: '{settings.S3_BUCKET}'")
         | 
| 360 | 
             
                if settings.STORAGE_PROVIDER == "s3":
         | 
| 361 | 
             
                    try:
         | 
| 362 | 
            +
                        logger.info("Checking S3 storage connection...")
         | 
| 363 | 
             
                        from app.storage import _ensure_bucket
         | 
| 364 | 
             
                        _ensure_bucket()
         | 
| 365 | 
            +
                        logger.info("S3 storage ready")
         | 
| 366 | 
             
                    except Exception as e:
         | 
| 367 | 
            +
                        logger.warning(f"S3 storage not ready: {e}")
         | 
| 368 | 
            +
                        logger.warning("Storage operations may fail until S3 is available")
         | 
| 369 | 
             
                elif settings.STORAGE_PROVIDER == "local":
         | 
| 370 | 
            +
                    logger.info("Using local storage - no external dependencies")
         | 
| 371 | 
             
                else:
         | 
| 372 | 
            +
                    logger.warning(f"Unknown storage provider: {settings.STORAGE_PROVIDER}")
         | 
| 373 |  | 
| 374 | 
             
            # --------------------------------------------------------------------
         | 
| 375 | 
             
            # VLM service registration on startup
         | 
|  | |
| 388 |  | 
| 389 |  | 
| 390 | 
             
            @app.on_event("startup")
         | 
| 391 | 
            +
            async def startup_tasks() -> None:
         | 
| 392 | 
            +
                """Run all startup tasks including migrations, storage setup, and VLM service registration."""
         | 
| 393 | 
            +
                logger.info("Starting application initialization...")
         | 
| 394 | 
            +
                
         | 
| 395 | 
            +
                # Run database migrations
         | 
| 396 | 
            +
                logger.info("Running database migrations...")
         | 
| 397 | 
            +
                run_migrations()
         | 
| 398 | 
            +
                
         | 
| 399 | 
            +
                # Ensure storage is ready
         | 
| 400 | 
            +
                logger.info("Checking storage...")
         | 
| 401 | 
            +
                ensure_storage_ready()
         | 
| 402 | 
            +
                
         | 
| 403 | 
            +
                # Register VLM services
         | 
| 404 | 
            +
                logger.info("Registering VLM services...")
         | 
| 405 |  | 
| 406 | 
             
                # Always have a stub as a safe fallback
         | 
| 407 | 
             
                try:
         | 
| 408 | 
             
                    vlm_manager.register_service(StubVLMService())
         | 
| 409 | 
            +
                    logger.info("β STUB_MODEL registered")
         | 
| 410 | 
             
                except Exception as e:
         | 
| 411 | 
            +
                    logger.error(f"β Failed to register STUB_MODEL: {e}")
         | 
| 412 |  | 
| 413 | 
             
                # OpenAI GPT-4V (if configured)
         | 
| 414 | 
             
                if settings.OPENAI_API_KEY:
         | 
| 415 | 
             
                    try:
         | 
| 416 | 
             
                        vlm_manager.register_service(GPT4VService(settings.OPENAI_API_KEY))
         | 
| 417 | 
            +
                        logger.info("β GPT-4 Vision service registered")
         | 
| 418 | 
             
                    except Exception as e:
         | 
| 419 | 
            +
                        logger.error(f"β GPT-4 Vision service failed to register: {e}")
         | 
| 420 | 
             
                else:
         | 
| 421 | 
            +
                    logger.info("β GPT-4 Vision not configured (OPENAI_API_KEY missing)")
         | 
| 422 |  | 
| 423 | 
             
                # Google Gemini (if configured)
         | 
| 424 | 
             
                if settings.GOOGLE_API_KEY:
         | 
| 425 | 
             
                    try:
         | 
| 426 | 
             
                        vlm_manager.register_service(GeminiService(settings.GOOGLE_API_KEY))
         | 
| 427 | 
            +
                        logger.info("β Gemini service registered")
         | 
| 428 | 
             
                    except Exception as e:
         | 
| 429 | 
            +
                        logger.error(f"β Gemini service failed to register: {e}")
         | 
| 430 | 
             
                else:
         | 
| 431 | 
            +
                    logger.info("β Gemini not configured (GOOGLE_API_KEY missing)")
         | 
| 432 |  | 
| 433 | 
             
                # Hugging Face Inference Providers (if configured)
         | 
| 434 | 
             
                if settings.HF_API_KEY:
         | 
|  | |
| 451 | 
             
                                        public_name=m.m_code,  # stable name your UI/DB uses
         | 
| 452 | 
             
                                    )
         | 
| 453 | 
             
                                    vlm_manager.register_service(svc)
         | 
| 454 | 
            +
                                    logger.info(f"β HF registered: {m.m_code} -> {m.model_id}")
         | 
| 455 | 
             
                                    registered += 1
         | 
| 456 | 
             
                                except Exception as e:
         | 
| 457 | 
            +
                                    logger.error(f"β HF model {m.m_code} failed to register: {e}")
         | 
| 458 | 
             
                            else:
         | 
| 459 | 
             
                                skipped += 1
         | 
| 460 |  | 
| 461 | 
             
                        if registered:
         | 
| 462 | 
            +
                            logger.info(f"β Hugging Face services registered: {registered}")
         | 
| 463 | 
             
                        else:
         | 
| 464 | 
            +
                            logger.info("β No Hugging Face models registered (none found or all skipped)")
         | 
| 465 | 
             
                        if skipped:
         | 
| 466 | 
            +
                            logger.info(f"βΉ HF skipped entries: {skipped}")
         | 
| 467 | 
             
                    finally:
         | 
| 468 | 
             
                        db.close()
         | 
| 469 | 
             
                else:
         | 
| 470 | 
            +
                    logger.info("β Hugging Face not configured (HF_API_KEY missing)")
         | 
| 471 |  | 
| 472 | 
            +
                # Kick off lightweight probes in the background (don't block startup)
         | 
| 473 | 
             
                try:
         | 
| 474 | 
             
                    asyncio.create_task(vlm_manager.probe_all())
         | 
| 475 | 
             
                except Exception as e:
         | 
| 476 | 
            +
                    logger.error(f"Probe scheduling failed: {e}")
         | 
|  | |
|  | |
|  | |
| 477 |  | 
| 478 | 
            +
                logger.info(f"β Available models now: {', '.join(vlm_manager.get_available_models())}")
         | 
| 479 | 
            +
                logger.info(f"β Total services: {len(vlm_manager.services)}")
         | 
| 480 |  | 
|  | |
|  | |
|  | |
| 481 |  | 
| 482 | 
            +
            logger.info("PromptAid Vision API server ready")
         | 
| 483 | 
            +
            logger.info("Endpoints: /api/images, /api/captions, /api/metadata, /api/models")
         | 
| 484 | 
            +
            logger.info(f"Environment: {settings.ENVIRONMENT}")
         | 
| 485 | 
            +
            logger.info("CORS: localhost + *.hf.space")
         | 
    	
        py_backend/app/routers/caption.py
    CHANGED
    
    | @@ -2,6 +2,7 @@ | |
| 2 | 
             
            from fastapi import APIRouter, HTTPException, Depends, Form, Request
         | 
| 3 | 
             
            from sqlalchemy.orm import Session
         | 
| 4 | 
             
            from typing import List
         | 
|  | |
| 5 |  | 
| 6 | 
             
            from .. import crud, database, schemas, storage
         | 
| 7 | 
             
            from ..services.vlm_service import vlm_manager
         | 
| @@ -9,6 +10,7 @@ from ..services.schema_validator import schema_validator | |
| 9 | 
             
            from ..config import settings
         | 
| 10 |  | 
| 11 | 
             
            router = APIRouter()
         | 
|  | |
| 12 |  | 
| 13 | 
             
            def get_db():
         | 
| 14 | 
             
                db = database.SessionLocal()
         | 
| @@ -28,7 +30,7 @@ async def create_caption( | |
| 28 | 
             
                model_name: str | None = Form(None),
         | 
| 29 | 
             
                db: Session = Depends(get_db),
         | 
| 30 | 
             
            ):
         | 
| 31 | 
            -
                 | 
| 32 |  | 
| 33 | 
             
                img = crud.get_image(db, image_id)
         | 
| 34 | 
             
                if not img:
         | 
| @@ -36,24 +38,24 @@ async def create_caption( | |
| 36 |  | 
| 37 | 
             
                # Get the prompt (explicit by code/label, or active for image type)
         | 
| 38 | 
             
                if prompt:
         | 
| 39 | 
            -
                     | 
| 40 | 
             
                    prompt_obj = crud.get_prompt(db, prompt) or crud.get_prompt_by_label(db, prompt)
         | 
| 41 | 
             
                else:
         | 
| 42 | 
            -
                     | 
| 43 | 
             
                    prompt_obj = crud.get_active_prompt_by_image_type(db, img.image_type)
         | 
| 44 |  | 
| 45 | 
            -
                 | 
| 46 | 
             
                if not prompt_obj:
         | 
| 47 | 
             
                    raise HTTPException(400, f"No prompt found (requested: '{prompt}' or active for type '{img.image_type}')")
         | 
| 48 |  | 
| 49 | 
             
                prompt_text = prompt_obj.label
         | 
| 50 | 
             
                metadata_instructions = prompt_obj.metadata_instructions or ""
         | 
| 51 | 
            -
                 | 
| 52 | 
            -
                 | 
| 53 |  | 
| 54 | 
             
                # Load image bytes (S3 or local)
         | 
| 55 | 
             
                try:
         | 
| 56 | 
            -
                     | 
| 57 | 
             
                    if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 58 | 
             
                        response = storage.s3.get_object(
         | 
| 59 | 
             
                            Bucket=settings.S3_BUCKET,
         | 
| @@ -66,7 +68,7 @@ async def create_caption( | |
| 66 | 
             
                        with open(file_path, 'rb') as f:
         | 
| 67 | 
             
                            img_bytes = f.read()
         | 
| 68 | 
             
                except Exception as e:
         | 
| 69 | 
            -
                     | 
| 70 | 
             
                    # fallback: try presigned/public URL
         | 
| 71 | 
             
                    try:
         | 
| 72 | 
             
                        url = storage.get_object_url(img.file_key)
         | 
| @@ -77,7 +79,7 @@ async def create_caption( | |
| 77 | 
             
                        resp.raise_for_status()
         | 
| 78 | 
             
                        img_bytes = resp.content
         | 
| 79 | 
             
                    except Exception as fallback_error:
         | 
| 80 | 
            -
                         | 
| 81 | 
             
                        raise HTTPException(500, f"Could not read image file: {e}")
         | 
| 82 |  | 
| 83 | 
             
                metadata = {}
         | 
| @@ -90,24 +92,24 @@ async def create_caption( | |
| 90 | 
             
                        db_session=db,
         | 
| 91 | 
             
                    )
         | 
| 92 |  | 
| 93 | 
            -
                     | 
| 94 | 
            -
                     | 
| 95 |  | 
| 96 | 
             
                    raw = result.get("raw_response", {})
         | 
| 97 |  | 
| 98 | 
             
                    # Validate and clean the data using schema validation
         | 
| 99 | 
             
                    image_type = img.image_type
         | 
| 100 | 
            -
                     | 
| 101 | 
            -
                     | 
| 102 |  | 
| 103 | 
             
                    cleaned_data, is_valid, validation_error = schema_validator.clean_and_validate_data(raw, image_type)
         | 
| 104 |  | 
| 105 | 
             
                    if is_valid:
         | 
| 106 | 
            -
                         | 
| 107 | 
             
                        text = cleaned_data.get("analysis", "")
         | 
| 108 | 
             
                        metadata = cleaned_data.get("metadata", {})
         | 
| 109 | 
             
                    else:
         | 
| 110 | 
            -
                         | 
| 111 | 
             
                        text = result.get("caption", "This is a fallback caption due to schema validation error.")
         | 
| 112 | 
             
                        metadata = result.get("metadata", {})
         | 
| 113 | 
             
                        raw["validation_error"] = validation_error
         | 
| @@ -115,7 +117,7 @@ async def create_caption( | |
| 115 |  | 
| 116 | 
             
                    used_model = result.get("model", model_name) or "STUB_MODEL"
         | 
| 117 | 
             
                    if used_model == "random":
         | 
| 118 | 
            -
                         | 
| 119 | 
             
                        used_model = "STUB_MODEL"
         | 
| 120 |  | 
| 121 | 
             
                    # Fallback info (if any)
         | 
| @@ -127,7 +129,7 @@ async def create_caption( | |
| 127 | 
             
                        }
         | 
| 128 |  | 
| 129 | 
             
                except Exception as e:
         | 
| 130 | 
            -
                     | 
| 131 | 
             
                    text = "This is a fallback caption due to VLM service error."
         | 
| 132 | 
             
                    used_model = "STUB_MODEL"
         | 
| 133 | 
             
                    raw = {"error": str(e), "fallback": True}
         | 
| @@ -145,8 +147,8 @@ async def create_caption( | |
| 145 | 
             
                )
         | 
| 146 |  | 
| 147 | 
             
                db.refresh(caption)
         | 
| 148 | 
            -
                 | 
| 149 | 
            -
                 | 
| 150 | 
             
                return schemas.CaptionOut.from_orm(caption)
         | 
| 151 |  | 
| 152 | 
             
            @router.get(
         | 
| @@ -158,9 +160,9 @@ def get_all_captions_legacy_format( | |
| 158 | 
             
                db: Session = Depends(get_db),
         | 
| 159 | 
             
            ):
         | 
| 160 | 
             
                """Get all images with captions in the old format for backward compatibility"""
         | 
| 161 | 
            -
                 | 
| 162 | 
             
                captions = crud.get_all_captions_with_images(db)
         | 
| 163 | 
            -
                 | 
| 164 |  | 
| 165 | 
             
                result = []
         | 
| 166 | 
             
                for caption in captions:
         | 
| @@ -170,7 +172,7 @@ def get_all_captions_legacy_format( | |
| 170 | 
             
                            from .upload import convert_image_to_dict
         | 
| 171 | 
             
                            base_url = str(request.base_url).rstrip('/')
         | 
| 172 | 
             
                            url = f"{base_url}/api/images/{image.image_id}/file"
         | 
| 173 | 
            -
                             | 
| 174 | 
             
                            img_dict = convert_image_to_dict(image, url)
         | 
| 175 |  | 
| 176 | 
             
                            # Overlay caption fields (legacy shape)
         | 
| @@ -190,7 +192,7 @@ def get_all_captions_legacy_format( | |
| 190 | 
             
                                "updated_at": caption.updated_at,
         | 
| 191 | 
             
                            })
         | 
| 192 | 
             
                            result.append(schemas.ImageOut(**img_dict))
         | 
| 193 | 
            -
                 | 
| 194 | 
             
                return result
         | 
| 195 |  | 
| 196 | 
             
            @router.get(
         | 
| @@ -201,16 +203,16 @@ def get_all_captions_with_images( | |
| 201 | 
             
                db: Session = Depends(get_db),
         | 
| 202 | 
             
            ):
         | 
| 203 | 
             
                """Get all captions"""
         | 
| 204 | 
            -
                 | 
| 205 | 
             
                captions = crud.get_all_captions_with_images(db)
         | 
| 206 | 
            -
                 | 
| 207 |  | 
| 208 | 
             
                result = []
         | 
| 209 | 
             
                for caption in captions:
         | 
| 210 | 
            -
                     | 
| 211 | 
             
                    db.refresh(caption)
         | 
| 212 | 
             
                    result.append(schemas.CaptionOut.from_orm(caption))
         | 
| 213 | 
            -
                 | 
| 214 | 
             
                return result
         | 
| 215 |  | 
| 216 | 
             
            @router.get(
         | 
|  | |
| 2 | 
             
            from fastapi import APIRouter, HTTPException, Depends, Form, Request
         | 
| 3 | 
             
            from sqlalchemy.orm import Session
         | 
| 4 | 
             
            from typing import List
         | 
| 5 | 
            +
            import logging
         | 
| 6 |  | 
| 7 | 
             
            from .. import crud, database, schemas, storage
         | 
| 8 | 
             
            from ..services.vlm_service import vlm_manager
         | 
|  | |
| 10 | 
             
            from ..config import settings
         | 
| 11 |  | 
| 12 | 
             
            router = APIRouter()
         | 
| 13 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 14 |  | 
| 15 | 
             
            def get_db():
         | 
| 16 | 
             
                db = database.SessionLocal()
         | 
|  | |
| 30 | 
             
                model_name: str | None = Form(None),
         | 
| 31 | 
             
                db: Session = Depends(get_db),
         | 
| 32 | 
             
            ):
         | 
| 33 | 
            +
                logger.debug(f"Received request - image_id: {image_id}, title: {title}, prompt: {prompt}, model_name: {model_name}")
         | 
| 34 |  | 
| 35 | 
             
                img = crud.get_image(db, image_id)
         | 
| 36 | 
             
                if not img:
         | 
|  | |
| 38 |  | 
| 39 | 
             
                # Get the prompt (explicit by code/label, or active for image type)
         | 
| 40 | 
             
                if prompt:
         | 
| 41 | 
            +
                    logger.debug(f"Looking for prompt: '{prompt}' (type: {type(prompt)})")
         | 
| 42 | 
             
                    prompt_obj = crud.get_prompt(db, prompt) or crud.get_prompt_by_label(db, prompt)
         | 
| 43 | 
             
                else:
         | 
| 44 | 
            +
                    logger.debug(f"Looking for active prompt for image type: {img.image_type}")
         | 
| 45 | 
             
                    prompt_obj = crud.get_active_prompt_by_image_type(db, img.image_type)
         | 
| 46 |  | 
| 47 | 
            +
                logger.debug(f"Prompt lookup result: {prompt_obj}")
         | 
| 48 | 
             
                if not prompt_obj:
         | 
| 49 | 
             
                    raise HTTPException(400, f"No prompt found (requested: '{prompt}' or active for type '{img.image_type}')")
         | 
| 50 |  | 
| 51 | 
             
                prompt_text = prompt_obj.label
         | 
| 52 | 
             
                metadata_instructions = prompt_obj.metadata_instructions or ""
         | 
| 53 | 
            +
                logger.debug(f"Using prompt text: '{prompt_text}'")
         | 
| 54 | 
            +
                logger.debug(f"Using metadata instructions: '{metadata_instructions[:100]}...'")
         | 
| 55 |  | 
| 56 | 
             
                # Load image bytes (S3 or local)
         | 
| 57 | 
             
                try:
         | 
| 58 | 
            +
                    logger.debug(f"About to call VLM service with model_name: {model_name}")
         | 
| 59 | 
             
                    if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 60 | 
             
                        response = storage.s3.get_object(
         | 
| 61 | 
             
                            Bucket=settings.S3_BUCKET,
         | 
|  | |
| 68 | 
             
                        with open(file_path, 'rb') as f:
         | 
| 69 | 
             
                            img_bytes = f.read()
         | 
| 70 | 
             
                except Exception as e:
         | 
| 71 | 
            +
                    logger.error(f"Error reading image file: {e}")
         | 
| 72 | 
             
                    # fallback: try presigned/public URL
         | 
| 73 | 
             
                    try:
         | 
| 74 | 
             
                        url = storage.get_object_url(img.file_key)
         | 
|  | |
| 79 | 
             
                        resp.raise_for_status()
         | 
| 80 | 
             
                        img_bytes = resp.content
         | 
| 81 | 
             
                    except Exception as fallback_error:
         | 
| 82 | 
            +
                        logger.error(f"Fallback also failed: {fallback_error}")
         | 
| 83 | 
             
                        raise HTTPException(500, f"Could not read image file: {e}")
         | 
| 84 |  | 
| 85 | 
             
                metadata = {}
         | 
|  | |
| 92 | 
             
                        db_session=db,
         | 
| 93 | 
             
                    )
         | 
| 94 |  | 
| 95 | 
            +
                    logger.debug(f"VLM service result: {result}")
         | 
| 96 | 
            +
                    logger.debug(f"Result model field: {result.get('model', 'NOT_FOUND')}")
         | 
| 97 |  | 
| 98 | 
             
                    raw = result.get("raw_response", {})
         | 
| 99 |  | 
| 100 | 
             
                    # Validate and clean the data using schema validation
         | 
| 101 | 
             
                    image_type = img.image_type
         | 
| 102 | 
            +
                    logger.debug(f"Validating data for image type: {image_type}")
         | 
| 103 | 
            +
                    logger.debug(f"Raw data structure: {list(raw.keys()) if isinstance(raw, dict) else 'Not a dict'}")
         | 
| 104 |  | 
| 105 | 
             
                    cleaned_data, is_valid, validation_error = schema_validator.clean_and_validate_data(raw, image_type)
         | 
| 106 |  | 
| 107 | 
             
                    if is_valid:
         | 
| 108 | 
            +
                        logger.debug(f"β Schema validation passed for {image_type}")
         | 
| 109 | 
             
                        text = cleaned_data.get("analysis", "")
         | 
| 110 | 
             
                        metadata = cleaned_data.get("metadata", {})
         | 
| 111 | 
             
                    else:
         | 
| 112 | 
            +
                        logger.debug(f"β  Schema validation failed for {image_type}: {validation_error}")
         | 
| 113 | 
             
                        text = result.get("caption", "This is a fallback caption due to schema validation error.")
         | 
| 114 | 
             
                        metadata = result.get("metadata", {})
         | 
| 115 | 
             
                        raw["validation_error"] = validation_error
         | 
|  | |
| 117 |  | 
| 118 | 
             
                    used_model = result.get("model", model_name) or "STUB_MODEL"
         | 
| 119 | 
             
                    if used_model == "random":
         | 
| 120 | 
            +
                        logger.warning(f"VLM service returned 'random' as model name, using STUB_MODEL fallback")
         | 
| 121 | 
             
                        used_model = "STUB_MODEL"
         | 
| 122 |  | 
| 123 | 
             
                    # Fallback info (if any)
         | 
|  | |
| 129 | 
             
                        }
         | 
| 130 |  | 
| 131 | 
             
                except Exception as e:
         | 
| 132 | 
            +
                    logger.warning(f"VLM error, using fallback: {e}")
         | 
| 133 | 
             
                    text = "This is a fallback caption due to VLM service error."
         | 
| 134 | 
             
                    used_model = "STUB_MODEL"
         | 
| 135 | 
             
                    raw = {"error": str(e), "fallback": True}
         | 
|  | |
| 147 | 
             
                )
         | 
| 148 |  | 
| 149 | 
             
                db.refresh(caption)
         | 
| 150 | 
            +
                logger.debug(f"Caption created, caption object: {caption}")
         | 
| 151 | 
            +
                logger.debug(f"caption_id: {caption.caption_id}")
         | 
| 152 | 
             
                return schemas.CaptionOut.from_orm(caption)
         | 
| 153 |  | 
| 154 | 
             
            @router.get(
         | 
|  | |
| 160 | 
             
                db: Session = Depends(get_db),
         | 
| 161 | 
             
            ):
         | 
| 162 | 
             
                """Get all images with captions in the old format for backward compatibility"""
         | 
| 163 | 
            +
                logger.debug(f"Fetching all captions in legacy format...")
         | 
| 164 | 
             
                captions = crud.get_all_captions_with_images(db)
         | 
| 165 | 
            +
                logger.debug(f"Found {len(captions)} captions")
         | 
| 166 |  | 
| 167 | 
             
                result = []
         | 
| 168 | 
             
                for caption in captions:
         | 
|  | |
| 172 | 
             
                            from .upload import convert_image_to_dict
         | 
| 173 | 
             
                            base_url = str(request.base_url).rstrip('/')
         | 
| 174 | 
             
                            url = f"{base_url}/api/images/{image.image_id}/file"
         | 
| 175 | 
            +
                            logger.debug(f"Generated image URL: {url}")
         | 
| 176 | 
             
                            img_dict = convert_image_to_dict(image, url)
         | 
| 177 |  | 
| 178 | 
             
                            # Overlay caption fields (legacy shape)
         | 
|  | |
| 192 | 
             
                                "updated_at": caption.updated_at,
         | 
| 193 | 
             
                            })
         | 
| 194 | 
             
                            result.append(schemas.ImageOut(**img_dict))
         | 
| 195 | 
            +
                logger.debug(f"Returning {len(result)} legacy format results")
         | 
| 196 | 
             
                return result
         | 
| 197 |  | 
| 198 | 
             
            @router.get(
         | 
|  | |
| 203 | 
             
                db: Session = Depends(get_db),
         | 
| 204 | 
             
            ):
         | 
| 205 | 
             
                """Get all captions"""
         | 
| 206 | 
            +
                logger.debug(f"Fetching all captions...")
         | 
| 207 | 
             
                captions = crud.get_all_captions_with_images(db)
         | 
| 208 | 
            +
                logger.debug(f"Found {len(captions)} captions")
         | 
| 209 |  | 
| 210 | 
             
                result = []
         | 
| 211 | 
             
                for caption in captions:
         | 
| 212 | 
            +
                    logger.debug(f"Processing caption {caption.caption_id}, title: {caption.title}, generated: {caption.generated}, model: {caption.model}")
         | 
| 213 | 
             
                    db.refresh(caption)
         | 
| 214 | 
             
                    result.append(schemas.CaptionOut.from_orm(caption))
         | 
| 215 | 
            +
                logger.debug(f"Returning {len(result)} formatted results")
         | 
| 216 | 
             
                return result
         | 
| 217 |  | 
| 218 | 
             
            @router.get(
         | 
    	
        py_backend/app/routers/images.py
    CHANGED
    
    | @@ -1,5 +1,6 @@ | |
| 1 | 
             
            import hashlib
         | 
| 2 | 
             
            import mimetypes
         | 
|  | |
| 3 | 
             
            from fastapi import APIRouter, HTTPException, Depends
         | 
| 4 | 
             
            from sqlalchemy.orm import Session
         | 
| 5 | 
             
            from sqlalchemy import text
         | 
| @@ -13,6 +14,7 @@ from ..config import settings | |
| 13 | 
             
            from ..services.image_preprocessor import ImagePreprocessor
         | 
| 14 |  | 
| 15 | 
             
            router = APIRouter()
         | 
|  | |
| 16 |  | 
| 17 | 
             
            def get_db():
         | 
| 18 | 
             
                db = SessionLocal()
         | 
| @@ -24,15 +26,15 @@ def get_db(): | |
| 24 | 
             
            @router.post("/from-url", response_model=CreateImageFromUrlOut)
         | 
| 25 | 
             
            async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Depends(get_db)):
         | 
| 26 | 
             
                try:
         | 
| 27 | 
            -
                     | 
| 28 | 
            -
                     | 
| 29 |  | 
| 30 | 
             
                    # Check database connectivity
         | 
| 31 | 
             
                    try:
         | 
| 32 | 
             
                        db.execute(text("SELECT 1"))
         | 
| 33 | 
            -
                         | 
| 34 | 
             
                    except Exception as db_error:
         | 
| 35 | 
            -
                         | 
| 36 | 
             
                        raise HTTPException(status_code=500, detail=f"Database connection failed: {db_error}")
         | 
| 37 |  | 
| 38 | 
             
                    # Check if required tables exist
         | 
| @@ -41,9 +43,9 @@ async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Dep | |
| 41 | 
             
                        db.execute(text("SELECT 1 FROM event_types LIMIT 1")) 
         | 
| 42 | 
             
                        db.execute(text("SELECT 1 FROM spatial_references LIMIT 1"))
         | 
| 43 | 
             
                        db.execute(text("SELECT 1 FROM image_types LIMIT 1"))
         | 
| 44 | 
            -
                         | 
| 45 | 
             
                    except Exception as table_error:
         | 
| 46 | 
            -
                         | 
| 47 | 
             
                        raise HTTPException(status_code=500, detail=f"Required tables missing: {table_error}")
         | 
| 48 |  | 
| 49 | 
             
                    if '/api/images/' in payload.url and '/file' in payload.url:
         | 
| @@ -55,33 +57,33 @@ async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Dep | |
| 55 | 
             
                    else:
         | 
| 56 | 
             
                        raise HTTPException(status_code=400, detail="Invalid image URL format")
         | 
| 57 |  | 
| 58 | 
            -
                     | 
| 59 |  | 
| 60 | 
             
                    existing_image = db.query(Images).filter(Images.image_id == image_id).first()
         | 
| 61 | 
             
                    if not existing_image:
         | 
| 62 | 
             
                        raise HTTPException(status_code=404, detail="Source image not found")
         | 
| 63 |  | 
| 64 | 
            -
                     | 
| 65 |  | 
| 66 | 
             
                    try:
         | 
| 67 | 
             
                        if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 68 | 
            -
                             | 
| 69 | 
             
                            response = storage.s3.get_object(
         | 
| 70 | 
             
                                Bucket=settings.S3_BUCKET,
         | 
| 71 | 
             
                                Key=existing_image.file_key,
         | 
| 72 | 
             
                            )
         | 
| 73 | 
             
                            data = response["Body"].read()
         | 
| 74 | 
             
                        else:
         | 
| 75 | 
            -
                             | 
| 76 | 
             
                            import os
         | 
| 77 | 
             
                            file_path = os.path.join(settings.STORAGE_DIR, existing_image.file_key)
         | 
| 78 | 
             
                            with open(file_path, 'rb') as f:
         | 
| 79 | 
             
                                data = f.read()
         | 
| 80 |  | 
| 81 | 
             
                        content_type = "image/jpeg"
         | 
| 82 | 
            -
                         | 
| 83 | 
             
                    except Exception as e:
         | 
| 84 | 
            -
                         | 
| 85 | 
             
                        raise HTTPException(status_code=400, detail=f"Failed to fetch image from storage: {e}")
         | 
| 86 |  | 
| 87 | 
             
                    if len(data) > 25 * 1024 * 1024:
         | 
| @@ -97,10 +99,10 @@ async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Dep | |
| 97 | 
             
                        )
         | 
| 98 |  | 
| 99 | 
             
                        # Log preprocessing info
         | 
| 100 | 
            -
                         | 
| 101 |  | 
| 102 | 
             
                    except Exception as e:
         | 
| 103 | 
            -
                         | 
| 104 | 
             
                        # Fall back to original content if preprocessing fails
         | 
| 105 | 
             
                        processed_data = data
         | 
| 106 | 
             
                        processed_filename = f"contributed.jpg"
         | 
| @@ -110,11 +112,11 @@ async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Dep | |
| 110 | 
             
                    key = upload_bytes(processed_data, filename=processed_filename, content_type=mime_type)
         | 
| 111 | 
             
                    image_url = get_object_url(key, expires_in=86400)
         | 
| 112 |  | 
| 113 | 
            -
                     | 
| 114 | 
            -
                     | 
| 115 |  | 
| 116 | 
             
                    sha = hashlib.sha256(processed_data).hexdigest()
         | 
| 117 | 
            -
                     | 
| 118 |  | 
| 119 | 
             
                    # Set prompt and schema based on image type
         | 
| 120 | 
             
                    prompt_code = "DEFAULT_CRISIS_MAP"
         | 
| @@ -166,28 +168,28 @@ async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Dep | |
| 166 | 
             
                        std_v_m=payload.std_v_m
         | 
| 167 | 
             
                    )
         | 
| 168 |  | 
| 169 | 
            -
                     | 
| 170 | 
             
                    db.add(img)
         | 
| 171 | 
             
                    db.flush()
         | 
| 172 | 
            -
                     | 
| 173 |  | 
| 174 | 
             
                    for c in payload.countries:
         | 
| 175 | 
            -
                         | 
| 176 | 
             
                        db.execute(image_countries.insert().values(image_id=img.image_id, c_code=c))
         | 
| 177 |  | 
| 178 | 
            -
                     | 
| 179 | 
             
                    db.commit()
         | 
| 180 | 
            -
                     | 
| 181 |  | 
| 182 | 
             
                    result = CreateImageFromUrlOut(image_id=str(img.image_id), image_url=image_url)
         | 
| 183 | 
            -
                     | 
| 184 | 
             
                    return result
         | 
| 185 |  | 
| 186 | 
             
                except Exception as e:
         | 
| 187 | 
            -
                     | 
| 188 | 
            -
                     | 
| 189 | 
             
                    import traceback
         | 
| 190 | 
            -
                    traceback. | 
| 191 | 
             
                    db.rollback()
         | 
| 192 | 
             
                    raise HTTPException(status_code=500, detail=f"Failed to create image: {str(e)}")
         | 
| 193 |  | 
|  | |
| 1 | 
             
            import hashlib
         | 
| 2 | 
             
            import mimetypes
         | 
| 3 | 
            +
            import logging
         | 
| 4 | 
             
            from fastapi import APIRouter, HTTPException, Depends
         | 
| 5 | 
             
            from sqlalchemy.orm import Session
         | 
| 6 | 
             
            from sqlalchemy import text
         | 
|  | |
| 14 | 
             
            from ..services.image_preprocessor import ImagePreprocessor
         | 
| 15 |  | 
| 16 | 
             
            router = APIRouter()
         | 
| 17 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 18 |  | 
| 19 | 
             
            def get_db():
         | 
| 20 | 
             
                db = SessionLocal()
         | 
|  | |
| 26 | 
             
            @router.post("/from-url", response_model=CreateImageFromUrlOut)
         | 
| 27 | 
             
            async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Depends(get_db)):
         | 
| 28 | 
             
                try:
         | 
| 29 | 
            +
                    logger.debug(f"Creating contribution from URL: {payload.url}")
         | 
| 30 | 
            +
                    logger.debug(f"Payload: {payload}")
         | 
| 31 |  | 
| 32 | 
             
                    # Check database connectivity
         | 
| 33 | 
             
                    try:
         | 
| 34 | 
             
                        db.execute(text("SELECT 1"))
         | 
| 35 | 
            +
                        logger.info("Database connection OK")
         | 
| 36 | 
             
                    except Exception as db_error:
         | 
| 37 | 
            +
                        logger.error(f"Database connection failed: {db_error}")
         | 
| 38 | 
             
                        raise HTTPException(status_code=500, detail=f"Database connection failed: {db_error}")
         | 
| 39 |  | 
| 40 | 
             
                    # Check if required tables exist
         | 
|  | |
| 43 | 
             
                        db.execute(text("SELECT 1 FROM event_types LIMIT 1")) 
         | 
| 44 | 
             
                        db.execute(text("SELECT 1 FROM spatial_references LIMIT 1"))
         | 
| 45 | 
             
                        db.execute(text("SELECT 1 FROM image_types LIMIT 1"))
         | 
| 46 | 
            +
                        logger.info("Required tables exist")
         | 
| 47 | 
             
                    except Exception as table_error:
         | 
| 48 | 
            +
                        logger.error(f"Required tables missing: {table_error}")
         | 
| 49 | 
             
                        raise HTTPException(status_code=500, detail=f"Required tables missing: {table_error}")
         | 
| 50 |  | 
| 51 | 
             
                    if '/api/images/' in payload.url and '/file' in payload.url:
         | 
|  | |
| 57 | 
             
                    else:
         | 
| 58 | 
             
                        raise HTTPException(status_code=400, detail="Invalid image URL format")
         | 
| 59 |  | 
| 60 | 
            +
                    logger.debug(f"Extracted image_id: {image_id}")
         | 
| 61 |  | 
| 62 | 
             
                    existing_image = db.query(Images).filter(Images.image_id == image_id).first()
         | 
| 63 | 
             
                    if not existing_image:
         | 
| 64 | 
             
                        raise HTTPException(status_code=404, detail="Source image not found")
         | 
| 65 |  | 
| 66 | 
            +
                    logger.debug(f"Found existing image: {existing_image.image_id}")
         | 
| 67 |  | 
| 68 | 
             
                    try:
         | 
| 69 | 
             
                        if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 70 | 
            +
                            logger.debug(f"Using S3 storage, bucket: {settings.S3_BUCKET}")
         | 
| 71 | 
             
                            response = storage.s3.get_object(
         | 
| 72 | 
             
                                Bucket=settings.S3_BUCKET,
         | 
| 73 | 
             
                                Key=existing_image.file_key,
         | 
| 74 | 
             
                            )
         | 
| 75 | 
             
                            data = response["Body"].read()
         | 
| 76 | 
             
                        else:
         | 
| 77 | 
            +
                            logger.debug(f"Using local storage: {settings.STORAGE_DIR}")
         | 
| 78 | 
             
                            import os
         | 
| 79 | 
             
                            file_path = os.path.join(settings.STORAGE_DIR, existing_image.file_key)
         | 
| 80 | 
             
                            with open(file_path, 'rb') as f:
         | 
| 81 | 
             
                                data = f.read()
         | 
| 82 |  | 
| 83 | 
             
                        content_type = "image/jpeg"
         | 
| 84 | 
            +
                        logger.debug(f"Image data size: {len(data)} bytes")
         | 
| 85 | 
             
                    except Exception as e:
         | 
| 86 | 
            +
                        logger.error(f"Failed to fetch image from storage: {e}")
         | 
| 87 | 
             
                        raise HTTPException(status_code=400, detail=f"Failed to fetch image from storage: {e}")
         | 
| 88 |  | 
| 89 | 
             
                    if len(data) > 25 * 1024 * 1024:
         | 
|  | |
| 99 | 
             
                        )
         | 
| 100 |  | 
| 101 | 
             
                        # Log preprocessing info
         | 
| 102 | 
            +
                        logger.debug(f"Image preprocessed: {mime_type} -> {processed_filename}")
         | 
| 103 |  | 
| 104 | 
             
                    except Exception as e:
         | 
| 105 | 
            +
                        logger.debug(f"Image preprocessing failed: {str(e)}")
         | 
| 106 | 
             
                        # Fall back to original content if preprocessing fails
         | 
| 107 | 
             
                        processed_data = data
         | 
| 108 | 
             
                        processed_filename = f"contributed.jpg"
         | 
|  | |
| 112 | 
             
                    key = upload_bytes(processed_data, filename=processed_filename, content_type=mime_type)
         | 
| 113 | 
             
                    image_url = get_object_url(key, expires_in=86400)
         | 
| 114 |  | 
| 115 | 
            +
                    logger.debug(f"Uploaded to key: {key}")
         | 
| 116 | 
            +
                    logger.debug(f"Generated URL: {image_url}")
         | 
| 117 |  | 
| 118 | 
             
                    sha = hashlib.sha256(processed_data).hexdigest()
         | 
| 119 | 
            +
                    logger.debug(f"Generated SHA256: {sha}")
         | 
| 120 |  | 
| 121 | 
             
                    # Set prompt and schema based on image type
         | 
| 122 | 
             
                    prompt_code = "DEFAULT_CRISIS_MAP"
         | 
|  | |
| 168 | 
             
                        std_v_m=payload.std_v_m
         | 
| 169 | 
             
                    )
         | 
| 170 |  | 
| 171 | 
            +
                    logger.debug(f"Created Images object: {img}")
         | 
| 172 | 
             
                    db.add(img)
         | 
| 173 | 
             
                    db.flush()
         | 
| 174 | 
            +
                    logger.debug(f"Flushed to database, image_id: {img.image_id}")
         | 
| 175 |  | 
| 176 | 
             
                    for c in payload.countries:
         | 
| 177 | 
            +
                        logger.debug(f"Adding country: {c}")
         | 
| 178 | 
             
                        db.execute(image_countries.insert().values(image_id=img.image_id, c_code=c))
         | 
| 179 |  | 
| 180 | 
            +
                    logger.debug(f"About to commit transaction")
         | 
| 181 | 
             
                    db.commit()
         | 
| 182 | 
            +
                    logger.debug(f"Transaction committed successfully")
         | 
| 183 |  | 
| 184 | 
             
                    result = CreateImageFromUrlOut(image_id=str(img.image_id), image_url=image_url)
         | 
| 185 | 
            +
                    logger.debug(f"Returning result: {result}")
         | 
| 186 | 
             
                    return result
         | 
| 187 |  | 
| 188 | 
             
                except Exception as e:
         | 
| 189 | 
            +
                    logger.error(f"Exception in create_image_from_url: {e}")
         | 
| 190 | 
            +
                    logger.error(f"Exception type: {type(e)}")
         | 
| 191 | 
             
                    import traceback
         | 
| 192 | 
            +
                    traceback.logger.debug_exc()
         | 
| 193 | 
             
                    db.rollback()
         | 
| 194 | 
             
                    raise HTTPException(status_code=500, detail=f"Failed to create image: {str(e)}")
         | 
| 195 |  | 
    	
        py_backend/app/routers/images_files.py
    ADDED
    
    | @@ -0,0 +1,196 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Image File Operations Router
         | 
| 3 | 
            +
            Handles file serving, copying, and preprocessing operations
         | 
| 4 | 
            +
            """
         | 
| 5 | 
            +
            from fastapi import APIRouter, Depends, HTTPException, Response, UploadFile, Form
         | 
| 6 | 
            +
            from pydantic import BaseModel
         | 
| 7 | 
            +
            from sqlalchemy.orm import Session
         | 
| 8 | 
            +
            from typing import List, Optional
         | 
| 9 | 
            +
            import logging
         | 
| 10 | 
            +
            import io
         | 
| 11 | 
            +
            import os
         | 
| 12 | 
            +
            import time
         | 
| 13 | 
            +
            import mimetypes
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            from .. import crud, schemas, database, storage
         | 
| 16 | 
            +
            from ..config import settings
         | 
| 17 | 
            +
            from ..services.image_preprocessor import ImagePreprocessor
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 20 | 
            +
            router = APIRouter()
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            def get_db():
         | 
| 23 | 
            +
                db = database.SessionLocal()
         | 
| 24 | 
            +
                try:
         | 
| 25 | 
            +
                    yield db
         | 
| 26 | 
            +
                finally:
         | 
| 27 | 
            +
                    db.close()
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            class CopyImageRequest(BaseModel):
         | 
| 30 | 
            +
                source_image_id: str
         | 
| 31 | 
            +
                source: str
         | 
| 32 | 
            +
                event_type: str
         | 
| 33 | 
            +
                countries: str = ""
         | 
| 34 | 
            +
                epsg: str = ""
         | 
| 35 | 
            +
                image_type: str = "crisis_map"
         | 
| 36 | 
            +
                # Drone-specific fields (optional)
         | 
| 37 | 
            +
                center_lon: Optional[float] = None
         | 
| 38 | 
            +
                center_lat: Optional[float] = None
         | 
| 39 | 
            +
                amsl_m: Optional[float] = None
         | 
| 40 | 
            +
                agl_m: Optional[float] = None
         | 
| 41 | 
            +
                heading_deg: Optional[float] = None
         | 
| 42 | 
            +
                yaw_deg: Optional[float] = None
         | 
| 43 | 
            +
                pitch_deg: Optional[float] = None
         | 
| 44 | 
            +
                roll_deg: Optional[float] = None
         | 
| 45 | 
            +
                rtk_fix: Optional[bool] = None
         | 
| 46 | 
            +
                std_h_m: Optional[float] = None
         | 
| 47 | 
            +
                std_v_m: Optional[float] = None
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            @router.get("/{image_id}/file")
         | 
| 50 | 
            +
            async def get_image_file(image_id: str, db: Session = Depends(get_db)):
         | 
| 51 | 
            +
                """Serve the actual image file"""
         | 
| 52 | 
            +
                logger.debug(f"Serving image file for image_id: {image_id}")
         | 
| 53 | 
            +
                
         | 
| 54 | 
            +
                img = crud.get_image(db, image_id)
         | 
| 55 | 
            +
                if not img:
         | 
| 56 | 
            +
                    logger.warning(f"Image not found: {image_id}")
         | 
| 57 | 
            +
                    raise HTTPException(404, "Image not found")
         | 
| 58 | 
            +
                
         | 
| 59 | 
            +
                logger.debug(f"Found image: {img.image_id}, file_key: {img.file_key}")
         | 
| 60 | 
            +
                
         | 
| 61 | 
            +
                try:
         | 
| 62 | 
            +
                    if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 63 | 
            +
                        logger.debug(f"Using S3 storage - serving file content directly")
         | 
| 64 | 
            +
                        try:
         | 
| 65 | 
            +
                            response = storage.s3.get_object(Bucket=settings.S3_BUCKET, Key=img.file_key)
         | 
| 66 | 
            +
                            content = response['Body'].read()
         | 
| 67 | 
            +
                            logger.debug(f"Read {len(content)} bytes from S3")
         | 
| 68 | 
            +
                        except Exception as e:
         | 
| 69 | 
            +
                            logger.error(f"Failed to get S3 object: {e}")
         | 
| 70 | 
            +
                            raise HTTPException(500, f"Failed to retrieve image from storage: {e}")
         | 
| 71 | 
            +
                    else:
         | 
| 72 | 
            +
                        logger.debug(f"Using local storage")
         | 
| 73 | 
            +
                        file_path = os.path.join(settings.STORAGE_DIR, img.file_key)
         | 
| 74 | 
            +
                        logger.debug(f"Reading from: {file_path}")
         | 
| 75 | 
            +
                        logger.debug(f"File exists: {os.path.exists(file_path)}")
         | 
| 76 | 
            +
                        
         | 
| 77 | 
            +
                        if not os.path.exists(file_path):
         | 
| 78 | 
            +
                            logger.error(f"File not found at: {file_path}")
         | 
| 79 | 
            +
                            raise FileNotFoundError(f"Image file not found: {file_path}")
         | 
| 80 | 
            +
                        
         | 
| 81 | 
            +
                        with open(file_path, 'rb') as f:
         | 
| 82 | 
            +
                            content = f.read()
         | 
| 83 | 
            +
                        
         | 
| 84 | 
            +
                        logger.debug(f"Read {len(content)} bytes from file")
         | 
| 85 | 
            +
                    
         | 
| 86 | 
            +
                    content_type, _ = mimetypes.guess_type(img.file_key)
         | 
| 87 | 
            +
                    if not content_type:
         | 
| 88 | 
            +
                        content_type = 'application/octet-stream'
         | 
| 89 | 
            +
                    
         | 
| 90 | 
            +
                    logger.debug(f"Serving image with content-type: {content_type}, size: {len(content)} bytes")
         | 
| 91 | 
            +
                    return Response(content=content, media_type=content_type)
         | 
| 92 | 
            +
                except Exception as e:
         | 
| 93 | 
            +
                    logger.error(f"Error serving image: {e}")
         | 
| 94 | 
            +
                    import traceback
         | 
| 95 | 
            +
                    logger.debug(f"Full traceback: {traceback.format_exc()}")
         | 
| 96 | 
            +
                    raise HTTPException(500, f"Failed to serve image file: {e}")
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            @router.post("/copy", response_model=schemas.ImageOut)
         | 
| 99 | 
            +
            async def copy_image_for_contribution(
         | 
| 100 | 
            +
                request: CopyImageRequest,
         | 
| 101 | 
            +
                db: Session = Depends(get_db)
         | 
| 102 | 
            +
            ):
         | 
| 103 | 
            +
                """Copy an existing image for contribution purposes, creating a new image_id"""
         | 
| 104 | 
            +
                logger.info(f"Copying image {request.source_image_id} for contribution")
         | 
| 105 | 
            +
                
         | 
| 106 | 
            +
                source_img = crud.get_image(db, request.source_image_id)
         | 
| 107 | 
            +
                if not source_img:
         | 
| 108 | 
            +
                    logger.warning(f"Source image not found: {request.source_image_id}")
         | 
| 109 | 
            +
                    raise HTTPException(404, "Source image not found")
         | 
| 110 | 
            +
                
         | 
| 111 | 
            +
                try:
         | 
| 112 | 
            +
                    # Get image content from storage
         | 
| 113 | 
            +
                    if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 114 | 
            +
                        response = storage.s3.get_object(
         | 
| 115 | 
            +
                            Bucket=settings.S3_BUCKET,
         | 
| 116 | 
            +
                            Key=source_img.file_key,
         | 
| 117 | 
            +
                        )
         | 
| 118 | 
            +
                        image_content = response["Body"].read()
         | 
| 119 | 
            +
                    else:
         | 
| 120 | 
            +
                        file_path = os.path.join(settings.STORAGE_DIR, source_img.file_key)
         | 
| 121 | 
            +
                        with open(file_path, 'rb') as f:
         | 
| 122 | 
            +
                            image_content = f.read()
         | 
| 123 | 
            +
                    
         | 
| 124 | 
            +
                    # Create new file with unique name
         | 
| 125 | 
            +
                    new_filename = f"contribution_{request.source_image_id}_{int(time.time())}.jpg"
         | 
| 126 | 
            +
                    new_key = storage.upload_fileobj(io.BytesIO(image_content), new_filename)
         | 
| 127 | 
            +
                    
         | 
| 128 | 
            +
                    # Parse countries
         | 
| 129 | 
            +
                    countries_list = [c.strip() for c in request.countries.split(',') if c.strip()] if request.countries else []
         | 
| 130 | 
            +
                    
         | 
| 131 | 
            +
                    # Create new image record
         | 
| 132 | 
            +
                    new_img = crud.create_image(
         | 
| 133 | 
            +
                        db, 
         | 
| 134 | 
            +
                        request.source, 
         | 
| 135 | 
            +
                        request.event_type, 
         | 
| 136 | 
            +
                        new_key, 
         | 
| 137 | 
            +
                        source_img.sha256,
         | 
| 138 | 
            +
                        countries_list, 
         | 
| 139 | 
            +
                        request.epsg, 
         | 
| 140 | 
            +
                        request.image_type,
         | 
| 141 | 
            +
                        request.center_lon, request.center_lat, request.amsl_m, request.agl_m,
         | 
| 142 | 
            +
                        request.heading_deg, request.yaw_deg, request.pitch_deg, request.roll_deg,
         | 
| 143 | 
            +
                        request.rtk_fix, request.std_h_m, request.std_v_m
         | 
| 144 | 
            +
                    )
         | 
| 145 | 
            +
                    
         | 
| 146 | 
            +
                    # Generate URL
         | 
| 147 | 
            +
                    try:
         | 
| 148 | 
            +
                        url = storage.get_object_url(new_key)
         | 
| 149 | 
            +
                    except Exception as e:
         | 
| 150 | 
            +
                        url = f"/api/images/{new_img.image_id}/file"
         | 
| 151 | 
            +
                    
         | 
| 152 | 
            +
                    # Convert to response format
         | 
| 153 | 
            +
                    from ..utils.image_utils import convert_image_to_dict
         | 
| 154 | 
            +
                    img_dict = convert_image_to_dict(new_img, url)
         | 
| 155 | 
            +
                    result = schemas.ImageOut(**img_dict)
         | 
| 156 | 
            +
                    
         | 
| 157 | 
            +
                    logger.info(f"Successfully copied image {request.source_image_id} -> {new_img.image_id}")
         | 
| 158 | 
            +
                    return result
         | 
| 159 | 
            +
                    
         | 
| 160 | 
            +
                except Exception as e:
         | 
| 161 | 
            +
                    logger.error(f"Failed to copy image: {str(e)}")
         | 
| 162 | 
            +
                    raise HTTPException(500, f"Failed to copy image: {str(e)}")
         | 
| 163 | 
            +
             | 
| 164 | 
            +
            @router.post("/preprocess")
         | 
| 165 | 
            +
            async def preprocess_image(
         | 
| 166 | 
            +
                file: UploadFile = Form(...),
         | 
| 167 | 
            +
                db: Session = Depends(get_db)
         | 
| 168 | 
            +
            ):
         | 
| 169 | 
            +
                """Preprocess an image file (convert format, optimize, etc.)"""
         | 
| 170 | 
            +
                logger.info(f"Preprocessing image: {file.filename}")
         | 
| 171 | 
            +
                
         | 
| 172 | 
            +
                try:
         | 
| 173 | 
            +
                    content = await file.read()
         | 
| 174 | 
            +
                    
         | 
| 175 | 
            +
                    # Preprocess the image
         | 
| 176 | 
            +
                    processed_content, processed_filename, mime_type = ImagePreprocessor.preprocess_image(
         | 
| 177 | 
            +
                        content, 
         | 
| 178 | 
            +
                        file.filename,
         | 
| 179 | 
            +
                        target_format='PNG',
         | 
| 180 | 
            +
                        quality=95
         | 
| 181 | 
            +
                    )
         | 
| 182 | 
            +
                    
         | 
| 183 | 
            +
                    logger.info(f"Image preprocessed: {file.filename} -> {processed_filename}")
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                    return {
         | 
| 186 | 
            +
                        "original_filename": file.filename,
         | 
| 187 | 
            +
                        "processed_filename": processed_filename,
         | 
| 188 | 
            +
                        "original_size": len(content),
         | 
| 189 | 
            +
                        "processed_size": len(processed_content),
         | 
| 190 | 
            +
                        "mime_type": mime_type,
         | 
| 191 | 
            +
                        "preprocessing_applied": processed_filename != file.filename
         | 
| 192 | 
            +
                    }
         | 
| 193 | 
            +
                    
         | 
| 194 | 
            +
                except Exception as e:
         | 
| 195 | 
            +
                    logger.error(f"Image preprocessing failed: {str(e)}")
         | 
| 196 | 
            +
                    raise HTTPException(500, f"Image preprocessing failed: {str(e)}")
         | 
    	
        py_backend/app/routers/images_listing.py
    ADDED
    
    | @@ -0,0 +1,129 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Image Listing Router
         | 
| 3 | 
            +
            Handles listing, pagination, and filtering of images
         | 
| 4 | 
            +
            """
         | 
| 5 | 
            +
            from fastapi import APIRouter, Depends, Query
         | 
| 6 | 
            +
            from sqlalchemy.orm import Session
         | 
| 7 | 
            +
            from typing import List, Optional
         | 
| 8 | 
            +
            import logging
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            from .. import crud, schemas, database
         | 
| 11 | 
            +
            from ..utils.image_utils import convert_image_to_dict
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 14 | 
            +
            router = APIRouter()
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            def get_db():
         | 
| 17 | 
            +
                db = database.SessionLocal()
         | 
| 18 | 
            +
                try:
         | 
| 19 | 
            +
                    yield db
         | 
| 20 | 
            +
                finally:
         | 
| 21 | 
            +
                    db.close()
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            @router.get("/", response_model=List[schemas.ImageOut])
         | 
| 24 | 
            +
            def list_images(db: Session = Depends(get_db)):
         | 
| 25 | 
            +
                """Get all images with their caption data"""
         | 
| 26 | 
            +
                logger.debug("Listing all images")
         | 
| 27 | 
            +
                images = crud.get_images(db)
         | 
| 28 | 
            +
                result = []
         | 
| 29 | 
            +
                for img in images:
         | 
| 30 | 
            +
                    img_dict = convert_image_to_dict(img, f"/api/images/{img.image_id}/file")
         | 
| 31 | 
            +
                    result.append(schemas.ImageOut(**img_dict))
         | 
| 32 | 
            +
                
         | 
| 33 | 
            +
                logger.info(f"Returned {len(result)} images")
         | 
| 34 | 
            +
                return result
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            @router.get("/grouped", response_model=List[schemas.ImageOut])
         | 
| 37 | 
            +
            def list_images_grouped(
         | 
| 38 | 
            +
                page: int = Query(1, ge=1),
         | 
| 39 | 
            +
                limit: int = Query(10, ge=1, le=100),
         | 
| 40 | 
            +
                search: str = Query(None),
         | 
| 41 | 
            +
                source: str = Query(None),
         | 
| 42 | 
            +
                event_type: str = Query(None),
         | 
| 43 | 
            +
                region: str = Query(None),
         | 
| 44 | 
            +
                country: str = Query(None),
         | 
| 45 | 
            +
                image_type: str = Query(None),
         | 
| 46 | 
            +
                db: Session = Depends(get_db)
         | 
| 47 | 
            +
            ):
         | 
| 48 | 
            +
                """Get paginated and filtered images"""
         | 
| 49 | 
            +
                logger.debug(f"Listing grouped images - page: {page}, limit: {limit}")
         | 
| 50 | 
            +
                
         | 
| 51 | 
            +
                # Build filter parameters
         | 
| 52 | 
            +
                filters = {}
         | 
| 53 | 
            +
                if search:
         | 
| 54 | 
            +
                    filters['search'] = search
         | 
| 55 | 
            +
                if source:
         | 
| 56 | 
            +
                    filters['source'] = source
         | 
| 57 | 
            +
                if event_type:
         | 
| 58 | 
            +
                    filters['event_type'] = event_type
         | 
| 59 | 
            +
                if region:
         | 
| 60 | 
            +
                    filters['region'] = region
         | 
| 61 | 
            +
                if country:
         | 
| 62 | 
            +
                    filters['country'] = country
         | 
| 63 | 
            +
                if image_type:
         | 
| 64 | 
            +
                    filters['image_type'] = image_type
         | 
| 65 | 
            +
                
         | 
| 66 | 
            +
                logger.debug(f"Applied filters: {filters}")
         | 
| 67 | 
            +
                
         | 
| 68 | 
            +
                # Get paginated results
         | 
| 69 | 
            +
                images = crud.get_images_paginated(
         | 
| 70 | 
            +
                    db, 
         | 
| 71 | 
            +
                    page=page, 
         | 
| 72 | 
            +
                    limit=limit, 
         | 
| 73 | 
            +
                    **filters
         | 
| 74 | 
            +
                )
         | 
| 75 | 
            +
                
         | 
| 76 | 
            +
                result = []
         | 
| 77 | 
            +
                for img in images:
         | 
| 78 | 
            +
                    img_dict = convert_image_to_dict(img, f"/api/images/{img.image_id}/file")
         | 
| 79 | 
            +
                    result.append(schemas.ImageOut(**img_dict))
         | 
| 80 | 
            +
                
         | 
| 81 | 
            +
                logger.info(f"Returned {len(result)} images for page {page}")
         | 
| 82 | 
            +
                return result
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            @router.get("/grouped/count")
         | 
| 85 | 
            +
            def get_images_grouped_count(
         | 
| 86 | 
            +
                search: str = Query(None),
         | 
| 87 | 
            +
                source: str = Query(None),
         | 
| 88 | 
            +
                event_type: str = Query(None),
         | 
| 89 | 
            +
                region: str = Query(None),
         | 
| 90 | 
            +
                country: str = Query(None),
         | 
| 91 | 
            +
                image_type: str = Query(None),
         | 
| 92 | 
            +
                db: Session = Depends(get_db)
         | 
| 93 | 
            +
            ):
         | 
| 94 | 
            +
                """Get total count of images matching filters"""
         | 
| 95 | 
            +
                logger.debug("Getting images count")
         | 
| 96 | 
            +
                
         | 
| 97 | 
            +
                # Build filter parameters
         | 
| 98 | 
            +
                filters = {}
         | 
| 99 | 
            +
                if search:
         | 
| 100 | 
            +
                    filters['search'] = search
         | 
| 101 | 
            +
                if source:
         | 
| 102 | 
            +
                    filters['source'] = source
         | 
| 103 | 
            +
                if event_type:
         | 
| 104 | 
            +
                    filters['event_type'] = event_type
         | 
| 105 | 
            +
                if region:
         | 
| 106 | 
            +
                    filters['region'] = region
         | 
| 107 | 
            +
                if country:
         | 
| 108 | 
            +
                    filters['country'] = country
         | 
| 109 | 
            +
                if image_type:
         | 
| 110 | 
            +
                    filters['image_type'] = image_type
         | 
| 111 | 
            +
                
         | 
| 112 | 
            +
                count = crud.get_images_count(db, **filters)
         | 
| 113 | 
            +
                logger.info(f"Total images count: {count}")
         | 
| 114 | 
            +
                return {"count": count}
         | 
| 115 | 
            +
             | 
| 116 | 
            +
            @router.get("/{image_id}", response_model=schemas.ImageOut)
         | 
| 117 | 
            +
            def get_image(image_id: str, db: Session = Depends(get_db)):
         | 
| 118 | 
            +
                """Get a specific image by ID"""
         | 
| 119 | 
            +
                logger.debug(f"Getting image: {image_id}")
         | 
| 120 | 
            +
                
         | 
| 121 | 
            +
                img = crud.get_image(db, image_id)
         | 
| 122 | 
            +
                if not img:
         | 
| 123 | 
            +
                    logger.warning(f"Image not found: {image_id}")
         | 
| 124 | 
            +
                    from fastapi import HTTPException
         | 
| 125 | 
            +
                    raise HTTPException(404, "Image not found")
         | 
| 126 | 
            +
                
         | 
| 127 | 
            +
                img_dict = convert_image_to_dict(img, f"/api/images/{img.image_id}/file")
         | 
| 128 | 
            +
                logger.info(f"Returned image: {image_id}")
         | 
| 129 | 
            +
                return schemas.ImageOut(**img_dict)
         | 
    	
        py_backend/app/routers/images_metadata.py
    ADDED
    
    | @@ -0,0 +1,150 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Image Metadata Router
         | 
| 3 | 
            +
            Handles metadata updates and image deletion
         | 
| 4 | 
            +
            """
         | 
| 5 | 
            +
            from fastapi import APIRouter, Depends, HTTPException
         | 
| 6 | 
            +
            from sqlalchemy.orm import Session
         | 
| 7 | 
            +
            import logging
         | 
| 8 | 
            +
            import datetime
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            from .. import crud, schemas, database, storage
         | 
| 11 | 
            +
            from ..utils.image_utils import convert_image_to_dict
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 14 | 
            +
            router = APIRouter()
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            def get_db():
         | 
| 17 | 
            +
                db = database.SessionLocal()
         | 
| 18 | 
            +
                try:
         | 
| 19 | 
            +
                    yield db
         | 
| 20 | 
            +
                finally:
         | 
| 21 | 
            +
                    db.close()
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            @router.put("/{image_id}")
         | 
| 24 | 
            +
            def update_image_metadata(
         | 
| 25 | 
            +
                image_id: str,
         | 
| 26 | 
            +
                metadata: schemas.ImageMetadataUpdate,
         | 
| 27 | 
            +
                db: Session = Depends(get_db)
         | 
| 28 | 
            +
            ):
         | 
| 29 | 
            +
                """Update image metadata (source, type, epsg, image_type, countries)"""
         | 
| 30 | 
            +
                logger.debug(f"Updating metadata for image {image_id}")
         | 
| 31 | 
            +
                logger.debug(f"Metadata received: {metadata}")
         | 
| 32 | 
            +
                
         | 
| 33 | 
            +
                img = crud.get_image(db, image_id)
         | 
| 34 | 
            +
                if not img:
         | 
| 35 | 
            +
                    logger.warning(f"Image {image_id} not found in database")
         | 
| 36 | 
            +
                    raise HTTPException(404, "Image not found")
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                logger.debug(f"Found image {image_id} in database")
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                try:
         | 
| 41 | 
            +
                    # Update basic fields
         | 
| 42 | 
            +
                    if metadata.source is not None:
         | 
| 43 | 
            +
                        img.source = metadata.source
         | 
| 44 | 
            +
                    if metadata.event_type is not None:
         | 
| 45 | 
            +
                        img.event_type = metadata.event_type
         | 
| 46 | 
            +
                    if metadata.epsg is not None:
         | 
| 47 | 
            +
                        img.epsg = metadata.epsg
         | 
| 48 | 
            +
                    if metadata.image_type is not None:
         | 
| 49 | 
            +
                        img.image_type = metadata.image_type
         | 
| 50 | 
            +
                    
         | 
| 51 | 
            +
                    # Handle starred field - update the first caption's starred status
         | 
| 52 | 
            +
                    if metadata.starred is not None:
         | 
| 53 | 
            +
                        if img.captions:
         | 
| 54 | 
            +
                            # Update the first caption's starred status
         | 
| 55 | 
            +
                            img.captions[0].starred = metadata.starred
         | 
| 56 | 
            +
                        else:
         | 
| 57 | 
            +
                            # If no captions exist, create a minimal caption with starred status
         | 
| 58 | 
            +
                            from .. import models
         | 
| 59 | 
            +
                            caption = models.Captions(
         | 
| 60 | 
            +
                                title="",
         | 
| 61 | 
            +
                                starred=metadata.starred,
         | 
| 62 | 
            +
                                created_at=datetime.datetime.utcnow()
         | 
| 63 | 
            +
                            )
         | 
| 64 | 
            +
                            db.add(caption)
         | 
| 65 | 
            +
                            img.captions.append(caption)
         | 
| 66 | 
            +
                    
         | 
| 67 | 
            +
                    # Update drone-specific fields
         | 
| 68 | 
            +
                    if metadata.center_lon is not None:
         | 
| 69 | 
            +
                        img.center_lon = metadata.center_lon
         | 
| 70 | 
            +
                    if metadata.center_lat is not None:
         | 
| 71 | 
            +
                        img.center_lat = metadata.center_lat
         | 
| 72 | 
            +
                    if metadata.amsl_m is not None:
         | 
| 73 | 
            +
                        img.amsl_m = metadata.amsl_m
         | 
| 74 | 
            +
                    if metadata.agl_m is not None:
         | 
| 75 | 
            +
                        img.agl_m = metadata.agl_m
         | 
| 76 | 
            +
                    if metadata.heading_deg is not None:
         | 
| 77 | 
            +
                        img.heading_deg = metadata.heading_deg
         | 
| 78 | 
            +
                    if metadata.yaw_deg is not None:
         | 
| 79 | 
            +
                        img.yaw_deg = metadata.yaw_deg
         | 
| 80 | 
            +
                    if metadata.pitch_deg is not None:
         | 
| 81 | 
            +
                        img.pitch_deg = metadata.pitch_deg
         | 
| 82 | 
            +
                    if metadata.roll_deg is not None:
         | 
| 83 | 
            +
                        img.roll_deg = metadata.roll_deg
         | 
| 84 | 
            +
                    if metadata.rtk_fix is not None:
         | 
| 85 | 
            +
                        img.rtk_fix = metadata.rtk_fix
         | 
| 86 | 
            +
                    if metadata.std_h_m is not None:
         | 
| 87 | 
            +
                        img.std_h_m = metadata.std_h_m
         | 
| 88 | 
            +
                    if metadata.std_v_m is not None:
         | 
| 89 | 
            +
                        img.std_v_m = metadata.std_v_m
         | 
| 90 | 
            +
                    
         | 
| 91 | 
            +
                    # Update countries
         | 
| 92 | 
            +
                    if metadata.countries is not None:
         | 
| 93 | 
            +
                        logger.debug(f"Updating countries to: {metadata.countries}")
         | 
| 94 | 
            +
                        img.countries.clear()
         | 
| 95 | 
            +
                        for country_code in metadata.countries:
         | 
| 96 | 
            +
                            country = crud.get_country(db, country_code)
         | 
| 97 | 
            +
                            if country:
         | 
| 98 | 
            +
                                img.countries.append(country)
         | 
| 99 | 
            +
                                logger.debug(f"Added country: {country_code}")
         | 
| 100 | 
            +
                    
         | 
| 101 | 
            +
                    db.commit()
         | 
| 102 | 
            +
                    db.refresh(img)
         | 
| 103 | 
            +
                    logger.info(f"Metadata update successful for image {image_id}")
         | 
| 104 | 
            +
                    
         | 
| 105 | 
            +
                    # Generate image URL
         | 
| 106 | 
            +
                    try:
         | 
| 107 | 
            +
                        url = storage.get_object_url(img.file_key)
         | 
| 108 | 
            +
                    except Exception:
         | 
| 109 | 
            +
                        url = f"/api/images/{img.image_id}/file"
         | 
| 110 | 
            +
                    
         | 
| 111 | 
            +
                    img_dict = convert_image_to_dict(img, url)
         | 
| 112 | 
            +
                    return schemas.ImageOut(**img_dict)
         | 
| 113 | 
            +
                    
         | 
| 114 | 
            +
                except Exception as e:
         | 
| 115 | 
            +
                    db.rollback()
         | 
| 116 | 
            +
                    logger.error(f"Metadata update failed for image {image_id}: {str(e)}")
         | 
| 117 | 
            +
                    raise HTTPException(500, f"Failed to update image metadata: {str(e)}")
         | 
| 118 | 
            +
             | 
| 119 | 
            +
            @router.delete("/{image_id}")
         | 
| 120 | 
            +
            def delete_image(image_id: str, db: Session = Depends(get_db), content_management: bool = False):
         | 
| 121 | 
            +
                """Delete an image and its associated caption data
         | 
| 122 | 
            +
                
         | 
| 123 | 
            +
                Args:
         | 
| 124 | 
            +
                    image_id: The ID of the image to delete
         | 
| 125 | 
            +
                    content_management: If True, this is a content management delete (from map details)
         | 
| 126 | 
            +
                """
         | 
| 127 | 
            +
                logger.info(f"Deleting image {image_id} (content_management={content_management})")
         | 
| 128 | 
            +
                
         | 
| 129 | 
            +
                img = crud.get_image(db, image_id)
         | 
| 130 | 
            +
                if not img:
         | 
| 131 | 
            +
                    logger.warning(f"Image {image_id} not found")
         | 
| 132 | 
            +
                    raise HTTPException(404, "Image not found")
         | 
| 133 | 
            +
                
         | 
| 134 | 
            +
                try:
         | 
| 135 | 
            +
                    # Delete associated captions first
         | 
| 136 | 
            +
                    for caption in img.captions:
         | 
| 137 | 
            +
                        db.delete(caption)
         | 
| 138 | 
            +
                        logger.debug(f"Deleted caption {caption.caption_id}")
         | 
| 139 | 
            +
                    
         | 
| 140 | 
            +
                    # Delete the image
         | 
| 141 | 
            +
                    db.delete(img)
         | 
| 142 | 
            +
                    db.commit()
         | 
| 143 | 
            +
                    
         | 
| 144 | 
            +
                    logger.info(f"Successfully deleted image {image_id}")
         | 
| 145 | 
            +
                    return {"message": "Image deleted successfully", "image_id": image_id}
         | 
| 146 | 
            +
                    
         | 
| 147 | 
            +
                except Exception as e:
         | 
| 148 | 
            +
                    db.rollback()
         | 
| 149 | 
            +
                    logger.error(f"Failed to delete image {image_id}: {str(e)}")
         | 
| 150 | 
            +
                    raise HTTPException(500, f"Failed to delete image: {str(e)}")
         | 
    	
        py_backend/app/routers/images_upload.py
    ADDED
    
    | @@ -0,0 +1,135 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Simplified Upload Router
         | 
| 3 | 
            +
            Handles only the core upload endpoints, delegating to service layer
         | 
| 4 | 
            +
            """
         | 
| 5 | 
            +
            from fastapi import APIRouter, UploadFile, Form, Depends, HTTPException
         | 
| 6 | 
            +
            from sqlalchemy.orm import Session
         | 
| 7 | 
            +
            from typing import List, Optional
         | 
| 8 | 
            +
            import logging
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            from .. import schemas, database
         | 
| 11 | 
            +
            from ..services.upload_service import UploadService
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 14 | 
            +
            router = APIRouter()
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            def get_db():
         | 
| 17 | 
            +
                db = database.SessionLocal()
         | 
| 18 | 
            +
                try:
         | 
| 19 | 
            +
                    yield db
         | 
| 20 | 
            +
                finally:
         | 
| 21 | 
            +
                    db.close()
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            @router.post("/", response_model=schemas.ImageOut)
         | 
| 24 | 
            +
            async def upload_image(
         | 
| 25 | 
            +
                source: Optional[str] = Form(default=None),
         | 
| 26 | 
            +
                event_type: str = Form(default="OTHER"),
         | 
| 27 | 
            +
                countries: str = Form(default=""),
         | 
| 28 | 
            +
                epsg: str = Form(default=""),
         | 
| 29 | 
            +
                image_type: str = Form(default="crisis_map"),
         | 
| 30 | 
            +
                file: UploadFile = Form(...),
         | 
| 31 | 
            +
                title: str = Form(default=""),
         | 
| 32 | 
            +
                model_name: Optional[str] = Form(default=None),
         | 
| 33 | 
            +
                # Drone-specific fields (optional)
         | 
| 34 | 
            +
                center_lon: Optional[float] = Form(default=None),
         | 
| 35 | 
            +
                center_lat: Optional[float] = Form(default=None),
         | 
| 36 | 
            +
                amsl_m: Optional[float] = Form(default=None),
         | 
| 37 | 
            +
                agl_m: Optional[float] = Form(default=None),
         | 
| 38 | 
            +
                heading_deg: Optional[float] = Form(default=None),
         | 
| 39 | 
            +
                yaw_deg: Optional[float] = Form(default=None),
         | 
| 40 | 
            +
                pitch_deg: Optional[float] = Form(default=None),
         | 
| 41 | 
            +
                roll_deg: Optional[float] = Form(default=None),
         | 
| 42 | 
            +
                rtk_fix: Optional[bool] = Form(default=None),
         | 
| 43 | 
            +
                std_h_m: Optional[float] = Form(default=None),
         | 
| 44 | 
            +
                std_v_m: Optional[float] = Form(default=None),
         | 
| 45 | 
            +
                db: Session = Depends(get_db)
         | 
| 46 | 
            +
            ):
         | 
| 47 | 
            +
                """Upload a single image"""
         | 
| 48 | 
            +
                logger.info(f"Single image upload request: {file.filename}")
         | 
| 49 | 
            +
                
         | 
| 50 | 
            +
                try:
         | 
| 51 | 
            +
                    result = await UploadService.process_single_upload(
         | 
| 52 | 
            +
                        file=file,
         | 
| 53 | 
            +
                        source=source,
         | 
| 54 | 
            +
                        event_type=event_type,
         | 
| 55 | 
            +
                        countries=countries,
         | 
| 56 | 
            +
                        epsg=epsg,
         | 
| 57 | 
            +
                        image_type=image_type,
         | 
| 58 | 
            +
                        title=title,
         | 
| 59 | 
            +
                        model_name=model_name,
         | 
| 60 | 
            +
                        center_lon=center_lon,
         | 
| 61 | 
            +
                        center_lat=center_lat,
         | 
| 62 | 
            +
                        amsl_m=amsl_m,
         | 
| 63 | 
            +
                        agl_m=agl_m,
         | 
| 64 | 
            +
                        heading_deg=heading_deg,
         | 
| 65 | 
            +
                        yaw_deg=yaw_deg,
         | 
| 66 | 
            +
                        pitch_deg=pitch_deg,
         | 
| 67 | 
            +
                        roll_deg=roll_deg,
         | 
| 68 | 
            +
                        rtk_fix=rtk_fix,
         | 
| 69 | 
            +
                        std_h_m=std_h_m,
         | 
| 70 | 
            +
                        std_v_m=std_v_m,
         | 
| 71 | 
            +
                        db=db
         | 
| 72 | 
            +
                    )
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                    return result['image']
         | 
| 75 | 
            +
                    
         | 
| 76 | 
            +
                except Exception as e:
         | 
| 77 | 
            +
                    logger.error(f"Single upload failed: {str(e)}")
         | 
| 78 | 
            +
                    raise HTTPException(500, f"Upload failed: {str(e)}")
         | 
| 79 | 
            +
             | 
| 80 | 
            +
            @router.post("/multi", response_model=dict)
         | 
| 81 | 
            +
            async def upload_multiple_images(
         | 
| 82 | 
            +
                files: List[UploadFile] = Form(...),
         | 
| 83 | 
            +
                source: Optional[str] = Form(default=None),
         | 
| 84 | 
            +
                event_type: str = Form(default="OTHER"),
         | 
| 85 | 
            +
                countries: str = Form(default=""),
         | 
| 86 | 
            +
                epsg: str = Form(default=""),
         | 
| 87 | 
            +
                image_type: str = Form(default="crisis_map"),
         | 
| 88 | 
            +
                title: str = Form(default=""),
         | 
| 89 | 
            +
                model_name: Optional[str] = Form(default=None),
         | 
| 90 | 
            +
                # Drone-specific fields (optional)
         | 
| 91 | 
            +
                center_lon: Optional[float] = Form(default=None),
         | 
| 92 | 
            +
                center_lat: Optional[float] = Form(default=None),
         | 
| 93 | 
            +
                amsl_m: Optional[float] = Form(default=None),
         | 
| 94 | 
            +
                agl_m: Optional[float] = Form(default=None),
         | 
| 95 | 
            +
                heading_deg: Optional[float] = Form(default=None),
         | 
| 96 | 
            +
                yaw_deg: Optional[float] = Form(default=None),
         | 
| 97 | 
            +
                pitch_deg: Optional[float] = Form(default=None),
         | 
| 98 | 
            +
                roll_deg: Optional[float] = Form(default=None),
         | 
| 99 | 
            +
                rtk_fix: Optional[bool] = Form(default=None),
         | 
| 100 | 
            +
                std_h_m: Optional[float] = Form(default=None),
         | 
| 101 | 
            +
                std_v_m: Optional[float] = Form(default=None),
         | 
| 102 | 
            +
                db: Session = Depends(get_db)
         | 
| 103 | 
            +
            ):
         | 
| 104 | 
            +
                """Upload multiple images"""
         | 
| 105 | 
            +
                logger.info(f"Multi image upload request: {len(files)} files")
         | 
| 106 | 
            +
                
         | 
| 107 | 
            +
                try:
         | 
| 108 | 
            +
                    result = await UploadService.process_multi_upload(
         | 
| 109 | 
            +
                        files=files,
         | 
| 110 | 
            +
                        source=source,
         | 
| 111 | 
            +
                        event_type=event_type,
         | 
| 112 | 
            +
                        countries=countries,
         | 
| 113 | 
            +
                        epsg=epsg,
         | 
| 114 | 
            +
                        image_type=image_type,
         | 
| 115 | 
            +
                        title=title,
         | 
| 116 | 
            +
                        model_name=model_name,
         | 
| 117 | 
            +
                        center_lon=center_lon,
         | 
| 118 | 
            +
                        center_lat=center_lat,
         | 
| 119 | 
            +
                        amsl_m=amsl_m,
         | 
| 120 | 
            +
                        agl_m=agl_m,
         | 
| 121 | 
            +
                        heading_deg=heading_deg,
         | 
| 122 | 
            +
                        yaw_deg=yaw_deg,
         | 
| 123 | 
            +
                        pitch_deg=pitch_deg,
         | 
| 124 | 
            +
                        roll_deg=roll_deg,
         | 
| 125 | 
            +
                        rtk_fix=rtk_fix,
         | 
| 126 | 
            +
                        std_h_m=std_h_m,
         | 
| 127 | 
            +
                        std_v_m=std_v_m,
         | 
| 128 | 
            +
                        db=db
         | 
| 129 | 
            +
                    )
         | 
| 130 | 
            +
                    
         | 
| 131 | 
            +
                    return result
         | 
| 132 | 
            +
                    
         | 
| 133 | 
            +
                except Exception as e:
         | 
| 134 | 
            +
                    logger.error(f"Multi upload failed: {str(e)}")
         | 
| 135 | 
            +
                    raise HTTPException(500, f"Multi upload failed: {str(e)}")
         | 
    	
        py_backend/app/routers/prompts.py
    CHANGED
    
    | @@ -1,9 +1,11 @@ | |
| 1 | 
             
            from fastapi import APIRouter, Depends
         | 
| 2 | 
             
            from sqlalchemy.orm import Session
         | 
| 3 | 
             
            from typing import List
         | 
|  | |
| 4 | 
             
            from .. import crud, database, schemas
         | 
| 5 |  | 
| 6 | 
             
            router = APIRouter()
         | 
|  | |
| 7 |  | 
| 8 | 
             
            def get_db():
         | 
| 9 | 
             
                db = database.SessionLocal()
         | 
| @@ -15,15 +17,15 @@ def get_db(): | |
| 15 | 
             
            @router.get("/", response_model=List[schemas.PromptOut])
         | 
| 16 | 
             
            def get_prompts(db: Session = Depends(get_db)):
         | 
| 17 | 
             
                """Get all available prompts"""
         | 
| 18 | 
            -
                 | 
| 19 | 
             
                try:
         | 
| 20 | 
             
                    prompts = crud.get_prompts(db)
         | 
| 21 | 
            -
                     | 
| 22 | 
             
                    for prompt in prompts:
         | 
| 23 | 
            -
                         | 
| 24 | 
             
                    return prompts
         | 
| 25 | 
             
                except Exception as e:
         | 
| 26 | 
            -
                     | 
| 27 | 
             
                    raise
         | 
| 28 |  | 
| 29 | 
             
            @router.post("/", response_model=schemas.PromptOut)
         | 
|  | |
| 1 | 
             
            from fastapi import APIRouter, Depends
         | 
| 2 | 
             
            from sqlalchemy.orm import Session
         | 
| 3 | 
             
            from typing import List
         | 
| 4 | 
            +
            import logging
         | 
| 5 | 
             
            from .. import crud, database, schemas
         | 
| 6 |  | 
| 7 | 
             
            router = APIRouter()
         | 
| 8 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 9 |  | 
| 10 | 
             
            def get_db():
         | 
| 11 | 
             
                db = database.SessionLocal()
         | 
|  | |
| 17 | 
             
            @router.get("/", response_model=List[schemas.PromptOut])
         | 
| 18 | 
             
            def get_prompts(db: Session = Depends(get_db)):
         | 
| 19 | 
             
                """Get all available prompts"""
         | 
| 20 | 
            +
                logger.debug("get_prompts called")
         | 
| 21 | 
             
                try:
         | 
| 22 | 
             
                    prompts = crud.get_prompts(db)
         | 
| 23 | 
            +
                    logger.debug(f"Found {len(prompts)} prompts")
         | 
| 24 | 
             
                    for prompt in prompts:
         | 
| 25 | 
            +
                        logger.debug(f"  - {prompt.p_code}: {prompt.label} ({prompt.image_type}, active: {prompt.is_active})")
         | 
| 26 | 
             
                    return prompts
         | 
| 27 | 
             
                except Exception as e:
         | 
| 28 | 
            +
                    logger.error(f"Error in get_prompts: {e}")
         | 
| 29 | 
             
                    raise
         | 
| 30 |  | 
| 31 | 
             
            @router.post("/", response_model=schemas.PromptOut)
         | 
    	
        py_backend/app/routers/upload.py
    CHANGED
    
    | @@ -1,6 +1,7 @@ | |
| 1 | 
             
            from fastapi import APIRouter, UploadFile, Form, Depends, HTTPException, Response
         | 
| 2 | 
             
            from pydantic import BaseModel
         | 
| 3 | 
             
            import io
         | 
|  | |
| 4 | 
             
            from sqlalchemy.orm import Session
         | 
| 5 | 
             
            from .. import crud, schemas, storage, database
         | 
| 6 | 
             
            from ..config import settings
         | 
| @@ -13,6 +14,7 @@ import base64 | |
| 13 | 
             
            import datetime
         | 
| 14 |  | 
| 15 | 
             
            router = APIRouter()
         | 
|  | |
| 16 |  | 
| 17 | 
             
            class CopyImageRequest(BaseModel):
         | 
| 18 | 
             
                source_image_id: str
         | 
| @@ -49,7 +51,7 @@ def convert_image_to_dict(img, image_url): | |
| 49 | 
             
                    try:
         | 
| 50 | 
             
                        countries_list = [{"c_code": c.c_code, "label": c.label, "r_code": c.r_code} for c in img.countries]
         | 
| 51 | 
             
                    except Exception as e:
         | 
| 52 | 
            -
                         | 
| 53 | 
             
                        countries_list = []
         | 
| 54 |  | 
| 55 | 
             
                captions_list = []
         | 
| @@ -74,7 +76,7 @@ def convert_image_to_dict(img, image_url): | |
| 74 | 
             
                            } for c in img.captions
         | 
| 75 | 
             
                        ]
         | 
| 76 | 
             
                    except Exception as e:
         | 
| 77 | 
            -
                         | 
| 78 | 
             
                        captions_list = []
         | 
| 79 |  | 
| 80 | 
             
                # Get starred status and other caption fields from first caption for backward compatibility
         | 
| @@ -116,13 +118,13 @@ def convert_image_to_dict(img, image_url): | |
| 116 | 
             
                    try:
         | 
| 117 | 
             
                        thumbnail_url = storage.get_object_url(img.thumbnail_key)
         | 
| 118 | 
             
                    except Exception as e:
         | 
| 119 | 
            -
                         | 
| 120 |  | 
| 121 | 
             
                if hasattr(img, 'detail_key') and img.detail_key:
         | 
| 122 | 
             
                    try:
         | 
| 123 | 
             
                        detail_url = storage.get_object_url(img.detail_key)
         | 
| 124 | 
             
                    except Exception as e:
         | 
| 125 | 
            -
                         | 
| 126 |  | 
| 127 | 
             
                img_dict = {
         | 
| 128 | 
             
                    "image_id": img.image_id,
         | 
| @@ -585,7 +587,7 @@ async def upload_image( | |
| 585 | 
             
                    # Log preprocessing info
         | 
| 586 | 
             
                    preprocessing_info = None
         | 
| 587 | 
             
                    if processed_filename != file.filename:
         | 
| 588 | 
            -
                         | 
| 589 | 
             
                        preprocessing_info = {
         | 
| 590 | 
             
                            "original_filename": file.filename,
         | 
| 591 | 
             
                            "processed_filename": processed_filename,
         | 
| @@ -603,7 +605,7 @@ async def upload_image( | |
| 603 | 
             
                        }
         | 
| 604 |  | 
| 605 | 
             
                except Exception as e:
         | 
| 606 | 
            -
                     | 
| 607 | 
             
                    # Fall back to original content if preprocessing fails
         | 
| 608 | 
             
                    processed_content = content
         | 
| 609 | 
             
                    processed_filename = file.filename
         | 
| @@ -636,14 +638,14 @@ async def upload_image( | |
| 636 |  | 
| 637 | 
             
                    if thumbnail_result:
         | 
| 638 | 
             
                        thumbnail_key, thumbnail_sha256 = thumbnail_result
         | 
| 639 | 
            -
                         | 
| 640 |  | 
| 641 | 
             
                    if detail_result:
         | 
| 642 | 
             
                        detail_key, detail_sha256 = detail_result
         | 
| 643 | 
            -
                         | 
| 644 |  | 
| 645 | 
             
                except Exception as e:
         | 
| 646 | 
            -
                     | 
| 647 | 
             
                    # Continue without processed versions if generation fails
         | 
| 648 |  | 
| 649 | 
             
                try:
         | 
| @@ -710,7 +712,7 @@ async def upload_image( | |
| 710 | 
             
                    )
         | 
| 711 |  | 
| 712 | 
             
                except Exception as e:
         | 
| 713 | 
            -
                     | 
| 714 | 
             
                    # Continue without caption if VLM fails
         | 
| 715 |  | 
| 716 | 
             
                img_dict = convert_image_to_dict(img, url)
         | 
| @@ -798,7 +800,7 @@ async def upload_multiple_images( | |
| 798 | 
             
                            quality=95
         | 
| 799 | 
             
                        )
         | 
| 800 | 
             
                    except Exception as e:
         | 
| 801 | 
            -
                         | 
| 802 | 
             
                        processed_content = content
         | 
| 803 | 
             
                        processed_filename = file.filename
         | 
| 804 | 
             
                        mime_type = 'image/png'
         | 
| @@ -900,7 +902,7 @@ async def upload_multiple_images( | |
| 900 | 
             
                    db.commit()
         | 
| 901 |  | 
| 902 | 
             
                except Exception as e:
         | 
| 903 | 
            -
                     | 
| 904 | 
             
                    # Create fallback caption
         | 
| 905 | 
             
                    fallback_text = f"Analysis of {len(image_bytes_list)} images"
         | 
| 906 | 
             
                    caption = crud.create_caption(
         | 
| @@ -988,52 +990,52 @@ async def copy_image_for_contribution( | |
| 988 | 
             
            @router.get("/{image_id}/file")
         | 
| 989 | 
             
            async def get_image_file(image_id: str, db: Session = Depends(get_db)):
         | 
| 990 | 
             
                """Serve the actual image file"""
         | 
| 991 | 
            -
                 | 
| 992 |  | 
| 993 | 
             
                img = crud.get_image(db, image_id)
         | 
| 994 | 
             
                if not img:
         | 
| 995 | 
            -
                     | 
| 996 | 
             
                    raise HTTPException(404, "Image not found")
         | 
| 997 |  | 
| 998 | 
            -
                 | 
| 999 |  | 
| 1000 | 
             
                try:
         | 
| 1001 | 
             
                    if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 1002 | 
            -
                         | 
| 1003 | 
             
                        try:
         | 
| 1004 | 
             
                            response = storage.s3.get_object(Bucket=settings.S3_BUCKET, Key=img.file_key)
         | 
| 1005 | 
             
                            content = response['Body'].read()
         | 
| 1006 | 
            -
                             | 
| 1007 | 
             
                        except Exception as e:
         | 
| 1008 | 
            -
                             | 
| 1009 | 
             
                            raise HTTPException(500, f"Failed to retrieve image from storage: {e}")
         | 
| 1010 | 
             
                    else:
         | 
| 1011 | 
            -
                         | 
| 1012 | 
             
                        import os
         | 
| 1013 | 
             
                        file_path = os.path.join(settings.STORAGE_DIR, img.file_key)
         | 
| 1014 | 
            -
                         | 
| 1015 | 
            -
                         | 
| 1016 |  | 
| 1017 | 
             
                        if not os.path.exists(file_path):
         | 
| 1018 | 
            -
                             | 
| 1019 | 
             
                            raise FileNotFoundError(f"Image file not found: {file_path}")
         | 
| 1020 |  | 
| 1021 | 
             
                        with open(file_path, 'rb') as f:
         | 
| 1022 | 
             
                            content = f.read()
         | 
| 1023 |  | 
| 1024 | 
            -
                         | 
| 1025 |  | 
| 1026 | 
             
                    import mimetypes
         | 
| 1027 | 
             
                    content_type, _ = mimetypes.guess_type(img.file_key)
         | 
| 1028 | 
             
                    if not content_type:
         | 
| 1029 | 
             
                        content_type = 'application/octet-stream'
         | 
| 1030 |  | 
| 1031 | 
            -
                     | 
| 1032 | 
             
                    return Response(content=content, media_type=content_type)
         | 
| 1033 | 
             
                except Exception as e:
         | 
| 1034 | 
            -
                     | 
| 1035 | 
             
                    import traceback
         | 
| 1036 | 
            -
                     | 
| 1037 | 
             
                    raise HTTPException(500, f"Failed to serve image file: {e}")
         | 
| 1038 |  | 
| 1039 | 
             
            @router.put("/{image_id}")
         | 
| @@ -1043,15 +1045,15 @@ def update_image_metadata( | |
| 1043 | 
             
                db: Session = Depends(get_db)
         | 
| 1044 | 
             
            ):
         | 
| 1045 | 
             
                """Update image metadata (source, type, epsg, image_type, countries)"""
         | 
| 1046 | 
            -
                 | 
| 1047 | 
            -
                 | 
| 1048 |  | 
| 1049 | 
             
                img = crud.get_image(db, image_id)
         | 
| 1050 | 
             
                if not img:
         | 
| 1051 | 
            -
                     | 
| 1052 | 
             
                    raise HTTPException(404, "Image not found")
         | 
| 1053 |  | 
| 1054 | 
            -
                 | 
| 1055 |  | 
| 1056 | 
             
                try:
         | 
| 1057 | 
             
                    if metadata.source is not None:
         | 
| @@ -1103,17 +1105,17 @@ def update_image_metadata( | |
| 1103 | 
             
                        img.std_v_m = metadata.std_v_m
         | 
| 1104 |  | 
| 1105 | 
             
                    if metadata.countries is not None:
         | 
| 1106 | 
            -
                         | 
| 1107 | 
             
                        img.countries.clear()
         | 
| 1108 | 
             
                        for country_code in metadata.countries:
         | 
| 1109 | 
             
                            country = crud.get_country(db, country_code)
         | 
| 1110 | 
             
                            if country:
         | 
| 1111 | 
             
                                img.countries.append(country)
         | 
| 1112 | 
            -
                                 | 
| 1113 |  | 
| 1114 | 
             
                    db.commit()
         | 
| 1115 | 
             
                    db.refresh(img)
         | 
| 1116 | 
            -
                     | 
| 1117 |  | 
| 1118 | 
             
                    try:
         | 
| 1119 | 
             
                        url = storage.get_object_url(img.file_key)
         | 
| @@ -1125,7 +1127,7 @@ def update_image_metadata( | |
| 1125 |  | 
| 1126 | 
             
                except Exception as e:
         | 
| 1127 | 
             
                    db.rollback()
         | 
| 1128 | 
            -
                     | 
| 1129 | 
             
                    raise HTTPException(500, f"Failed to update image metadata: {str(e)}")
         | 
| 1130 |  | 
| 1131 | 
             
            @router.delete("/{image_id}")
         | 
|  | |
| 1 | 
             
            from fastapi import APIRouter, UploadFile, Form, Depends, HTTPException, Response
         | 
| 2 | 
             
            from pydantic import BaseModel
         | 
| 3 | 
             
            import io
         | 
| 4 | 
            +
            import logging
         | 
| 5 | 
             
            from sqlalchemy.orm import Session
         | 
| 6 | 
             
            from .. import crud, schemas, storage, database
         | 
| 7 | 
             
            from ..config import settings
         | 
|  | |
| 14 | 
             
            import datetime
         | 
| 15 |  | 
| 16 | 
             
            router = APIRouter()
         | 
| 17 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 18 |  | 
| 19 | 
             
            class CopyImageRequest(BaseModel):
         | 
| 20 | 
             
                source_image_id: str
         | 
|  | |
| 51 | 
             
                    try:
         | 
| 52 | 
             
                        countries_list = [{"c_code": c.c_code, "label": c.label, "r_code": c.r_code} for c in img.countries]
         | 
| 53 | 
             
                    except Exception as e:
         | 
| 54 | 
            +
                        logger.warning(f"Error processing countries for image {img.image_id}: {e}")
         | 
| 55 | 
             
                        countries_list = []
         | 
| 56 |  | 
| 57 | 
             
                captions_list = []
         | 
|  | |
| 76 | 
             
                            } for c in img.captions
         | 
| 77 | 
             
                        ]
         | 
| 78 | 
             
                    except Exception as e:
         | 
| 79 | 
            +
                        logger.warning(f"Error processing captions for image {img.image_id}: {e}")
         | 
| 80 | 
             
                        captions_list = []
         | 
| 81 |  | 
| 82 | 
             
                # Get starred status and other caption fields from first caption for backward compatibility
         | 
|  | |
| 118 | 
             
                    try:
         | 
| 119 | 
             
                        thumbnail_url = storage.get_object_url(img.thumbnail_key)
         | 
| 120 | 
             
                    except Exception as e:
         | 
| 121 | 
            +
                        logger.warning(f"Error generating thumbnail URL for image {img.image_id}: {e}")
         | 
| 122 |  | 
| 123 | 
             
                if hasattr(img, 'detail_key') and img.detail_key:
         | 
| 124 | 
             
                    try:
         | 
| 125 | 
             
                        detail_url = storage.get_object_url(img.detail_key)
         | 
| 126 | 
             
                    except Exception as e:
         | 
| 127 | 
            +
                        logger.warning(f"Error generating detail URL for image {img.image_id}: {e}")
         | 
| 128 |  | 
| 129 | 
             
                img_dict = {
         | 
| 130 | 
             
                    "image_id": img.image_id,
         | 
|  | |
| 587 | 
             
                    # Log preprocessing info
         | 
| 588 | 
             
                    preprocessing_info = None
         | 
| 589 | 
             
                    if processed_filename != file.filename:
         | 
| 590 | 
            +
                        logger.info(f"Image preprocessed: {file.filename} -> {processed_filename} ({mime_type})")
         | 
| 591 | 
             
                        preprocessing_info = {
         | 
| 592 | 
             
                            "original_filename": file.filename,
         | 
| 593 | 
             
                            "processed_filename": processed_filename,
         | 
|  | |
| 605 | 
             
                        }
         | 
| 606 |  | 
| 607 | 
             
                except Exception as e:
         | 
| 608 | 
            +
                    logger.error(f"Image preprocessing failed: {str(e)}")
         | 
| 609 | 
             
                    # Fall back to original content if preprocessing fails
         | 
| 610 | 
             
                    processed_content = content
         | 
| 611 | 
             
                    processed_filename = file.filename
         | 
|  | |
| 638 |  | 
| 639 | 
             
                    if thumbnail_result:
         | 
| 640 | 
             
                        thumbnail_key, thumbnail_sha256 = thumbnail_result
         | 
| 641 | 
            +
                        logger.info(f"Thumbnail generated and uploaded: key={thumbnail_key}, sha256={thumbnail_sha256}")
         | 
| 642 |  | 
| 643 | 
             
                    if detail_result:
         | 
| 644 | 
             
                        detail_key, detail_sha256 = detail_result
         | 
| 645 | 
            +
                        logger.info(f"Detail version generated and uploaded: key={detail_key}, sha256={detail_sha256}")
         | 
| 646 |  | 
| 647 | 
             
                except Exception as e:
         | 
| 648 | 
            +
                    logger.error(f"Image resolution processing failed: {str(e)}")
         | 
| 649 | 
             
                    # Continue without processed versions if generation fails
         | 
| 650 |  | 
| 651 | 
             
                try:
         | 
|  | |
| 712 | 
             
                    )
         | 
| 713 |  | 
| 714 | 
             
                except Exception as e:
         | 
| 715 | 
            +
                    logger.error(f"VLM caption generation failed: {str(e)}")
         | 
| 716 | 
             
                    # Continue without caption if VLM fails
         | 
| 717 |  | 
| 718 | 
             
                img_dict = convert_image_to_dict(img, url)
         | 
|  | |
| 800 | 
             
                            quality=95
         | 
| 801 | 
             
                        )
         | 
| 802 | 
             
                    except Exception as e:
         | 
| 803 | 
            +
                        logger.debug(f"Image preprocessing failed: {str(e)}")
         | 
| 804 | 
             
                        processed_content = content
         | 
| 805 | 
             
                        processed_filename = file.filename
         | 
| 806 | 
             
                        mime_type = 'image/png'
         | 
|  | |
| 902 | 
             
                    db.commit()
         | 
| 903 |  | 
| 904 | 
             
                except Exception as e:
         | 
| 905 | 
            +
                    logger.debug(f"VLM error: {e}")
         | 
| 906 | 
             
                    # Create fallback caption
         | 
| 907 | 
             
                    fallback_text = f"Analysis of {len(image_bytes_list)} images"
         | 
| 908 | 
             
                    caption = crud.create_caption(
         | 
|  | |
| 990 | 
             
            @router.get("/{image_id}/file")
         | 
| 991 | 
             
            async def get_image_file(image_id: str, db: Session = Depends(get_db)):
         | 
| 992 | 
             
                """Serve the actual image file"""
         | 
| 993 | 
            +
                logger.debug(f"Serving image file for image_id: {image_id}")
         | 
| 994 |  | 
| 995 | 
             
                img = crud.get_image(db, image_id)
         | 
| 996 | 
             
                if not img:
         | 
| 997 | 
            +
                    logger.warning(f"Image not found: {image_id}")
         | 
| 998 | 
             
                    raise HTTPException(404, "Image not found")
         | 
| 999 |  | 
| 1000 | 
            +
                logger.debug(f"Found image: {img.image_id}, file_key: {img.file_key}")
         | 
| 1001 |  | 
| 1002 | 
             
                try:
         | 
| 1003 | 
             
                    if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local":
         | 
| 1004 | 
            +
                        logger.debug(f"Using S3 storage - serving file content directly")
         | 
| 1005 | 
             
                        try:
         | 
| 1006 | 
             
                            response = storage.s3.get_object(Bucket=settings.S3_BUCKET, Key=img.file_key)
         | 
| 1007 | 
             
                            content = response['Body'].read()
         | 
| 1008 | 
            +
                            logger.debug(f"Read {len(content)} bytes from S3")
         | 
| 1009 | 
             
                        except Exception as e:
         | 
| 1010 | 
            +
                            logger.error(f"Failed to get S3 object: {e}")
         | 
| 1011 | 
             
                            raise HTTPException(500, f"Failed to retrieve image from storage: {e}")
         | 
| 1012 | 
             
                    else:
         | 
| 1013 | 
            +
                        logger.debug(f"Using local storage")
         | 
| 1014 | 
             
                        import os
         | 
| 1015 | 
             
                        file_path = os.path.join(settings.STORAGE_DIR, img.file_key)
         | 
| 1016 | 
            +
                        logger.debug(f"Reading from: {file_path}")
         | 
| 1017 | 
            +
                        logger.debug(f"File exists: {os.path.exists(file_path)}")
         | 
| 1018 |  | 
| 1019 | 
             
                        if not os.path.exists(file_path):
         | 
| 1020 | 
            +
                            logger.error(f"File not found at: {file_path}")
         | 
| 1021 | 
             
                            raise FileNotFoundError(f"Image file not found: {file_path}")
         | 
| 1022 |  | 
| 1023 | 
             
                        with open(file_path, 'rb') as f:
         | 
| 1024 | 
             
                            content = f.read()
         | 
| 1025 |  | 
| 1026 | 
            +
                        logger.debug(f"Read {len(content)} bytes from file")
         | 
| 1027 |  | 
| 1028 | 
             
                    import mimetypes
         | 
| 1029 | 
             
                    content_type, _ = mimetypes.guess_type(img.file_key)
         | 
| 1030 | 
             
                    if not content_type:
         | 
| 1031 | 
             
                        content_type = 'application/octet-stream'
         | 
| 1032 |  | 
| 1033 | 
            +
                    logger.debug(f"Serving image with content-type: {content_type}, size: {len(content)} bytes")
         | 
| 1034 | 
             
                    return Response(content=content, media_type=content_type)
         | 
| 1035 | 
             
                except Exception as e:
         | 
| 1036 | 
            +
                    logger.error(f"Error serving image: {e}")
         | 
| 1037 | 
             
                    import traceback
         | 
| 1038 | 
            +
                    logger.debug(f"Full traceback: {traceback.format_exc()}")
         | 
| 1039 | 
             
                    raise HTTPException(500, f"Failed to serve image file: {e}")
         | 
| 1040 |  | 
| 1041 | 
             
            @router.put("/{image_id}")
         | 
|  | |
| 1045 | 
             
                db: Session = Depends(get_db)
         | 
| 1046 | 
             
            ):
         | 
| 1047 | 
             
                """Update image metadata (source, type, epsg, image_type, countries)"""
         | 
| 1048 | 
            +
                logger.debug(f"DEBUG: Updating metadata for image {image_id}")
         | 
| 1049 | 
            +
                logger.debug(f"DEBUG: Metadata received: {metadata}")
         | 
| 1050 |  | 
| 1051 | 
             
                img = crud.get_image(db, image_id)
         | 
| 1052 | 
             
                if not img:
         | 
| 1053 | 
            +
                    logger.debug(f"DEBUG: Image {image_id} not found in database")
         | 
| 1054 | 
             
                    raise HTTPException(404, "Image not found")
         | 
| 1055 |  | 
| 1056 | 
            +
                logger.debug(f"DEBUG: Found image {image_id} in database")
         | 
| 1057 |  | 
| 1058 | 
             
                try:
         | 
| 1059 | 
             
                    if metadata.source is not None:
         | 
|  | |
| 1105 | 
             
                        img.std_v_m = metadata.std_v_m
         | 
| 1106 |  | 
| 1107 | 
             
                    if metadata.countries is not None:
         | 
| 1108 | 
            +
                        logger.debug(f"DEBUG: Updating countries to: {metadata.countries}")
         | 
| 1109 | 
             
                        img.countries.clear()
         | 
| 1110 | 
             
                        for country_code in metadata.countries:
         | 
| 1111 | 
             
                            country = crud.get_country(db, country_code)
         | 
| 1112 | 
             
                            if country:
         | 
| 1113 | 
             
                                img.countries.append(country)
         | 
| 1114 | 
            +
                                logger.debug(f"DEBUG: Added country: {country_code}")
         | 
| 1115 |  | 
| 1116 | 
             
                    db.commit()
         | 
| 1117 | 
             
                    db.refresh(img)
         | 
| 1118 | 
            +
                    logger.debug(f"DEBUG: Metadata update successful for image {image_id}")
         | 
| 1119 |  | 
| 1120 | 
             
                    try:
         | 
| 1121 | 
             
                        url = storage.get_object_url(img.file_key)
         | 
|  | |
| 1127 |  | 
| 1128 | 
             
                except Exception as e:
         | 
| 1129 | 
             
                    db.rollback()
         | 
| 1130 | 
            +
                    logger.debug(f"DEBUG: Metadata update failed for image {image_id}: {str(e)}")
         | 
| 1131 | 
             
                    raise HTTPException(500, f"Failed to update image metadata: {str(e)}")
         | 
| 1132 |  | 
| 1133 | 
             
            @router.delete("/{image_id}")
         | 
    	
        py_backend/app/services/gpt4v_service.py
    CHANGED
    
    | @@ -4,29 +4,32 @@ import openai | |
| 4 | 
             
            import base64
         | 
| 5 | 
             
            import asyncio
         | 
| 6 | 
             
            import json
         | 
|  | |
|  | |
|  | |
| 7 |  | 
| 8 | 
             
            class GPT4VService(VLMService):
         | 
| 9 | 
             
                """GPT-4 Vision service implementation"""
         | 
| 10 |  | 
| 11 | 
             
                def __init__(self, api_key: str):
         | 
| 12 | 
             
                    super().__init__("GPT4V", ModelType.GPT4V)
         | 
| 13 | 
            -
                     | 
| 14 | 
             
                    self.client = openai.OpenAI(api_key=api_key)
         | 
| 15 | 
             
                    self.model_name = "GPT-4O"
         | 
| 16 | 
            -
                     | 
| 17 |  | 
| 18 | 
             
                async def generate_caption(self, image_bytes: bytes, prompt: str, metadata_instructions: str = "") -> Dict[str, Any]:
         | 
| 19 | 
             
                    """Generate caption using GPT-4 Vision"""
         | 
| 20 | 
             
                    try:
         | 
| 21 | 
             
                        # Debug logging
         | 
| 22 | 
             
                        api_key_preview = self.client.api_key[:10] + "..." + self.client.api_key[-4:] if self.client.api_key else "None"
         | 
| 23 | 
            -
                         | 
| 24 | 
            -
                         | 
| 25 | 
            -
                         | 
| 26 |  | 
| 27 | 
             
                        image_base64 = base64.b64encode(image_bytes).decode('utf-8')
         | 
| 28 |  | 
| 29 | 
            -
                         | 
| 30 | 
             
                        response = await asyncio.to_thread(
         | 
| 31 | 
             
                            self.client.chat.completions.create,
         | 
| 32 | 
             
                            model="gpt-4o",
         | 
| @@ -46,7 +49,7 @@ class GPT4VService(VLMService): | |
| 46 | 
             
                            ],
         | 
| 47 | 
             
                            max_tokens=800
         | 
| 48 | 
             
                        )
         | 
| 49 | 
            -
                         | 
| 50 |  | 
| 51 | 
             
                        content = response.choices[0].message.content
         | 
| 52 |  | 
| @@ -69,7 +72,7 @@ class GPT4VService(VLMService): | |
| 69 | 
             
                                    try:
         | 
| 70 | 
             
                                        metadata = json.loads(json_str)
         | 
| 71 | 
             
                                    except json.JSONDecodeError as e:
         | 
| 72 | 
            -
                                         | 
| 73 | 
             
                            else:
         | 
| 74 | 
             
                                import re
         | 
| 75 | 
             
                                json_match = re.search(r'\{[^{}]*"metadata"[^{}]*\{[^{}]*\}', content)
         | 
| @@ -101,11 +104,11 @@ class GPT4VService(VLMService): | |
| 101 | 
             
                        }
         | 
| 102 |  | 
| 103 | 
             
                    except Exception as e:
         | 
| 104 | 
            -
                         | 
| 105 | 
            -
                         | 
| 106 | 
             
                        if hasattr(e, 'response'):
         | 
| 107 | 
            -
                             | 
| 108 | 
            -
                             | 
| 109 | 
             
                        raise Exception(f"GPT-4 Vision API error: {str(e)}")
         | 
| 110 |  | 
| 111 | 
             
                async def generate_multi_image_caption(self, image_bytes_list: List[bytes], prompt: str, metadata_instructions: str = "") -> Dict[str, Any]:
         | 
| @@ -157,7 +160,7 @@ class GPT4VService(VLMService): | |
| 157 | 
             
                                    try:
         | 
| 158 | 
             
                                        metadata = json.loads(json_str)
         | 
| 159 | 
             
                                    except json.JSONDecodeError as e:
         | 
| 160 | 
            -
                                         | 
| 161 | 
             
                            else:
         | 
| 162 | 
             
                                import re
         | 
| 163 | 
             
                                json_match = re.search(r'\{[^{}]*"metadata"[^{}]*\{[^{}]*\}', content)
         | 
| @@ -190,9 +193,9 @@ class GPT4VService(VLMService): | |
| 190 | 
             
                        }
         | 
| 191 |  | 
| 192 | 
             
                    except Exception as e:
         | 
| 193 | 
            -
                         | 
| 194 | 
            -
                         | 
| 195 | 
             
                        if hasattr(e, 'response'):
         | 
| 196 | 
            -
                             | 
| 197 | 
            -
                             | 
| 198 | 
             
                        raise Exception(f"GPT-4 Vision API error: {str(e)}") 
         | 
|  | |
| 4 | 
             
            import base64
         | 
| 5 | 
             
            import asyncio
         | 
| 6 | 
             
            import json
         | 
| 7 | 
            +
            import logging
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 10 |  | 
| 11 | 
             
            class GPT4VService(VLMService):
         | 
| 12 | 
             
                """GPT-4 Vision service implementation"""
         | 
| 13 |  | 
| 14 | 
             
                def __init__(self, api_key: str):
         | 
| 15 | 
             
                    super().__init__("GPT4V", ModelType.GPT4V)
         | 
| 16 | 
            +
                    logger.debug(f"Initializing with API key: {api_key[:10]}...{api_key[-4:] if api_key else 'None'}")
         | 
| 17 | 
             
                    self.client = openai.OpenAI(api_key=api_key)
         | 
| 18 | 
             
                    self.model_name = "GPT-4O"
         | 
| 19 | 
            +
                    logger.info("Initialized successfully")
         | 
| 20 |  | 
| 21 | 
             
                async def generate_caption(self, image_bytes: bytes, prompt: str, metadata_instructions: str = "") -> Dict[str, Any]:
         | 
| 22 | 
             
                    """Generate caption using GPT-4 Vision"""
         | 
| 23 | 
             
                    try:
         | 
| 24 | 
             
                        # Debug logging
         | 
| 25 | 
             
                        api_key_preview = self.client.api_key[:10] + "..." + self.client.api_key[-4:] if self.client.api_key else "None"
         | 
| 26 | 
            +
                        logger.debug(f"API Key preview: {api_key_preview}")
         | 
| 27 | 
            +
                        logger.debug(f"Image size: {len(image_bytes)} bytes")
         | 
| 28 | 
            +
                        logger.debug(f"Prompt length: {len(prompt)} chars")
         | 
| 29 |  | 
| 30 | 
             
                        image_base64 = base64.b64encode(image_bytes).decode('utf-8')
         | 
| 31 |  | 
| 32 | 
            +
                        logger.debug(f"Making API call to OpenAI...")
         | 
| 33 | 
             
                        response = await asyncio.to_thread(
         | 
| 34 | 
             
                            self.client.chat.completions.create,
         | 
| 35 | 
             
                            model="gpt-4o",
         | 
|  | |
| 49 | 
             
                            ],
         | 
| 50 | 
             
                            max_tokens=800
         | 
| 51 | 
             
                        )
         | 
| 52 | 
            +
                        logger.info("API call successful!")
         | 
| 53 |  | 
| 54 | 
             
                        content = response.choices[0].message.content
         | 
| 55 |  | 
|  | |
| 72 | 
             
                                    try:
         | 
| 73 | 
             
                                        metadata = json.loads(json_str)
         | 
| 74 | 
             
                                    except json.JSONDecodeError as e:
         | 
| 75 | 
            +
                                        logger.error(f"JSON parse error: {e}")
         | 
| 76 | 
             
                            else:
         | 
| 77 | 
             
                                import re
         | 
| 78 | 
             
                                json_match = re.search(r'\{[^{}]*"metadata"[^{}]*\{[^{}]*\}', content)
         | 
|  | |
| 104 | 
             
                        }
         | 
| 105 |  | 
| 106 | 
             
                    except Exception as e:
         | 
| 107 | 
            +
                        logger.error(f"API call failed: {str(e)}")
         | 
| 108 | 
            +
                        logger.error(f"Error type: {type(e).__name__}")
         | 
| 109 | 
             
                        if hasattr(e, 'response'):
         | 
| 110 | 
            +
                            logger.error(f"Response status: {getattr(e.response, 'status_code', 'Unknown')}")
         | 
| 111 | 
            +
                            logger.error(f"Response body: {getattr(e.response, 'text', 'Unknown')}")
         | 
| 112 | 
             
                        raise Exception(f"GPT-4 Vision API error: {str(e)}")
         | 
| 113 |  | 
| 114 | 
             
                async def generate_multi_image_caption(self, image_bytes_list: List[bytes], prompt: str, metadata_instructions: str = "") -> Dict[str, Any]:
         | 
|  | |
| 160 | 
             
                                    try:
         | 
| 161 | 
             
                                        metadata = json.loads(json_str)
         | 
| 162 | 
             
                                    except json.JSONDecodeError as e:
         | 
| 163 | 
            +
                                        logger.error(f"JSON parse error: {e}")
         | 
| 164 | 
             
                            else:
         | 
| 165 | 
             
                                import re
         | 
| 166 | 
             
                                json_match = re.search(r'\{[^{}]*"metadata"[^{}]*\{[^{}]*\}', content)
         | 
|  | |
| 193 | 
             
                        }
         | 
| 194 |  | 
| 195 | 
             
                    except Exception as e:
         | 
| 196 | 
            +
                        logger.error(f"API call failed: {str(e)}")
         | 
| 197 | 
            +
                        logger.error(f"Error type: {type(e).__name__}")
         | 
| 198 | 
             
                        if hasattr(e, 'response'):
         | 
| 199 | 
            +
                            logger.error(f"Response status: {getattr(e.response, 'status_code', 'Unknown')}")
         | 
| 200 | 
            +
                            logger.error(f"Response body: {getattr(e.response, 'text', 'Unknown')}")
         | 
| 201 | 
             
                        raise Exception(f"GPT-4 Vision API error: {str(e)}") 
         | 
    	
        py_backend/app/services/image_preprocessor.py
    CHANGED
    
    | @@ -1,8 +1,11 @@ | |
| 1 | 
             
            import io
         | 
| 2 | 
             
            import mimetypes
         | 
|  | |
| 3 | 
             
            from typing import Tuple, Optional, BinaryIO
         | 
| 4 | 
             
            from PIL import Image, ImageOps
         | 
| 5 |  | 
|  | |
|  | |
| 6 | 
             
            # Import PyMuPDF for PDF processing
         | 
| 7 | 
             
            try:
         | 
| 8 | 
             
                import fitz  # PyMuPDF for PDF processing
         | 
| @@ -146,7 +149,7 @@ class ImagePreprocessor: | |
| 146 | 
             
                        ImagePreprocessor.PDF_ZOOM_FACTOR = zoom_factor
         | 
| 147 | 
             
                        ImagePreprocessor.PDF_COMPRESS_LEVEL = compress_level
         | 
| 148 |  | 
| 149 | 
            -
                     | 
| 150 | 
             
                          f"compression={ImagePreprocessor.PDF_COMPRESS_LEVEL}, mode={quality_mode}")
         | 
| 151 |  | 
| 152 | 
             
                @staticmethod
         | 
| @@ -161,7 +164,7 @@ class ImagePreprocessor: | |
| 161 | 
             
                        raise ValueError("PDF processing is not available. PyMuPDF is not installed.")
         | 
| 162 |  | 
| 163 | 
             
                    try:
         | 
| 164 | 
            -
                         | 
| 165 |  | 
| 166 | 
             
                        # Open PDF with PyMuPDF
         | 
| 167 | 
             
                        pdf_document = fitz.open(stream=file_content, filetype="pdf")
         | 
| @@ -169,7 +172,7 @@ class ImagePreprocessor: | |
| 169 | 
             
                        if len(pdf_document) == 0:
         | 
| 170 | 
             
                            raise ValueError("PDF has no pages")
         | 
| 171 |  | 
| 172 | 
            -
                         | 
| 173 |  | 
| 174 | 
             
                        # Get first page
         | 
| 175 | 
             
                        page = pdf_document[0]
         | 
| @@ -178,7 +181,7 @@ class ImagePreprocessor: | |
| 178 | 
             
                        zoom = ImagePreprocessor.PDF_ZOOM_FACTOR
         | 
| 179 | 
             
                        mat = fitz.Matrix(zoom, zoom)
         | 
| 180 |  | 
| 181 | 
            -
                         | 
| 182 |  | 
| 183 | 
             
                        # Render page to image with optimized settings
         | 
| 184 | 
             
                        pix = page.get_pixmap(
         | 
| @@ -187,7 +190,7 @@ class ImagePreprocessor: | |
| 187 | 
             
                            colorspace="rgb"  # Force RGB colorspace
         | 
| 188 | 
             
                        )
         | 
| 189 |  | 
| 190 | 
            -
                         | 
| 191 |  | 
| 192 | 
             
                        # Convert to PIL Image - use more efficient method
         | 
| 193 | 
             
                        img_data = pix.tobytes("png")
         | 
| @@ -197,7 +200,7 @@ class ImagePreprocessor: | |
| 197 | 
             
                        if img.mode in ('RGBA', 'LA', 'P'):
         | 
| 198 | 
             
                            img = img.convert('RGB')
         | 
| 199 |  | 
| 200 | 
            -
                         | 
| 201 |  | 
| 202 | 
             
                        # Save to bytes with optimization
         | 
| 203 | 
             
                        output_buffer = io.BytesIO()
         | 
| @@ -218,12 +221,12 @@ class ImagePreprocessor: | |
| 218 | 
             
                        base_name = os.path.splitext(filename)[0]
         | 
| 219 | 
             
                        new_filename = f"{base_name}{new_extension}"
         | 
| 220 |  | 
| 221 | 
            -
                         | 
| 222 |  | 
| 223 | 
             
                        return output_buffer.getvalue(), new_filename, new_mime_type
         | 
| 224 |  | 
| 225 | 
             
                    except Exception as e:
         | 
| 226 | 
            -
                         | 
| 227 | 
             
                        raise ValueError(f"Failed to process PDF: {str(e)}")
         | 
| 228 |  | 
| 229 | 
             
                @staticmethod
         | 
|  | |
| 1 | 
             
            import io
         | 
| 2 | 
             
            import mimetypes
         | 
| 3 | 
            +
            import logging
         | 
| 4 | 
             
            from typing import Tuple, Optional, BinaryIO
         | 
| 5 | 
             
            from PIL import Image, ImageOps
         | 
| 6 |  | 
| 7 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 8 | 
            +
             | 
| 9 | 
             
            # Import PyMuPDF for PDF processing
         | 
| 10 | 
             
            try:
         | 
| 11 | 
             
                import fitz  # PyMuPDF for PDF processing
         | 
|  | |
| 149 | 
             
                        ImagePreprocessor.PDF_ZOOM_FACTOR = zoom_factor
         | 
| 150 | 
             
                        ImagePreprocessor.PDF_COMPRESS_LEVEL = compress_level
         | 
| 151 |  | 
| 152 | 
            +
                    logger.debug(f"PDF processing configured: zoom={ImagePreprocessor.PDF_ZOOM_FACTOR}, "
         | 
| 153 | 
             
                          f"compression={ImagePreprocessor.PDF_COMPRESS_LEVEL}, mode={quality_mode}")
         | 
| 154 |  | 
| 155 | 
             
                @staticmethod
         | 
|  | |
| 164 | 
             
                        raise ValueError("PDF processing is not available. PyMuPDF is not installed.")
         | 
| 165 |  | 
| 166 | 
             
                    try:
         | 
| 167 | 
            +
                        logger.info(f"Starting PDF processing for {filename}...")
         | 
| 168 |  | 
| 169 | 
             
                        # Open PDF with PyMuPDF
         | 
| 170 | 
             
                        pdf_document = fitz.open(stream=file_content, filetype="pdf")
         | 
|  | |
| 172 | 
             
                        if len(pdf_document) == 0:
         | 
| 173 | 
             
                            raise ValueError("PDF has no pages")
         | 
| 174 |  | 
| 175 | 
            +
                        logger.debug(f"PDF opened successfully, processing page 1 of {len(pdf_document)}...")
         | 
| 176 |  | 
| 177 | 
             
                        # Get first page
         | 
| 178 | 
             
                        page = pdf_document[0]
         | 
|  | |
| 181 | 
             
                        zoom = ImagePreprocessor.PDF_ZOOM_FACTOR
         | 
| 182 | 
             
                        mat = fitz.Matrix(zoom, zoom)
         | 
| 183 |  | 
| 184 | 
            +
                        logger.debug(f"Rendering page at {zoom}x zoom...")
         | 
| 185 |  | 
| 186 | 
             
                        # Render page to image with optimized settings
         | 
| 187 | 
             
                        pix = page.get_pixmap(
         | 
|  | |
| 190 | 
             
                            colorspace="rgb"  # Force RGB colorspace
         | 
| 191 | 
             
                        )
         | 
| 192 |  | 
| 193 | 
            +
                        logger.debug(f"Page rendered, size: {pix.width}x{pix.height}")
         | 
| 194 |  | 
| 195 | 
             
                        # Convert to PIL Image - use more efficient method
         | 
| 196 | 
             
                        img_data = pix.tobytes("png")
         | 
|  | |
| 200 | 
             
                        if img.mode in ('RGBA', 'LA', 'P'):
         | 
| 201 | 
             
                            img = img.convert('RGB')
         | 
| 202 |  | 
| 203 | 
            +
                        logger.debug(f"Image converted to RGB, mode: {img.mode}")
         | 
| 204 |  | 
| 205 | 
             
                        # Save to bytes with optimization
         | 
| 206 | 
             
                        output_buffer = io.BytesIO()
         | 
|  | |
| 221 | 
             
                        base_name = os.path.splitext(filename)[0]
         | 
| 222 | 
             
                        new_filename = f"{base_name}{new_extension}"
         | 
| 223 |  | 
| 224 | 
            +
                        logger.info(f"PDF processing completed: {filename} -> {new_filename}")
         | 
| 225 |  | 
| 226 | 
             
                        return output_buffer.getvalue(), new_filename, new_mime_type
         | 
| 227 |  | 
| 228 | 
             
                    except Exception as e:
         | 
| 229 | 
            +
                        logger.error(f"PDF processing failed: {str(e)}")
         | 
| 230 | 
             
                        raise ValueError(f"Failed to process PDF: {str(e)}")
         | 
| 231 |  | 
| 232 | 
             
                @staticmethod
         | 
    	
        py_backend/app/services/schema_validator.py
    CHANGED
    
    | @@ -4,8 +4,8 @@ from jsonschema import validate, ValidationError | |
| 4 | 
             
            from jsonschema.validators import Draft7Validator
         | 
| 5 | 
             
            import logging
         | 
| 6 | 
             
            from sqlalchemy.orm import Session
         | 
| 7 | 
            -
            from  | 
| 8 | 
            -
            from  | 
| 9 |  | 
| 10 | 
             
            logger = logging.getLogger(__name__)
         | 
| 11 |  | 
|  | |
| 4 | 
             
            from jsonschema.validators import Draft7Validator
         | 
| 5 | 
             
            import logging
         | 
| 6 | 
             
            from sqlalchemy.orm import Session
         | 
| 7 | 
            +
            from app.database import SessionLocal
         | 
| 8 | 
            +
            from app import crud
         | 
| 9 |  | 
| 10 | 
             
            logger = logging.getLogger(__name__)
         | 
| 11 |  | 
    	
        py_backend/app/services/thumbnail_service.py
    CHANGED
    
    | @@ -1,9 +1,12 @@ | |
| 1 | 
             
            import io
         | 
|  | |
| 2 | 
             
            from PIL import Image, ImageOps
         | 
| 3 | 
             
            from typing import Tuple, Optional
         | 
| 4 | 
             
            import base64
         | 
| 5 | 
             
            from ..storage import upload_fileobj, get_object_url
         | 
| 6 |  | 
|  | |
|  | |
| 7 | 
             
            class ImageProcessingService:
         | 
| 8 | 
             
                """Service for creating and managing multiple image resolutions"""
         | 
| 9 |  | 
| @@ -76,7 +79,7 @@ class ImageProcessingService: | |
| 76 | 
             
                        return resized_bytes, resized_filename
         | 
| 77 |  | 
| 78 | 
             
                    except Exception as e:
         | 
| 79 | 
            -
                         | 
| 80 | 
             
                        # Return original content as fallback
         | 
| 81 | 
             
                        return image_content, filename
         | 
| 82 |  | 
| @@ -153,7 +156,7 @@ class ImageProcessingService: | |
| 153 | 
             
                        return resized_bytes, resized_filename
         | 
| 154 |  | 
| 155 | 
             
                    except Exception as e:
         | 
| 156 | 
            -
                         | 
| 157 | 
             
                        # Return original content as fallback
         | 
| 158 | 
             
                        return image_content, filename
         | 
| 159 |  | 
| @@ -227,7 +230,7 @@ class ImageProcessingService: | |
| 227 | 
             
                        return resized_key, resized_sha256
         | 
| 228 |  | 
| 229 | 
             
                    except Exception as e:
         | 
| 230 | 
            -
                         | 
| 231 | 
             
                        return None
         | 
| 232 |  | 
| 233 | 
             
                @staticmethod
         | 
| @@ -278,7 +281,7 @@ class ImageProcessingService: | |
| 278 | 
             
                        return resized_key, resized_sha256
         | 
| 279 |  | 
| 280 | 
             
                    except Exception as e:
         | 
| 281 | 
            -
                         | 
| 282 | 
             
                        return None
         | 
| 283 |  | 
| 284 | 
             
                @staticmethod
         | 
| @@ -318,7 +321,7 @@ class ImageProcessingService: | |
| 318 | 
             
                        return uploaded_key, sha256
         | 
| 319 |  | 
| 320 | 
             
                    except Exception as e:
         | 
| 321 | 
            -
                         | 
| 322 | 
             
                        return None
         | 
| 323 |  | 
| 324 | 
             
                @staticmethod
         | 
| @@ -357,7 +360,7 @@ class ImageProcessingService: | |
| 357 | 
             
                            )
         | 
| 358 |  | 
| 359 | 
             
                    except Exception as e:
         | 
| 360 | 
            -
                         | 
| 361 |  | 
| 362 | 
             
                    return thumbnail_result, detail_result
         | 
| 363 |  | 
|  | |
| 1 | 
             
            import io
         | 
| 2 | 
            +
            import logging
         | 
| 3 | 
             
            from PIL import Image, ImageOps
         | 
| 4 | 
             
            from typing import Tuple, Optional
         | 
| 5 | 
             
            import base64
         | 
| 6 | 
             
            from ..storage import upload_fileobj, get_object_url
         | 
| 7 |  | 
| 8 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 9 | 
            +
             | 
| 10 | 
             
            class ImageProcessingService:
         | 
| 11 | 
             
                """Service for creating and managing multiple image resolutions"""
         | 
| 12 |  | 
|  | |
| 79 | 
             
                        return resized_bytes, resized_filename
         | 
| 80 |  | 
| 81 | 
             
                    except Exception as e:
         | 
| 82 | 
            +
                        logger.error(f"Error creating resized image: {str(e)}")
         | 
| 83 | 
             
                        # Return original content as fallback
         | 
| 84 | 
             
                        return image_content, filename
         | 
| 85 |  | 
|  | |
| 156 | 
             
                        return resized_bytes, resized_filename
         | 
| 157 |  | 
| 158 | 
             
                    except Exception as e:
         | 
| 159 | 
            +
                        logger.error(f"Error creating resized image: {str(e)}")
         | 
| 160 | 
             
                        # Return original content as fallback
         | 
| 161 | 
             
                        return image_content, filename
         | 
| 162 |  | 
|  | |
| 230 | 
             
                        return resized_key, resized_sha256
         | 
| 231 |  | 
| 232 | 
             
                    except Exception as e:
         | 
| 233 | 
            +
                        logger.error(f"Error uploading resized image: {str(e)}")
         | 
| 234 | 
             
                        return None
         | 
| 235 |  | 
| 236 | 
             
                @staticmethod
         | 
|  | |
| 281 | 
             
                        return resized_key, resized_sha256
         | 
| 282 |  | 
| 283 | 
             
                    except Exception as e:
         | 
| 284 | 
            +
                        logger.error(f"Error uploading resized image: {str(e)}")
         | 
| 285 | 
             
                        return None
         | 
| 286 |  | 
| 287 | 
             
                @staticmethod
         | 
|  | |
| 321 | 
             
                        return uploaded_key, sha256
         | 
| 322 |  | 
| 323 | 
             
                    except Exception as e:
         | 
| 324 | 
            +
                        logger.error(f"Error uploading image bytes: {str(e)}")
         | 
| 325 | 
             
                        return None
         | 
| 326 |  | 
| 327 | 
             
                @staticmethod
         | 
|  | |
| 360 | 
             
                            )
         | 
| 361 |  | 
| 362 | 
             
                    except Exception as e:
         | 
| 363 | 
            +
                        logger.error(f"Error processing image resolutions: {str(e)}")
         | 
| 364 |  | 
| 365 | 
             
                    return thumbnail_result, detail_result
         | 
| 366 |  | 
    	
        py_backend/app/services/upload_service.py
    ADDED
    
    | @@ -0,0 +1,293 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Upload Service
         | 
| 3 | 
            +
            Handles the core business logic for image uploads and processing
         | 
| 4 | 
            +
            """
         | 
| 5 | 
            +
            import logging
         | 
| 6 | 
            +
            import io
         | 
| 7 | 
            +
            from typing import Optional, Dict, Any, Tuple
         | 
| 8 | 
            +
            from fastapi import UploadFile
         | 
| 9 | 
            +
            from sqlalchemy.orm import Session
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            from .. import crud, schemas, storage
         | 
| 12 | 
            +
            from ..services.image_preprocessor import ImagePreprocessor
         | 
| 13 | 
            +
            from ..services.thumbnail_service import ImageProcessingService
         | 
| 14 | 
            +
            from ..services.vlm_service import vlm_manager
         | 
| 15 | 
            +
            from ..utils.image_utils import convert_image_to_dict
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            class UploadService:
         | 
| 20 | 
            +
                """Service for handling image upload operations"""
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                @staticmethod
         | 
| 23 | 
            +
                async def process_single_upload(
         | 
| 24 | 
            +
                    file: UploadFile,
         | 
| 25 | 
            +
                    source: Optional[str],
         | 
| 26 | 
            +
                    event_type: str,
         | 
| 27 | 
            +
                    countries: str,
         | 
| 28 | 
            +
                    epsg: str,
         | 
| 29 | 
            +
                    image_type: str,
         | 
| 30 | 
            +
                    title: str,
         | 
| 31 | 
            +
                    model_name: Optional[str],
         | 
| 32 | 
            +
                    # Drone-specific fields
         | 
| 33 | 
            +
                    center_lon: Optional[float] = None,
         | 
| 34 | 
            +
                    center_lat: Optional[float] = None,
         | 
| 35 | 
            +
                    amsl_m: Optional[float] = None,
         | 
| 36 | 
            +
                    agl_m: Optional[float] = None,
         | 
| 37 | 
            +
                    heading_deg: Optional[float] = None,
         | 
| 38 | 
            +
                    yaw_deg: Optional[float] = None,
         | 
| 39 | 
            +
                    pitch_deg: Optional[float] = None,
         | 
| 40 | 
            +
                    roll_deg: Optional[float] = None,
         | 
| 41 | 
            +
                    rtk_fix: Optional[bool] = None,
         | 
| 42 | 
            +
                    std_h_m: Optional[float] = None,
         | 
| 43 | 
            +
                    std_v_m: Optional[float] = None,
         | 
| 44 | 
            +
                    db: Session = None
         | 
| 45 | 
            +
                ) -> Dict[str, Any]:
         | 
| 46 | 
            +
                    """Process a single image upload"""
         | 
| 47 | 
            +
                    logger.info(f"Processing single upload: {file.filename}")
         | 
| 48 | 
            +
                    
         | 
| 49 | 
            +
                    # Parse and validate input
         | 
| 50 | 
            +
                    countries_list = [c.strip() for c in countries.split(',') if c.strip()] if countries else []
         | 
| 51 | 
            +
                    
         | 
| 52 | 
            +
                    # Set defaults based on image type
         | 
| 53 | 
            +
                    if image_type == "drone_image":
         | 
| 54 | 
            +
                        if not event_type or event_type.strip() == "":
         | 
| 55 | 
            +
                            event_type = "OTHER"
         | 
| 56 | 
            +
                        if not epsg or epsg.strip() == "":
         | 
| 57 | 
            +
                            epsg = "OTHER"
         | 
| 58 | 
            +
                    else:
         | 
| 59 | 
            +
                        if not source or source.strip() == "":
         | 
| 60 | 
            +
                            source = "OTHER"
         | 
| 61 | 
            +
                        if not event_type or event_type.strip() == "":
         | 
| 62 | 
            +
                            event_type = "OTHER"
         | 
| 63 | 
            +
                        if not epsg or epsg.strip() == "":
         | 
| 64 | 
            +
                            epsg = "OTHER"
         | 
| 65 | 
            +
                        
         | 
| 66 | 
            +
                        # Clear drone fields for non-drone images
         | 
| 67 | 
            +
                        center_lon = center_lat = amsl_m = agl_m = None
         | 
| 68 | 
            +
                        heading_deg = yaw_deg = pitch_deg = roll_deg = None
         | 
| 69 | 
            +
                        rtk_fix = std_h_m = std_v_m = None
         | 
| 70 | 
            +
                    
         | 
| 71 | 
            +
                    if not image_type or image_type.strip() == "":
         | 
| 72 | 
            +
                        image_type = "crisis_map"
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                    # Read file content
         | 
| 75 | 
            +
                    content = await file.read()
         | 
| 76 | 
            +
                    
         | 
| 77 | 
            +
                    # Preprocess image
         | 
| 78 | 
            +
                    preprocessing_info = await UploadService._preprocess_image(content, file.filename)
         | 
| 79 | 
            +
                    
         | 
| 80 | 
            +
                    # Upload to storage
         | 
| 81 | 
            +
                    key, sha = await UploadService._upload_to_storage(
         | 
| 82 | 
            +
                        preprocessing_info['processed_content'],
         | 
| 83 | 
            +
                        preprocessing_info['processed_filename'],
         | 
| 84 | 
            +
                        preprocessing_info['mime_type']
         | 
| 85 | 
            +
                    )
         | 
| 86 | 
            +
                    
         | 
| 87 | 
            +
                    # Generate thumbnails and detail versions
         | 
| 88 | 
            +
                    thumbnail_result, detail_result = await UploadService._generate_image_versions(
         | 
| 89 | 
            +
                        preprocessing_info['processed_content'],
         | 
| 90 | 
            +
                        preprocessing_info['processed_filename']
         | 
| 91 | 
            +
                    )
         | 
| 92 | 
            +
                    
         | 
| 93 | 
            +
                    # Create database record
         | 
| 94 | 
            +
                    img = crud.create_image(
         | 
| 95 | 
            +
                        db, source, event_type, key, sha, countries_list, epsg, image_type,
         | 
| 96 | 
            +
                        center_lon, center_lat, amsl_m, agl_m,
         | 
| 97 | 
            +
                        heading_deg, yaw_deg, pitch_deg, roll_deg,
         | 
| 98 | 
            +
                        rtk_fix, std_h_m, std_v_m,
         | 
| 99 | 
            +
                        thumbnail_key=thumbnail_result[0] if thumbnail_result else None,
         | 
| 100 | 
            +
                        detail_key=detail_result[0] if detail_result else None
         | 
| 101 | 
            +
                    )
         | 
| 102 | 
            +
                    
         | 
| 103 | 
            +
                    # Generate caption if requested
         | 
| 104 | 
            +
                    if title or model_name:
         | 
| 105 | 
            +
                        await UploadService._generate_caption(
         | 
| 106 | 
            +
                            img, preprocessing_info['processed_content'], title, model_name, db
         | 
| 107 | 
            +
                        )
         | 
| 108 | 
            +
                    
         | 
| 109 | 
            +
                    # Generate response
         | 
| 110 | 
            +
                    url = storage.get_object_url(key)
         | 
| 111 | 
            +
                    img_dict = convert_image_to_dict(img, url)
         | 
| 112 | 
            +
                    img_dict['preprocessing_info'] = preprocessing_info
         | 
| 113 | 
            +
                    
         | 
| 114 | 
            +
                    logger.info(f"Successfully processed upload: {img.image_id}")
         | 
| 115 | 
            +
                    return {
         | 
| 116 | 
            +
                        'image': schemas.ImageOut(**img_dict),
         | 
| 117 | 
            +
                        'preprocessing_info': preprocessing_info
         | 
| 118 | 
            +
                    }
         | 
| 119 | 
            +
                
         | 
| 120 | 
            +
                @staticmethod
         | 
| 121 | 
            +
                async def process_multi_upload(
         | 
| 122 | 
            +
                    files: list[UploadFile],
         | 
| 123 | 
            +
                    source: Optional[str],
         | 
| 124 | 
            +
                    event_type: str,
         | 
| 125 | 
            +
                    countries: str,
         | 
| 126 | 
            +
                    epsg: str,
         | 
| 127 | 
            +
                    image_type: str,
         | 
| 128 | 
            +
                    title: str,
         | 
| 129 | 
            +
                    model_name: Optional[str],
         | 
| 130 | 
            +
                    # Drone-specific fields
         | 
| 131 | 
            +
                    center_lon: Optional[float] = None,
         | 
| 132 | 
            +
                    center_lat: Optional[float] = None,
         | 
| 133 | 
            +
                    amsl_m: Optional[float] = None,
         | 
| 134 | 
            +
                    agl_m: Optional[float] = None,
         | 
| 135 | 
            +
                    heading_deg: Optional[float] = None,
         | 
| 136 | 
            +
                    yaw_deg: Optional[float] = None,
         | 
| 137 | 
            +
                    pitch_deg: Optional[float] = None,
         | 
| 138 | 
            +
                    roll_deg: Optional[float] = None,
         | 
| 139 | 
            +
                    rtk_fix: Optional[bool] = None,
         | 
| 140 | 
            +
                    std_h_m: Optional[float] = None,
         | 
| 141 | 
            +
                    std_v_m: Optional[float] = None,
         | 
| 142 | 
            +
                    db: Session = None
         | 
| 143 | 
            +
                ) -> Dict[str, Any]:
         | 
| 144 | 
            +
                    """Process multiple image uploads"""
         | 
| 145 | 
            +
                    logger.info(f"Processing multi upload: {len(files)} files")
         | 
| 146 | 
            +
                    
         | 
| 147 | 
            +
                    results = []
         | 
| 148 | 
            +
                    for file in files:
         | 
| 149 | 
            +
                        try:
         | 
| 150 | 
            +
                            result = await UploadService.process_single_upload(
         | 
| 151 | 
            +
                                file, source, event_type, countries, epsg, image_type,
         | 
| 152 | 
            +
                                title, model_name, center_lon, center_lat, amsl_m, agl_m,
         | 
| 153 | 
            +
                                heading_deg, yaw_deg, pitch_deg, roll_deg,
         | 
| 154 | 
            +
                                rtk_fix, std_h_m, std_v_m, db
         | 
| 155 | 
            +
                            )
         | 
| 156 | 
            +
                            results.append(result)
         | 
| 157 | 
            +
                        except Exception as e:
         | 
| 158 | 
            +
                            logger.error(f"Failed to process file {file.filename}: {str(e)}")
         | 
| 159 | 
            +
                            results.append({
         | 
| 160 | 
            +
                                'error': str(e),
         | 
| 161 | 
            +
                                'filename': file.filename
         | 
| 162 | 
            +
                            })
         | 
| 163 | 
            +
                    
         | 
| 164 | 
            +
                    logger.info(f"Multi upload completed: {len(results)} results")
         | 
| 165 | 
            +
                    return {
         | 
| 166 | 
            +
                        'results': results,
         | 
| 167 | 
            +
                        'total_files': len(files),
         | 
| 168 | 
            +
                        'successful': len([r for r in results if 'error' not in r])
         | 
| 169 | 
            +
                    }
         | 
| 170 | 
            +
                
         | 
| 171 | 
            +
                @staticmethod
         | 
| 172 | 
            +
                async def _preprocess_image(content: bytes, filename: str) -> Dict[str, Any]:
         | 
| 173 | 
            +
                    """Preprocess an image file"""
         | 
| 174 | 
            +
                    logger.debug(f"Preprocessing image: {filename}")
         | 
| 175 | 
            +
                    
         | 
| 176 | 
            +
                    try:
         | 
| 177 | 
            +
                        processed_content, processed_filename, mime_type = ImagePreprocessor.preprocess_image(
         | 
| 178 | 
            +
                            content, 
         | 
| 179 | 
            +
                            filename,
         | 
| 180 | 
            +
                            target_format='PNG',
         | 
| 181 | 
            +
                            quality=95
         | 
| 182 | 
            +
                        )
         | 
| 183 | 
            +
                        
         | 
| 184 | 
            +
                        preprocessing_info = {
         | 
| 185 | 
            +
                            "original_filename": filename,
         | 
| 186 | 
            +
                            "processed_filename": processed_filename,
         | 
| 187 | 
            +
                            "original_mime_type": ImagePreprocessor.detect_mime_type(content, filename),
         | 
| 188 | 
            +
                            "processed_mime_type": mime_type,
         | 
| 189 | 
            +
                            "processed_content": processed_content,
         | 
| 190 | 
            +
                            "was_preprocessed": processed_filename != filename
         | 
| 191 | 
            +
                        }
         | 
| 192 | 
            +
                        
         | 
| 193 | 
            +
                        if processed_filename != filename:
         | 
| 194 | 
            +
                            logger.info(f"Image preprocessed: {filename} -> {processed_filename}")
         | 
| 195 | 
            +
                        
         | 
| 196 | 
            +
                        return preprocessing_info
         | 
| 197 | 
            +
                        
         | 
| 198 | 
            +
                    except Exception as e:
         | 
| 199 | 
            +
                        logger.error(f"Image preprocessing failed: {str(e)}")
         | 
| 200 | 
            +
                        # Fall back to original content
         | 
| 201 | 
            +
                        return {
         | 
| 202 | 
            +
                            "original_filename": filename,
         | 
| 203 | 
            +
                            "processed_filename": filename,
         | 
| 204 | 
            +
                            "original_mime_type": "unknown",
         | 
| 205 | 
            +
                            "processed_mime_type": "image/png",
         | 
| 206 | 
            +
                            "processed_content": content,
         | 
| 207 | 
            +
                            "was_preprocessed": False,
         | 
| 208 | 
            +
                            "error": str(e)
         | 
| 209 | 
            +
                        }
         | 
| 210 | 
            +
                
         | 
| 211 | 
            +
                @staticmethod
         | 
| 212 | 
            +
                async def _upload_to_storage(content: bytes, filename: str, mime_type: str) -> Tuple[str, str]:
         | 
| 213 | 
            +
                    """Upload content to storage and return key and SHA256"""
         | 
| 214 | 
            +
                    logger.debug(f"Uploading to storage: {filename}")
         | 
| 215 | 
            +
                    
         | 
| 216 | 
            +
                    key = storage.upload_fileobj(
         | 
| 217 | 
            +
                        io.BytesIO(content), 
         | 
| 218 | 
            +
                        filename, 
         | 
| 219 | 
            +
                        content_type=mime_type
         | 
| 220 | 
            +
                    )
         | 
| 221 | 
            +
                    
         | 
| 222 | 
            +
                    sha = crud.hash_bytes(content)
         | 
| 223 | 
            +
                    logger.debug(f"Uploaded to key: {key}, SHA: {sha}")
         | 
| 224 | 
            +
                    
         | 
| 225 | 
            +
                    return key, sha
         | 
| 226 | 
            +
                
         | 
| 227 | 
            +
                @staticmethod
         | 
| 228 | 
            +
                async def _generate_image_versions(content: bytes, filename: str) -> Tuple[Optional[Tuple], Optional[Tuple]]:
         | 
| 229 | 
            +
                    """Generate thumbnail and detail versions of the image"""
         | 
| 230 | 
            +
                    logger.debug(f"Generating image versions: {filename}")
         | 
| 231 | 
            +
                    
         | 
| 232 | 
            +
                    try:
         | 
| 233 | 
            +
                        thumbnail_result = ImageProcessingService.create_and_upload_thumbnail(
         | 
| 234 | 
            +
                            content, filename
         | 
| 235 | 
            +
                        )
         | 
| 236 | 
            +
                        
         | 
| 237 | 
            +
                        detail_result = ImageProcessingService.create_and_upload_detail(
         | 
| 238 | 
            +
                            content, filename
         | 
| 239 | 
            +
                        )
         | 
| 240 | 
            +
                        
         | 
| 241 | 
            +
                        if thumbnail_result:
         | 
| 242 | 
            +
                            logger.info(f"Thumbnail generated: {thumbnail_result[0]}")
         | 
| 243 | 
            +
                        if detail_result:
         | 
| 244 | 
            +
                            logger.info(f"Detail version generated: {detail_result[0]}")
         | 
| 245 | 
            +
                        
         | 
| 246 | 
            +
                        return thumbnail_result, detail_result
         | 
| 247 | 
            +
                        
         | 
| 248 | 
            +
                    except Exception as e:
         | 
| 249 | 
            +
                        logger.error(f"Image version generation failed: {str(e)}")
         | 
| 250 | 
            +
                        return None, None
         | 
| 251 | 
            +
                
         | 
| 252 | 
            +
                @staticmethod
         | 
| 253 | 
            +
                async def _generate_caption(img, image_content: bytes, title: str, model_name: Optional[str], db: Session):
         | 
| 254 | 
            +
                    """Generate caption for the uploaded image"""
         | 
| 255 | 
            +
                    if not title and not model_name:
         | 
| 256 | 
            +
                        return
         | 
| 257 | 
            +
                    
         | 
| 258 | 
            +
                    logger.debug(f"Generating caption for image {img.image_id}")
         | 
| 259 | 
            +
                    
         | 
| 260 | 
            +
                    try:
         | 
| 261 | 
            +
                        # Get active prompt for image type
         | 
| 262 | 
            +
                        prompt_obj = crud.get_active_prompt_by_image_type(db, img.image_type)
         | 
| 263 | 
            +
                        if not prompt_obj:
         | 
| 264 | 
            +
                            logger.warning(f"No active prompt found for image type: {img.image_type}")
         | 
| 265 | 
            +
                            return
         | 
| 266 | 
            +
                        
         | 
| 267 | 
            +
                        # Generate caption using VLM service
         | 
| 268 | 
            +
                        result = await vlm_manager.generate_caption(
         | 
| 269 | 
            +
                            image_content=image_content,
         | 
| 270 | 
            +
                            prompt=prompt_obj.label,
         | 
| 271 | 
            +
                            metadata_instructions=prompt_obj.metadata_instructions or "",
         | 
| 272 | 
            +
                            model_name=model_name,
         | 
| 273 | 
            +
                            db_session=db
         | 
| 274 | 
            +
                        )
         | 
| 275 | 
            +
                        
         | 
| 276 | 
            +
                        # Create caption record
         | 
| 277 | 
            +
                        crud.create_caption(
         | 
| 278 | 
            +
                            db=db,
         | 
| 279 | 
            +
                            image_id=img.image_id,
         | 
| 280 | 
            +
                            title=title or result.get("title", ""),
         | 
| 281 | 
            +
                            prompt=prompt_obj.p_code,
         | 
| 282 | 
            +
                            model_code=result.get("model", model_name or "STUB_MODEL"),
         | 
| 283 | 
            +
                            raw_json=result.get("raw_response", {}),
         | 
| 284 | 
            +
                            text=result.get("text", ""),
         | 
| 285 | 
            +
                            metadata=result.get("metadata", {}),
         | 
| 286 | 
            +
                            image_count=1
         | 
| 287 | 
            +
                        )
         | 
| 288 | 
            +
                        
         | 
| 289 | 
            +
                        logger.info(f"Caption generated for image {img.image_id}")
         | 
| 290 | 
            +
                        
         | 
| 291 | 
            +
                    except Exception as e:
         | 
| 292 | 
            +
                        logger.error(f"Caption generation failed: {str(e)}")
         | 
| 293 | 
            +
                        # Continue without caption if generation fails
         | 
    	
        py_backend/app/utils/__init__.py
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            # Utils package
         | 
    	
        py_backend/app/utils/image_utils.py
    ADDED
    
    | @@ -0,0 +1,109 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Shared utilities for image operations
         | 
| 3 | 
            +
            """
         | 
| 4 | 
            +
            import logging
         | 
| 5 | 
            +
            from typing import Dict, Any
         | 
| 6 | 
            +
            from sqlalchemy.orm import Session
         | 
| 7 | 
            +
            from .. import crud, storage
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            def convert_image_to_dict(img, image_url: str) -> Dict[str, Any]:
         | 
| 12 | 
            +
                """Helper function to convert SQLAlchemy image model to dict for Pydantic"""
         | 
| 13 | 
            +
                countries_list = []
         | 
| 14 | 
            +
                if hasattr(img, 'countries') and img.countries is not None:
         | 
| 15 | 
            +
                    try:
         | 
| 16 | 
            +
                        countries_list = [{"c_code": c.c_code, "label": c.label, "r_code": c.r_code} for c in img.countries]
         | 
| 17 | 
            +
                    except Exception as e:
         | 
| 18 | 
            +
                        logger.warning(f"Error processing countries for image {img.image_id}: {e}")
         | 
| 19 | 
            +
                        countries_list = []
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                captions_list = []
         | 
| 22 | 
            +
                if hasattr(img, 'captions') and img.captions is not None:
         | 
| 23 | 
            +
                    try:
         | 
| 24 | 
            +
                        captions_list = [
         | 
| 25 | 
            +
                            {
         | 
| 26 | 
            +
                                "caption_id": c.caption_id,
         | 
| 27 | 
            +
                                "title": c.title,
         | 
| 28 | 
            +
                                "text": c.text,
         | 
| 29 | 
            +
                                "starred": c.starred,
         | 
| 30 | 
            +
                                "generated": c.generated,
         | 
| 31 | 
            +
                                "model": c.model,
         | 
| 32 | 
            +
                                "created_at": c.created_at,
         | 
| 33 | 
            +
                                "updated_at": c.updated_at
         | 
| 34 | 
            +
                            } for c in img.captions
         | 
| 35 | 
            +
                        ]
         | 
| 36 | 
            +
                    except Exception as e:
         | 
| 37 | 
            +
                        logger.warning(f"Error processing captions for image {img.image_id}: {e}")
         | 
| 38 | 
            +
                        captions_list = []
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                # Get starred status and other caption fields from first caption for backward compatibility
         | 
| 41 | 
            +
                starred = False
         | 
| 42 | 
            +
                title = None
         | 
| 43 | 
            +
                text = None
         | 
| 44 | 
            +
                generated = False
         | 
| 45 | 
            +
                model = None
         | 
| 46 | 
            +
                created_at = None
         | 
| 47 | 
            +
                updated_at = None
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                if captions_list:
         | 
| 50 | 
            +
                    first_caption = captions_list[0]
         | 
| 51 | 
            +
                    starred = first_caption.get("starred", False)
         | 
| 52 | 
            +
                    title = first_caption.get("title", "")
         | 
| 53 | 
            +
                    text = first_caption.get("text", "")
         | 
| 54 | 
            +
                    generated = first_caption.get("generated", False)
         | 
| 55 | 
            +
                    model = first_caption.get("model", "")
         | 
| 56 | 
            +
                    created_at = first_caption.get("created_at")
         | 
| 57 | 
            +
                    updated_at = first_caption.get("updated_at")
         | 
| 58 | 
            +
                
         | 
| 59 | 
            +
                # Generate URLs for thumbnail and detail versions
         | 
| 60 | 
            +
                thumbnail_url = None
         | 
| 61 | 
            +
                detail_url = None
         | 
| 62 | 
            +
                
         | 
| 63 | 
            +
                if hasattr(img, 'thumbnail_key') and img.thumbnail_key:
         | 
| 64 | 
            +
                    try:
         | 
| 65 | 
            +
                        thumbnail_url = storage.get_object_url(img.thumbnail_key)
         | 
| 66 | 
            +
                    except Exception as e:
         | 
| 67 | 
            +
                        logger.warning(f"Error generating thumbnail URL for image {img.image_id}: {e}")
         | 
| 68 | 
            +
                
         | 
| 69 | 
            +
                if hasattr(img, 'detail_key') and img.detail_key:
         | 
| 70 | 
            +
                    try:
         | 
| 71 | 
            +
                        detail_url = storage.get_object_url(img.detail_key)
         | 
| 72 | 
            +
                    except Exception as e:
         | 
| 73 | 
            +
                        logger.warning(f"Error generating detail URL for image {img.image_id}: {e}")
         | 
| 74 | 
            +
                
         | 
| 75 | 
            +
                img_dict = {
         | 
| 76 | 
            +
                    "image_id": img.image_id,
         | 
| 77 | 
            +
                    "file_key": img.file_key,
         | 
| 78 | 
            +
                    "sha256": img.sha256,
         | 
| 79 | 
            +
                    "source": img.source,
         | 
| 80 | 
            +
                    "event_type": img.event_type,
         | 
| 81 | 
            +
                    "epsg": img.epsg,
         | 
| 82 | 
            +
                    "image_type": img.image_type,
         | 
| 83 | 
            +
                    "countries": countries_list,
         | 
| 84 | 
            +
                    "captions": captions_list,
         | 
| 85 | 
            +
                    "starred": starred,
         | 
| 86 | 
            +
                    "title": title,
         | 
| 87 | 
            +
                    "text": text,
         | 
| 88 | 
            +
                    "generated": generated,
         | 
| 89 | 
            +
                    "model": model,
         | 
| 90 | 
            +
                    "created_at": created_at,
         | 
| 91 | 
            +
                    "updated_at": updated_at,
         | 
| 92 | 
            +
                    "url": image_url,
         | 
| 93 | 
            +
                    "thumbnail_url": thumbnail_url,
         | 
| 94 | 
            +
                    "detail_url": detail_url,
         | 
| 95 | 
            +
                    # Drone-specific fields
         | 
| 96 | 
            +
                    "center_lon": getattr(img, 'center_lon', None),
         | 
| 97 | 
            +
                    "center_lat": getattr(img, 'center_lat', None),
         | 
| 98 | 
            +
                    "amsl_m": getattr(img, 'amsl_m', None),
         | 
| 99 | 
            +
                    "agl_m": getattr(img, 'agl_m', None),
         | 
| 100 | 
            +
                    "heading_deg": getattr(img, 'heading_deg', None),
         | 
| 101 | 
            +
                    "yaw_deg": getattr(img, 'yaw_deg', None),
         | 
| 102 | 
            +
                    "pitch_deg": getattr(img, 'pitch_deg', None),
         | 
| 103 | 
            +
                    "roll_deg": getattr(img, 'roll_deg', None),
         | 
| 104 | 
            +
                    "rtk_fix": getattr(img, 'rtk_fix', None),
         | 
| 105 | 
            +
                    "std_h_m": getattr(img, 'std_h_m', None),
         | 
| 106 | 
            +
                    "std_v_m": getattr(img, 'std_v_m', None),
         | 
| 107 | 
            +
                }
         | 
| 108 | 
            +
                
         | 
| 109 | 
            +
                return img_dict
         | 
    	
        py_backend/requirements.txt
    CHANGED
    
    | @@ -1,32 +1,47 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
|  | |
|  | |
|  | |
| 4 | 
             
            sqlalchemy>=2.0.0
         | 
| 5 | 
             
            alembic>=1.12.0
         | 
| 6 | 
            -
            psycopg[binary,pool]
         | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
|  | |
|  | |
|  | |
| 17 | 
             
            Pillow>=10.4.0
         | 
| 18 | 
            -
            huggingface-hub
         | 
| 19 | 
            -
            pytest
         | 
| 20 | 
            -
            pytest-asyncio
         | 
| 21 | 
            -
            httpx
         | 
| 22 | 
            -
            requests
         | 
| 23 | 
            -
            pycountry>=22.3.5
         | 
| 24 | 
            -
            pycountry-convert>=0.7.2
         | 
| 25 | 
            -
            python-multipart
         | 
| 26 | 
            -
            PyJWT
         | 
| 27 | 
            -
            jsonschema
         | 
| 28 | 
            -
            aiohttp>=3.9
         | 
| 29 | 
             
            PyMuPDF>=1.23.0
         | 
| 30 | 
             
            Pillow-Heif>=1.0.0
         | 
| 31 | 
             
            imageio>=2.31.0
         | 
| 32 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            # Web Framework & Server
         | 
| 2 | 
            +
            fastapi>=0.104.0
         | 
| 3 | 
            +
            uvicorn[standard]>=0.24.0
         | 
| 4 | 
            +
            python-multipart>=0.0.6
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # Database & ORM
         | 
| 7 | 
             
            sqlalchemy>=2.0.0
         | 
| 8 | 
             
            alembic>=1.12.0
         | 
| 9 | 
            +
            psycopg[binary,pool]>=3.1.0
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            # HTTP Clients
         | 
| 12 | 
            +
            aiohttp>=3.9.0
         | 
| 13 | 
            +
            httpx>=0.25.0
         | 
| 14 | 
            +
            requests>=2.31.0
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            # AI/ML Services
         | 
| 17 | 
            +
            openai>=1.3.0
         | 
| 18 | 
            +
            anthropic>=0.7.0
         | 
| 19 | 
            +
            google-generativeai>=0.3.0
         | 
| 20 | 
            +
            huggingface-hub>=0.19.0
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            # Image Processing
         | 
| 23 | 
             
            Pillow>=10.4.0
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 24 | 
             
            PyMuPDF>=1.23.0
         | 
| 25 | 
             
            Pillow-Heif>=1.0.0
         | 
| 26 | 
             
            imageio>=2.31.0
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            # Data Processing & Validation
         | 
| 29 | 
            +
            pydantic>=2.7.0
         | 
| 30 | 
            +
            pydantic-settings>=0.2.0
         | 
| 31 | 
            +
            jsonschema>=4.19.0
         | 
| 32 | 
            +
            orjson>=3.9.0
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            # Cloud Storage
         | 
| 35 | 
            +
            boto3>=1.34.0
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            # Utilities
         | 
| 38 | 
            +
            python-dotenv>=1.0.0
         | 
| 39 | 
            +
            chardet>=5.2.0
         | 
| 40 | 
            +
            pycountry>=22.3.5
         | 
| 41 | 
            +
            pycountry-convert>=0.7.2
         | 
| 42 | 
            +
            PyJWT>=2.8.0
         | 
| 43 | 
            +
            psutil>=5.9.0
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            # Development & Testing
         | 
| 46 | 
            +
            pytest>=7.4.0
         | 
| 47 | 
            +
            pytest-asyncio>=0.21.0
         | 
    	
        py_backend/static/assets/{AdminPage-0qyl8wHV.js β AdminPage-DLWomD88.js}
    RENAMED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            import{r as t,j as a,N as K,H as Je,O as m,z as j,I as re,_ as Ge,n as i,J as b}from"./index- | 
| 2 | 
             
            Model "${o.label}" added successfully!
         | 
| 3 |  | 
| 4 | 
             
            β οΈ  IMPORTANT: Model will NOT work until you complete these steps:
         | 
|  | |
| 1 | 
            +
            import{r as t,j as a,N as K,H as Je,O as m,z as j,I as re,_ as Ge,n as i,J as b}from"./index-w0OOMPwN.js";import{u as Ve}from"./useAdmin-avReXN15.js";const We="_adminContainer_j11pf_5",qe="_adminHeader_j11pf_13",Ke="_adminSection_j11pf_20",Ye="_modelSelectionArea_j11pf_29",Qe="_modelSelectionRow_j11pf_36",Xe="_modelsTable_j11pf_89",Ze="_promptSubsection_j11pf_97",ea="_promptSubsectionTitle_j11pf_109",aa="_modelCode_j11pf_152",la="_modelId_j11pf_157",sa="_modelActions_j11pf_163",oa="_addModelButtonContainer_j11pf_169",ta="_addModelForm_j11pf_177",ia="_addModelFormTitle_j11pf_185",da="_addModelFormGrid_j11pf_193",na="_addModelFormField_j11pf_206",ra="_addModelFormCheckbox_j11pf_250",ca="_addModelFormActions_j11pf_268",ma="_modalOverlay_j11pf_277",ha="_modalContent_j11pf_291",pa="_modalBody_j11pf_302",xa="_modalTitle_j11pf_312",ua="_modalText_j11pf_320",ja="_modalTextLeft_j11pf_332",_a="_modalButtons_j11pf_355",va="_modalForm_j11pf_363",ga="_formField_j11pf_372",fa="_formLabel_j11pf_376",ba="_formInput_j11pf_385",ya="_textarea_j11pf_407",l={adminContainer:We,adminHeader:qe,adminSection:Ke,modelSelectionArea:Ye,modelSelectionRow:Qe,modelsTable:Xe,promptSubsection:Ze,promptSubsectionTitle:ea,modelCode:aa,modelId:la,modelActions:sa,addModelButtonContainer:oa,addModelForm:ta,addModelFormTitle:ia,addModelFormGrid:da,addModelFormField:na,addModelFormCheckbox:ra,addModelFormActions:ca,modalOverlay:ma,modalContent:ha,modalBody:pa,modalTitle:xa,modalText:ua,modalTextLeft:ja,modalButtons:_a,modalForm:va,formField:ga,formLabel:fa,formInput:ba,textarea:ya},A="selectedVlmModel";function Sa(){const{isAuthenticated:_,isLoading:ce,login:me,logout:he,verifyToken:Y}=Ve(),[$,Q]=t.useState(""),[X,v]=t.useState(""),[Z,ee]=t.useState(!1),[I,y]=t.useState([]),[pe,N]=t.useState(""),[xe,C]=t.useState(""),[ae,P]=t.useState([]),[ue,E]=t.useState([]),[je,L]=t.useState([]),[_e,S]=t.useState(!1),[B,O]=t.useState(null),[D,g]=t.useState({schema_id:"",title:"",version:"",schema:{}}),[ve,k]=t.useState(!1),[ge,w]=t.useState(!1),[fe,U]=t.useState(null),[M,z]=t.useState(null),[n,r]=t.useState({p_code:"",label:"",metadata_instructions:"",image_type:"crisis_map",is_active:!1}),[le,R]=t.useState(!1),[be,H]=t.useState(!1),[J,G]=t.useState(null),[o,c]=t.useState({m_code:"",label:"",model_type:"custom",provider:"huggingface",model_id:"",is_available:!1,is_fallback:!1}),[ye,T]=t.useState(!1),[Ne,V]=t.useState(!1),[Ce,p]=t.useState(!1),[se,oe]=t.useState(""),[Se,ke]=t.useState(""),[we,x]=t.useState(""),[Me,F]=t.useState(""),u=t.useCallback(()=>{fetch("/api/models").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}).then(e=>{if(console.log("Models data received:",e),e&&Array.isArray(e.models)){y(e.models);const s=localStorage.getItem(A);if(e.models.length>0)if(s==="random")N("random");else if(s&&e.models.find(d=>d.m_code===s&&d.is_available))N(s);else{const d=e.models.find(h=>h.is_available)||e.models[0];N(d.m_code),localStorage.setItem(A,d.m_code)}}else console.error("Expected models object but got:",e),y([])}).catch(e=>{console.error("Error fetching models:",e),y([])}),fetch("/api/admin/fallback-model",{headers:{Authorization:`Bearer ${localStorage.getItem("adminToken")}`}}).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}).then(e=>{console.log("Fallback model data received:",e),e.fallback_model?C(e.fallback_model.m_code):C("")}).catch(e=>{console.error("Error fetching fallback model:",e),C("")})},[]),f=t.useCallback(()=>{console.log("=== fetchPrompts called ==="),fetch("/api/prompts").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}).then(e=>{console.log("Prompts data received:",e),Array.isArray(e)?P(e):(console.error("Expected array but got:",e),P([])),console.log("State update triggered with:",e||[])}).catch(e=>{console.error("Error fetching prompts:",e),P([])})},[]),te=t.useCallback(()=>{fetch("/api/image-types").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}).then(e=>{console.log("Image types data received:",e),Array.isArray(e)?E(e):(console.error("Expected array but got:",e),E([]))}).catch(e=>{console.error("Error fetching image types:",e),E([])})},[]),W=t.useCallback(()=>{console.log("=== fetchSchemas called ==="),fetch("/api/schemas",{headers:{Authorization:`Bearer ${localStorage.getItem("adminToken")}`}}).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}).then(e=>{console.log("Schemas data received:",e),Array.isArray(e)?L(e):(console.error("Expected array but got:",e),L([]))}).catch(e=>{console.error("Error fetching schemas:",e),L([])})},[]);t.useEffect(()=>{_&&(u(),f(),te(),W())},[_,u,f,te,W]),t.useEffect(()=>{_&&Y()},[_,Y]);const ie=e=>{z(e),r({p_code:e.p_code,label:e.label||"",metadata_instructions:e.metadata_instructions||"",image_type:e.image_type||"crisis_map",is_active:e.is_active||!1}),k(!0)},Te=async()=>{try{if(!M){alert("No prompt selected for editing");return}const e=await fetch(`/api/prompts/${M.p_code}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({label:n.label,metadata_instructions:n.metadata_instructions,image_type:n.image_type,is_active:n.is_active})});if(e.ok)f(),k(!1),z(null),r({p_code:"",label:"",metadata_instructions:"",image_type:"crisis_map",is_active:!1});else{const s=await e.json();alert(`Failed to update prompt: ${s.error||"Unknown error"}`)}}catch{alert("Error updating prompt")}},de=async(e,s)=>{try{const d=await fetch(`/api/prompts/${e}/toggle-active?image_type=${s}`,{method:"POST",headers:{"Content-Type":"application/json"}});if(d.ok)f();else{const h=await d.json();alert(`Failed to toggle prompt active status: ${h.detail||"Unknown error"}`)}}catch{alert("Error toggling prompt active status")}},ne=e=>{U(e),r({p_code:"",label:"",metadata_instructions:"",image_type:e,is_active:!1}),w(!0)},Fe=async()=>{try{const e=await fetch("/api/prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(e.ok)f(),w(!1),U(null),r({p_code:"",label:"",metadata_instructions:"",image_type:"crisis_map",is_active:!1});else{const s=await e.json();alert(`Failed to create prompt: ${s.detail||"Unknown error"}`)}}catch{alert("Error creating prompt")}},Ae=e=>{O(e),g({schema_id:e.schema_id,title:e.title,version:e.version,schema:e.schema}),S(!0)},$e=async()=>{try{if(!B){alert("No schema selected for editing");return}const e=await fetch(`/api/schemas/${B.schema_id}`,{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${localStorage.getItem("adminToken")}`},body:JSON.stringify(D)});if(e.ok)W(),S(!1),O(null),g({schema_id:"",title:"",version:"",schema:{}});else{const s=await e.json();alert(`Failed to save schema: ${s.detail||"Unknown error"}`)}}catch(e){console.error("Error saving schema:",e),alert("Error saving schema")}},Ie=()=>{S(!1),O(null),g({schema_id:"",title:"",version:"",schema:{}})},Pe=async(e,s)=>{try{const d=await fetch(`/api/models/${e}/toggle`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({is_available:!s})});if(d.ok)y(h=>(h||[]).map(q=>q.m_code===e?{...q,is_available:!s}:q));else{const h=await d.json();alert(`Failed to toggle model availability: ${h.error||"Unknown error"}`)}}catch{alert("Error toggling model availability")}},Ee=e=>{N(e),e==="random"?localStorage.setItem(A,"random"):localStorage.setItem(A,e)},Le=async e=>{try{const s=await fetch(`/api/admin/models/${e}`,{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${localStorage.getItem("adminToken")}`},body:JSON.stringify({is_fallback:!0})});if(s.ok)C(e),u();else{const d=await s.json();alert(`Failed to set fallback model: ${d.detail||"Unknown error"}`)}}catch{alert("Error setting fallback model")}},Be=async()=>{try{const e=await fetch("/api/admin/models",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${localStorage.getItem("adminToken")}`},body:JSON.stringify(o)});if(e.ok){const s=`
         | 
| 2 | 
             
            Model "${o.label}" added successfully!
         | 
| 3 |  | 
| 4 | 
             
            β οΈ  IMPORTANT: Model will NOT work until you complete these steps:
         | 
    	
        py_backend/static/assets/{ExportModal-BI_JlHOZ.js β ExportModal-C5df6JIW.js}
    RENAMED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            import{r as n,j as e,P,S as W,o as F,T as V,D as A,z as g,n as b,O as H,J as M,V as q,_ as O,L as D}from"./index-B0L3qvtx.js";const G=({title:c,titleId:i,...r})=>n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":i},r),c?n.createElement("title",{id:i},c):null,n.createElement("g",{clipPath:"url(#checkbox-indeterminate-line_svg__a)"},n.createElement("path",{d:"M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Zm1 2v14h14V5H5Zm2 6h10v2H7v-2Z"})),n.createElement("defs",null,n.createElement("clipPath",{id:"checkbox-indeterminate-line_svg__a"},n.createElement("path",{d:"M0 0h24v24H0z"})))),$=({title:c,titleId:i,...r})=>n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":i},r),c?n.createElement("title",{id:i},c):null,n.createElement("g",{clipPath:"url(#filter-line_svg__a)"},n.createElement("path",{d:"M9 13.5 4 6H3V4h18v2h-1l-5 7.5V22H9v-8.5ZM6.404 6 11 12.894V20h2v-7.106L17.596 6H6.404Z"})),n.createElement("defs",null,n.createElement("clipPath",{id:"filter-line_svg__a"},n.createElement("path",{d:"M0 0h24v24H0z"}))));function R(c){const{className:i,indeterminate:r,value:v}=c;return e.jsxs(e.Fragment,{children:[r&&e.jsx(G,{className:i}),v&&!r&&e.jsx(P,{className:i}),!v&&!r&&e.jsx(W,{className:i})]})}const U="_checkbox_12g7n_1",Z="_with-background_12g7n_7",Y="_checkmark-container_12g7n_12",J="_input_12g7n_18",K="_content_12g7n_33",Q="_description_12g7n_40",X="_checked_12g7n_45",L="_checkmark_12g7n_12",ee="_disabled-checkbox_12g7n_58",m={checkbox:U,withBackground:Z,checkmarkContainer:Y,input:J,content:K,description:Q,checked:X,checkmark:L,disabledCheckbox:ee};function B(c){const{className:i,checkmark:r=R,checkmarkClassName:v,checkmarkContainerClassName:T,disabled:s,error:h,indeterminate:y,inputClassName:w,invertedLogic:d=!1,label:j,labelContainerClassName:_,name:I,onChange:f,readOnly:x,tooltip:k,value:C,description:u,withBackground:E,...S}=c,z=n.useCallback(t=>{const o=t.currentTarget.checked;f(d?!o:o,I)},[I,f,d]),N=d?!C:C,p=F(m.checkbox,i,!y&&N&&m.checked,E&&m.withBackground,s&&m.disabledCheckbox,x&&m.readOnly);return e.jsxs("label",{className:p,title:k,children:[e.jsxs("div",{className:F(m.checkmarkContainer,T),children:[e.jsx("input",{onChange:z,className:F(m.input,w),type:"checkbox",checked:N??!1,disabled:s||x,readOnly:x,...S}),e.jsx(r,{className:F(m.checkmark,v),value:N??!1,indeterminate:y,"aria-hidden":"true"})]}),(j||u)&&e.jsxs("div",{className:m.content,children:[j&&e.jsx("div",{className:_,children:j}),u&&e.jsx("div",{className:m.description,children:u})]}),h&&e.jsx(V,{children:h})]})}function _e({sources:c,types:i,regions:r,countries:v,imageTypes:T,isLoadingFilters:s=!1}){const[h,y]=n.useState(!1),[w,d]=n.useState(""),{search:j,setSearch:_,srcFilter:I,setSrcFilter:f,catFilter:x,setCatFilter:k,regionFilter:C,setRegionFilter:u,countryFilter:E,setCountryFilter:S,imageTypeFilter:z,setImageTypeFilter:N,uploadTypeFilter:p,setUploadTypeFilter:t,clearAllFilters:o}=A();return n.useEffect(()=>{d(j)},[j]),e.jsxs("div",{className:"mb-6 space-y-4",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-4",children:[e.jsx(g,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:e.jsx(b,{name:"toggle-filters",variant:"secondary",onClick:()=>y(!h),className:"whitespace-nowrap",title:h?"Hide Filters":"Show Filters",children:e.jsx($,{className:"w-4 h-4"})})}),e.jsx(g,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]",children:e.jsx(H,{name:"search",placeholder:"Search",value:w,onChange:a=>d(a||""),onKeyDown:a=>{a.key==="Enter"&&_(w)}})}),e.jsx(g,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:e.jsx(b,{name:"clear-filters",variant:"secondary",onClick:o,children:"Clear Filters"})})]}),h&&e.jsx("div",{className:"bg-white/20 backdrop-blur-sm rounded-md p-4",children:e.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-3 gap-4",children:[e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"source",placeholder:s?"Loading...":"All Sources",options:c,value:I||null,onChange:a=>f(a||""),keySelector:a=>a.s_code,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"category",placeholder:s?"Loading...":"All Categories",options:i,value:x||null,onChange:a=>k(a||""),keySelector:a=>a.t_code,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"region",placeholder:s?"Loading...":"All Regions",options:r,value:C||null,onChange:a=>u(a||""),keySelector:a=>a.r_code,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(q,{name:"country",placeholder:s?"Loading...":"All Countries",options:v,value:E?[E]:[],onChange:a=>S(a[0]||""),keySelector:a=>a.c_code,labelSelector:a=>a.label,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"imageType",placeholder:s?"Loading...":"All Image Types",options:T,value:z||null,onChange:a=>N(a||""),keySelector:a=>a.image_type,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"uploadType",placeholder:"All Upload Types",options:[{key:"single",label:"Single Upload"},{key:"multiple",label:"Multiple Upload"}],value:p||null,onChange:a=>t(a||""),keySelector:a=>a.key,labelSelector:a=>a.label,required:!1,disabled:!1})})]})})]})}const ae="_fullSizeModalOverlay_cyz3b_1",le="_fullSizeModalContent_cyz3b_29",te="_ratingWarningContent_cyz3b_53",se="_ratingWarningTitle_cyz3b_65",ne="_exportModeSection_cyz3b_133",ie="_splitConfigSection_cyz3b_143",re="_splitConfigTitle_cyz3b_153",ce="_splitInputsContainer_cyz3b_167",oe="_splitInputGroup_cyz3b_183",de="_splitInputLabel_cyz3b_197",pe="_splitInput_cyz3b_167",me="_splitTotal_cyz3b_247",he="_splitTotalError_cyz3b_261",xe="_checkboxesContainer_cyz3b_271",ue="_ratingWarningButtons_cyz3b_289",ge="_singleExportMessage_cyz3b_309",be="_navigateButtonContainer_cyz3b_333",ve="_loadingOverlay_cyz3b_349",l={fullSizeModalOverlay:ae,fullSizeModalContent:le,ratingWarningContent:te,ratingWarningTitle:se,exportModeSection:ne,splitConfigSection:ie,splitConfigTitle:re,splitInputsContainer:ce,splitInputGroup:oe,splitInputLabel:de,splitInput:pe,splitTotal:me,splitTotalError:he,checkboxesContainer:xe,ratingWarningButtons:ue,singleExportMessage:ge,navigateButtonContainer:be,loadingOverlay:ve};function fe({isOpen:c,onClose:i,onExport:r,crisisMapsCount:v,droneImagesCount:T,isLoading:s=!1,exportSuccess:h=!1,variant:y="bulk",onNavigateAndExport:w}){const[d,j]=n.useState("standard"),[_,I]=n.useState(80),[f,x]=n.useState(10),[k,C]=n.useState(10),[u,E]=n.useState(!0),[S,z]=n.useState(!0),N=()=>{if(y==="single"){r(d,["crisis_map","drone_image"]);return}if(!u&&!S){alert("Please select at least one image type to export.");return}const t=[];u&&t.push("crisis_map"),S&&t.push("drone_image"),r(d,t)},p=()=>{i()};return c?y==="single"?e.jsx("div",{className:l.fullSizeModalOverlay,onClick:p,children:e.jsxs("div",{className:l.fullSizeModalContent,onClick:t=>t.stopPropagation(),children:[s&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx(O,{className:"text-ifrcRed"}),e.jsx("div",{className:"text-lg font-medium",children:"Exporting..."}),e.jsx("div",{className:"text-sm text-gray-600",children:"This might take a few seconds"})]})}),h&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx("div",{className:"text-lg font-medium",children:"Export Successful!"}),e.jsx("div",{className:"text-sm text-gray-600",children:"Your dataset has been downloaded"}),e.jsx(b,{name:"close-export-success",onClick:p,className:"mt-4",children:"Close"})]})}),e.jsxs("div",{className:l.ratingWarningContent,children:[e.jsx("h3",{className:l.ratingWarningTitle,children:"Export Single Item"}),e.jsxs("div",{className:l.singleExportMessage,children:[e.jsx("p",{children:"This only exports the 1 item currently on display."}),e.jsx("p",{children:'You may export the entire dataset from the "list view" here:'})]}),e.jsx("div",{className:l.navigateButtonContainer,children:e.jsx(b,{name:"navigate-to-list",variant:"secondary",onClick:w,children:"Navigate to List View"})}),e.jsxs("div",{className:l.ratingWarningButtons,children:[e.jsx(b,{name:"continue-export",onClick:N,disabled:s,children:s?e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(O,{className:"text-white"}),"Exporting..."]}):"Continue"}),e.jsx(b,{name:"cancel-export",variant:"tertiary",onClick:p,disabled:s,children:"Cancel"})]})]})]})}):e.jsx("div",{className:l.fullSizeModalOverlay,onClick:p,children:e.jsxs("div",{className:l.fullSizeModalContent,onClick:t=>t.stopPropagation(),children:[s&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx(O,{className:"text-ifrcRed"}),e.jsx("div",{className:"text-lg font-medium",children:"Exporting..."}),e.jsx("div",{className:"text-sm text-gray-600",children:"This might take a few seconds"})]})}),h&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx("div",{className:"text-lg font-medium",children:"Export Successful!"}),e.jsx("div",{className:"text-sm text-gray-600",children:"Your dataset has been downloaded"}),e.jsx(b,{name:"close-export-success",onClick:p,className:"mt-4",children:"Close"})]})}),e.jsxs("div",{className:l.ratingWarningContent,children:[e.jsx("h3",{className:l.ratingWarningTitle,children:"Export Dataset"}),e.jsx("div",{className:l.exportModeSection,children:e.jsx(D,{name:"export-mode",value:d,onChange:t=>{(t==="standard"||t==="fine-tuning")&&j(t)},options:[{key:"standard",label:"Standard"},{key:"fine-tuning",label:"Fine-tuning"}],keySelector:t=>t.key,labelSelector:t=>t.label,disabled:s})}),d==="fine-tuning"&&e.jsxs("div",{className:l.splitConfigSection,children:[e.jsx("div",{className:l.splitConfigTitle,children:"Dataset Split Configuration"}),e.jsxs("div",{className:l.splitInputsContainer,children:[e.jsxs("div",{className:l.splitInputGroup,children:[e.jsx("label",{htmlFor:"train-split",className:l.splitInputLabel,children:"Train (%)"}),e.jsx("input",{id:"train-split",type:"number",min:"0",max:"100",value:_,onChange:t=>{const o=parseInt(t.target.value)||0,a=100-o;a>=0&&(I(o),f+k>a&&(x(Math.floor(a/2)),C(a-Math.floor(a/2))))},className:l.splitInput,disabled:s})]}),e.jsxs("div",{className:l.splitInputGroup,children:[e.jsx("label",{htmlFor:"test-split",className:l.splitInputLabel,children:"Test (%)"}),e.jsx("input",{id:"test-split",type:"number",min:"0",max:"100",value:f,onChange:t=>{const o=parseInt(t.target.value)||0,a=100-_-o;a>=0&&(x(o),C(a))},className:l.splitInput,disabled:s})]}),e.jsxs("div",{className:l.splitInputGroup,children:[e.jsx("label",{htmlFor:"val-split",className:l.splitInputLabel,children:"Val (%)"}),e.jsx("input",{id:"val-split",type:"number",min:"0",max:"100",value:k,onChange:t=>{const o=parseInt(t.target.value)||0,a=100-_-o;a>=0&&(C(o),x(a))},className:l.splitInput,disabled:s})]})]}),_+f+k!==100&&e.jsx("div",{className:l.splitTotal,children:e.jsx("span",{className:l.splitTotalError,children:"Must equal 100%"})})]}),e.jsxs("div",{className:l.checkboxesContainer,children:[e.jsx("div",{className:"flex items-center gap-3",children:e.jsx(B,{name:"crisis-maps",label:`Crisis Maps (${v} images)`,value:u,onChange:t=>E(t),disabled:s})}),e.jsx("div",{className:"flex items-center gap-3",children:e.jsx(B,{name:"drone-images",label:`Drone Images (${T} images)`,value:S,onChange:t=>z(t),disabled:s})})]}),e.jsxs("div",{className:l.ratingWarningButtons,children:[e.jsx(b,{name:"confirm-export",onClick:N,disabled:s,children:s?e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(O,{className:"text-white"}),"Exporting..."]}):"Export Selected"}),e.jsx(b,{name:"cancel-export",variant:"tertiary",onClick:p,disabled:s,children:"Cancel"})]})]})]})}):null}export{fe as E,_e as F};
         | 
|  | |
| 1 | 
            +
            import{r as n,j as e,P,S as W,o as F,T as V,D as A,z as g,n as b,O as H,J as M,V as q,_ as O,L as D}from"./index-w0OOMPwN.js";const G=({title:c,titleId:i,...r})=>n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":i},r),c?n.createElement("title",{id:i},c):null,n.createElement("g",{clipPath:"url(#checkbox-indeterminate-line_svg__a)"},n.createElement("path",{d:"M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Zm1 2v14h14V5H5Zm2 6h10v2H7v-2Z"})),n.createElement("defs",null,n.createElement("clipPath",{id:"checkbox-indeterminate-line_svg__a"},n.createElement("path",{d:"M0 0h24v24H0z"})))),$=({title:c,titleId:i,...r})=>n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":i},r),c?n.createElement("title",{id:i},c):null,n.createElement("g",{clipPath:"url(#filter-line_svg__a)"},n.createElement("path",{d:"M9 13.5 4 6H3V4h18v2h-1l-5 7.5V22H9v-8.5ZM6.404 6 11 12.894V20h2v-7.106L17.596 6H6.404Z"})),n.createElement("defs",null,n.createElement("clipPath",{id:"filter-line_svg__a"},n.createElement("path",{d:"M0 0h24v24H0z"}))));function R(c){const{className:i,indeterminate:r,value:v}=c;return e.jsxs(e.Fragment,{children:[r&&e.jsx(G,{className:i}),v&&!r&&e.jsx(P,{className:i}),!v&&!r&&e.jsx(W,{className:i})]})}const U="_checkbox_12g7n_1",Z="_with-background_12g7n_7",Y="_checkmark-container_12g7n_12",J="_input_12g7n_18",K="_content_12g7n_33",Q="_description_12g7n_40",X="_checked_12g7n_45",L="_checkmark_12g7n_12",ee="_disabled-checkbox_12g7n_58",m={checkbox:U,withBackground:Z,checkmarkContainer:Y,input:J,content:K,description:Q,checked:X,checkmark:L,disabledCheckbox:ee};function B(c){const{className:i,checkmark:r=R,checkmarkClassName:v,checkmarkContainerClassName:T,disabled:s,error:h,indeterminate:y,inputClassName:w,invertedLogic:d=!1,label:j,labelContainerClassName:_,name:I,onChange:f,readOnly:x,tooltip:k,value:C,description:u,withBackground:E,...S}=c,z=n.useCallback(t=>{const o=t.currentTarget.checked;f(d?!o:o,I)},[I,f,d]),N=d?!C:C,p=F(m.checkbox,i,!y&&N&&m.checked,E&&m.withBackground,s&&m.disabledCheckbox,x&&m.readOnly);return e.jsxs("label",{className:p,title:k,children:[e.jsxs("div",{className:F(m.checkmarkContainer,T),children:[e.jsx("input",{onChange:z,className:F(m.input,w),type:"checkbox",checked:N??!1,disabled:s||x,readOnly:x,...S}),e.jsx(r,{className:F(m.checkmark,v),value:N??!1,indeterminate:y,"aria-hidden":"true"})]}),(j||u)&&e.jsxs("div",{className:m.content,children:[j&&e.jsx("div",{className:_,children:j}),u&&e.jsx("div",{className:m.description,children:u})]}),h&&e.jsx(V,{children:h})]})}function _e({sources:c,types:i,regions:r,countries:v,imageTypes:T,isLoadingFilters:s=!1}){const[h,y]=n.useState(!1),[w,d]=n.useState(""),{search:j,setSearch:_,srcFilter:I,setSrcFilter:f,catFilter:x,setCatFilter:k,regionFilter:C,setRegionFilter:u,countryFilter:E,setCountryFilter:S,imageTypeFilter:z,setImageTypeFilter:N,uploadTypeFilter:p,setUploadTypeFilter:t,clearAllFilters:o}=A();return n.useEffect(()=>{d(j)},[j]),e.jsxs("div",{className:"mb-6 space-y-4",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-4",children:[e.jsx(g,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:e.jsx(b,{name:"toggle-filters",variant:"secondary",onClick:()=>y(!h),className:"whitespace-nowrap",title:h?"Hide Filters":"Show Filters",children:e.jsx($,{className:"w-4 h-4"})})}),e.jsx(g,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]",children:e.jsx(H,{name:"search",placeholder:"Search",value:w,onChange:a=>d(a||""),onKeyDown:a=>{a.key==="Enter"&&_(w)}})}),e.jsx(g,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:e.jsx(b,{name:"clear-filters",variant:"secondary",onClick:o,children:"Clear Filters"})})]}),h&&e.jsx("div",{className:"bg-white/20 backdrop-blur-sm rounded-md p-4",children:e.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-3 gap-4",children:[e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"source",placeholder:s?"Loading...":"All Sources",options:c,value:I||null,onChange:a=>f(a||""),keySelector:a=>a.s_code,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"category",placeholder:s?"Loading...":"All Categories",options:i,value:x||null,onChange:a=>k(a||""),keySelector:a=>a.t_code,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"region",placeholder:s?"Loading...":"All Regions",options:r,value:C||null,onChange:a=>u(a||""),keySelector:a=>a.r_code,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(q,{name:"country",placeholder:s?"Loading...":"All Countries",options:v,value:E?[E]:[],onChange:a=>S(a[0]||""),keySelector:a=>a.c_code,labelSelector:a=>a.label,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"imageType",placeholder:s?"Loading...":"All Image Types",options:T,value:z||null,onChange:a=>N(a||""),keySelector:a=>a.image_type,labelSelector:a=>a.label,required:!1,disabled:s})}),e.jsx(g,{withInternalPadding:!0,className:"p-2",children:e.jsx(M,{name:"uploadType",placeholder:"All Upload Types",options:[{key:"single",label:"Single Upload"},{key:"multiple",label:"Multiple Upload"}],value:p||null,onChange:a=>t(a||""),keySelector:a=>a.key,labelSelector:a=>a.label,required:!1,disabled:!1})})]})})]})}const ae="_fullSizeModalOverlay_cyz3b_1",le="_fullSizeModalContent_cyz3b_29",te="_ratingWarningContent_cyz3b_53",se="_ratingWarningTitle_cyz3b_65",ne="_exportModeSection_cyz3b_133",ie="_splitConfigSection_cyz3b_143",re="_splitConfigTitle_cyz3b_153",ce="_splitInputsContainer_cyz3b_167",oe="_splitInputGroup_cyz3b_183",de="_splitInputLabel_cyz3b_197",pe="_splitInput_cyz3b_167",me="_splitTotal_cyz3b_247",he="_splitTotalError_cyz3b_261",xe="_checkboxesContainer_cyz3b_271",ue="_ratingWarningButtons_cyz3b_289",ge="_singleExportMessage_cyz3b_309",be="_navigateButtonContainer_cyz3b_333",ve="_loadingOverlay_cyz3b_349",l={fullSizeModalOverlay:ae,fullSizeModalContent:le,ratingWarningContent:te,ratingWarningTitle:se,exportModeSection:ne,splitConfigSection:ie,splitConfigTitle:re,splitInputsContainer:ce,splitInputGroup:oe,splitInputLabel:de,splitInput:pe,splitTotal:me,splitTotalError:he,checkboxesContainer:xe,ratingWarningButtons:ue,singleExportMessage:ge,navigateButtonContainer:be,loadingOverlay:ve};function fe({isOpen:c,onClose:i,onExport:r,crisisMapsCount:v,droneImagesCount:T,isLoading:s=!1,exportSuccess:h=!1,variant:y="bulk",onNavigateAndExport:w}){const[d,j]=n.useState("standard"),[_,I]=n.useState(80),[f,x]=n.useState(10),[k,C]=n.useState(10),[u,E]=n.useState(!0),[S,z]=n.useState(!0),N=()=>{if(y==="single"){r(d,["crisis_map","drone_image"]);return}if(!u&&!S){alert("Please select at least one image type to export.");return}const t=[];u&&t.push("crisis_map"),S&&t.push("drone_image"),r(d,t)},p=()=>{i()};return c?y==="single"?e.jsx("div",{className:l.fullSizeModalOverlay,onClick:p,children:e.jsxs("div",{className:l.fullSizeModalContent,onClick:t=>t.stopPropagation(),children:[s&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx(O,{className:"text-ifrcRed"}),e.jsx("div",{className:"text-lg font-medium",children:"Exporting..."}),e.jsx("div",{className:"text-sm text-gray-600",children:"This might take a few seconds"})]})}),h&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx("div",{className:"text-lg font-medium",children:"Export Successful!"}),e.jsx("div",{className:"text-sm text-gray-600",children:"Your dataset has been downloaded"}),e.jsx(b,{name:"close-export-success",onClick:p,className:"mt-4",children:"Close"})]})}),e.jsxs("div",{className:l.ratingWarningContent,children:[e.jsx("h3",{className:l.ratingWarningTitle,children:"Export Single Item"}),e.jsxs("div",{className:l.singleExportMessage,children:[e.jsx("p",{children:"This only exports the 1 item currently on display."}),e.jsx("p",{children:'You may export the entire dataset from the "list view" here:'})]}),e.jsx("div",{className:l.navigateButtonContainer,children:e.jsx(b,{name:"navigate-to-list",variant:"secondary",onClick:w,children:"Navigate to List View"})}),e.jsxs("div",{className:l.ratingWarningButtons,children:[e.jsx(b,{name:"continue-export",onClick:N,disabled:s,children:s?e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(O,{className:"text-white"}),"Exporting..."]}):"Continue"}),e.jsx(b,{name:"cancel-export",variant:"tertiary",onClick:p,disabled:s,children:"Cancel"})]})]})]})}):e.jsx("div",{className:l.fullSizeModalOverlay,onClick:p,children:e.jsxs("div",{className:l.fullSizeModalContent,onClick:t=>t.stopPropagation(),children:[s&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx(O,{className:"text-ifrcRed"}),e.jsx("div",{className:"text-lg font-medium",children:"Exporting..."}),e.jsx("div",{className:"text-sm text-gray-600",children:"This might take a few seconds"})]})}),h&&e.jsx("div",{className:l.loadingOverlay,children:e.jsxs("div",{className:"flex flex-col items-center gap-4",children:[e.jsx("div",{className:"text-lg font-medium",children:"Export Successful!"}),e.jsx("div",{className:"text-sm text-gray-600",children:"Your dataset has been downloaded"}),e.jsx(b,{name:"close-export-success",onClick:p,className:"mt-4",children:"Close"})]})}),e.jsxs("div",{className:l.ratingWarningContent,children:[e.jsx("h3",{className:l.ratingWarningTitle,children:"Export Dataset"}),e.jsx("div",{className:l.exportModeSection,children:e.jsx(D,{name:"export-mode",value:d,onChange:t=>{(t==="standard"||t==="fine-tuning")&&j(t)},options:[{key:"standard",label:"Standard"},{key:"fine-tuning",label:"Fine-tuning"}],keySelector:t=>t.key,labelSelector:t=>t.label,disabled:s})}),d==="fine-tuning"&&e.jsxs("div",{className:l.splitConfigSection,children:[e.jsx("div",{className:l.splitConfigTitle,children:"Dataset Split Configuration"}),e.jsxs("div",{className:l.splitInputsContainer,children:[e.jsxs("div",{className:l.splitInputGroup,children:[e.jsx("label",{htmlFor:"train-split",className:l.splitInputLabel,children:"Train (%)"}),e.jsx("input",{id:"train-split",type:"number",min:"0",max:"100",value:_,onChange:t=>{const o=parseInt(t.target.value)||0,a=100-o;a>=0&&(I(o),f+k>a&&(x(Math.floor(a/2)),C(a-Math.floor(a/2))))},className:l.splitInput,disabled:s})]}),e.jsxs("div",{className:l.splitInputGroup,children:[e.jsx("label",{htmlFor:"test-split",className:l.splitInputLabel,children:"Test (%)"}),e.jsx("input",{id:"test-split",type:"number",min:"0",max:"100",value:f,onChange:t=>{const o=parseInt(t.target.value)||0,a=100-_-o;a>=0&&(x(o),C(a))},className:l.splitInput,disabled:s})]}),e.jsxs("div",{className:l.splitInputGroup,children:[e.jsx("label",{htmlFor:"val-split",className:l.splitInputLabel,children:"Val (%)"}),e.jsx("input",{id:"val-split",type:"number",min:"0",max:"100",value:k,onChange:t=>{const o=parseInt(t.target.value)||0,a=100-_-o;a>=0&&(C(o),x(a))},className:l.splitInput,disabled:s})]})]}),_+f+k!==100&&e.jsx("div",{className:l.splitTotal,children:e.jsx("span",{className:l.splitTotalError,children:"Must equal 100%"})})]}),e.jsxs("div",{className:l.checkboxesContainer,children:[e.jsx("div",{className:"flex items-center gap-3",children:e.jsx(B,{name:"crisis-maps",label:`Crisis Maps (${v} images)`,value:u,onChange:t=>E(t),disabled:s})}),e.jsx("div",{className:"flex items-center gap-3",children:e.jsx(B,{name:"drone-images",label:`Drone Images (${T} images)`,value:S,onChange:t=>z(t),disabled:s})})]}),e.jsxs("div",{className:l.ratingWarningButtons,children:[e.jsx(b,{name:"confirm-export",onClick:N,disabled:s,children:s?e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(O,{className:"text-white"}),"Exporting..."]}):"Export Selected"}),e.jsx(b,{name:"cancel-export",variant:"tertiary",onClick:p,disabled:s,children:"Cancel"})]})]})]})}):null}export{fe as E,_e as F};
         | 
    	
        py_backend/static/assets/index-BTevbmbp.js
    ADDED
    
    | @@ -0,0 +1,2 @@ | |
|  | |
|  | 
|  | |
| 1 | 
            +
            const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/jszip.min-zGid42TK.js","assets/index-w0OOMPwN.js","assets/index-cCOeofBN.css"])))=>i.map(i=>d[i]);
         | 
| 2 | 
            +
            import{K as ea,x as aa,r as d,D as ta,j as a,N as ee,n as M,_ as we,L as sa,z as E,v as ne,w as oe,F as ia,M as na,G as oa}from"./index-w0OOMPwN.js";import{u as ra}from"./useAdmin-avReXN15.js";import{F as la,E as ca}from"./ExportModal-C5df6JIW.js";const da="_tabSelector_usssr_1",ga="_imageContainer_usssr_12",ma="_imagePlaceholder_usssr_33",ua="_metadataTags_usssr_45",pa="_metadataTag_usssr_45",fa="_captionContainer_usssr_67",_a="_captionText_usssr_74",ha="_gridLayout_usssr_131",xa="_detailsSection_usssr_155",ya="_loadingContainer_usssr_161",va="_errorContainer_usssr_171",ja="_fullSizeModalOverlay_usssr_205",wa="_fullSizeModalContent_usssr_219",Ia="_ratingWarningContent_usssr_230",Na="_ratingWarningTitle_usssr_236",Ca="_ratingWarningText_usssr_243",ba="_ratingWarningButtons_usssr_250",Sa="_carouselContainer_usssr_365",ka="_carouselImageWrapper_usssr_370",Da="_carouselImage_usssr_370",La="_carouselNavigation_usssr_393",Ma="_carouselButton_usssr_405",Fa="_carouselIndicators_usssr_429",Ta="_carouselIndicator_usssr_429",Ea="_carouselIndicatorActive_usssr_458",$a="_singleImageContainer_usssr_488",Pa="_viewImageButtonContainer_usssr_494",g={tabSelector:da,imageContainer:ga,imagePlaceholder:ma,metadataTags:ua,metadataTag:pa,captionContainer:fa,captionText:_a,gridLayout:ha,detailsSection:xa,loadingContainer:ya,errorContainer:va,fullSizeModalOverlay:ja,fullSizeModalContent:wa,ratingWarningContent:Ia,ratingWarningTitle:Na,ratingWarningText:Ca,ratingWarningButtons:ba,carouselContainer:Sa,carouselImageWrapper:ka,carouselImage:Da,carouselNavigation:La,carouselButton:Ma,carouselIndicators:Fa,carouselIndicator:Ta,carouselIndicatorActive:Ea,singleImageContainer:$a,viewImageButtonContainer:Pa};function st(){const{mapId:u}=ea(),x=aa(),{isAuthenticated:re}=ra(),[le,Ie]=d.useState("mapDetails"),[e,ae]=d.useState(null),[z,R]=d.useState(!0),[ce,O]=d.useState(null),[de,Ne]=d.useState([]),[ge,Ce]=d.useState([]),[me,be]=d.useState([]),[ue,Se]=d.useState([]),[ke,De]=d.useState([]),[Le,Me]=d.useState(!1),[Fe,Te]=d.useState(!1),[W,q]=d.useState(!1),[Ee,K]=d.useState(!1),[pe,Z]=d.useState(!1),[$e,te]=d.useState(!1),[Pe,se]=d.useState(!1),[Ra,Aa]=d.useState("standard"),[$,za]=d.useState(80),[J,Oa]=d.useState(10),[Ua,Ba]=d.useState(10),[Wa,Ja]=d.useState(!0),[Va,Ga]=d.useState(!0),[U,Q]=d.useState(!1),[Re,fe]=d.useState(!1),[Ae,_e]=d.useState(null),[ze,V]=d.useState(!1),[h,G]=d.useState([]),[k,A]=d.useState(0),[H,he]=d.useState(!1),{search:p,setSearch:Ha,srcFilter:y,setSrcFilter:qa,catFilter:v,setCatFilter:Ka,regionFilter:j,setRegionFilter:Za,countryFilter:w,setCountryFilter:Qa,imageTypeFilter:I,setImageTypeFilter:Xa,uploadTypeFilter:N,setUploadTypeFilter:Ya,showReferenceExamples:b,setShowReferenceExamples:Oe,clearAllFilters:Ue}=ta(),Be=[{key:"explore",label:"List"},{key:"mapDetails",label:"Carousel"}],X=d.useCallback(async t=>{if(!(!t||t==="undefined"||t==="null"||t.trim()===""))try{const s=new URLSearchParams;p&&s.append("search",p),y&&s.append("source",y),v&&s.append("event_type",v),j&&s.append("region",j),w&&s.append("country",w),I&&s.append("image_type",I),N&&s.append("upload_type",N),b&&s.append("starred_only","true");const l=await fetch(`/api/images/grouped?${s.toString()}`);if(l.ok){const i=await l.json();console.log("Server response for upload_type=multiple:",{url:`/api/images/grouped?${s.toString()}`,count:i.length,images:i.map(r=>({image_id:r.image_id,image_count:r.image_count,all_image_ids:r.all_image_ids,all_image_ids_length:r.all_image_ids?.length}))});const n=i.findIndex(r=>r.image_id===t);console.log("Navigation availability check (server-side):",{filteredImagesCount:i.length,currentIndex:n,currentId:t,uploadTypeFilter:N,hasPrevious:i.length>1&&n>0,hasNext:i.length>1&&n<i.length-1,filteredImages:i.map(r=>({image_id:r.image_id,image_count:r.image_count,all_image_ids:r.all_image_ids,image_type:r.image_type}))}),Me(i.length>1&&n>0),Te(i.length>1&&n<i.length-1)}}catch(s){console.error("Failed to check navigation availability:",s)}},[p,y,v,j,w,I,N,b]),ie=d.useCallback(async t=>{console.log("fetchAllImages called with imageIds:",t),he(!0);try{const s=t.map(async i=>{const n=await fetch(`/api/images/${i}`);if(!n.ok)throw new Error(`Failed to fetch image ${i}`);return n.json()}),l=await Promise.all(s);G(l),A(0),console.log("fetchAllImages: Loaded",l.length,"images")}catch(s){console.error("fetchAllImages error:",s),O(s instanceof Error?s.message:"Failed to load all images")}finally{he(!1)}},[]),xe=d.useCallback(async t=>{if(console.log("fetchMapData called with id:",t),console.log("fetchMapData id type:",typeof t),!t||t==="undefined"||t==="null"||t.trim()===""){console.log("fetchMapData: Invalid ID detected:",t),O("Invalid Map ID"),R(!1);return}if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)){console.log("fetchMapData: Invalid UUID format:",t),O("Invalid Map ID format"),R(!1);return}console.log("fetchMapData: Making API call for id:",t),q(!0),R(!0);try{const l=await fetch(`/api/images/${t}`);if(!l.ok)throw new Error("Map not found");const i=await l.json();if(ae(i),i.all_image_ids&&i.all_image_ids.length>1)await ie(i.all_image_ids);else if(i.image_count&&i.image_count>1){console.log("Multi-upload detected but no all_image_ids, trying grouped endpoint");try{const n=await fetch("/api/images/grouped");if(n.ok){const o=(await n.json()).find(m=>m.all_image_ids&&m.all_image_ids.includes(i.image_id));o&&o.all_image_ids?await ie(o.all_image_ids):(G([i]),A(0))}else G([i]),A(0)}catch(n){console.error("Failed to fetch from grouped endpoint:",n),G([i]),A(0)}}else G([i]),A(0);await X(t)}catch(l){O(l instanceof Error?l.message:"Unknown error occurred")}finally{R(!1),q(!1)}},[X,ie]),We=d.useCallback(()=>{h.length>1&&A(t=>t>0?t-1:h.length-1)},[h.length]),Je=d.useCallback(()=>{h.length>1&&A(t=>t<h.length-1?t+1:0)},[h.length]),Ve=d.useCallback(t=>{t>=0&&t<h.length&&A(t)},[h.length]),ye=d.useCallback(async t=>{const s=t||(h.length>0?h[k]:e);if(s){V(!0),_e(s),fe(!0);try{const l=new Image;l.onload=()=>{V(!1)},l.onerror=()=>{V(!1)},l.src=s.image_url}catch(l){console.error("Error preloading full-size image:",l),V(!1)}}},[h,k,e]),Ge=d.useCallback(()=>{fe(!1),_e(null),V(!1)},[]);d.useEffect(()=>{if(console.log("MapDetailsPage: mapId from useParams:",u),console.log("MapDetailsPage: mapId type:",typeof u),console.log("MapDetailsPage: mapId value:",u),!u||u==="undefined"||u==="null"||u.trim()===""||u===void 0||u===null){console.log("MapDetailsPage: Invalid mapId, setting error"),O("Map ID is required"),R(!1);return}if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(u)){console.log("MapDetailsPage: Invalid UUID format:",u),O("Invalid Map ID format"),R(!1);return}console.log("MapDetailsPage: Fetching data for mapId:",u),xe(u)},[u,xe]),d.useEffect(()=>{if(!e||z||U)return;if(!u||u==="undefined"||u==="null"||u.trim()===""){console.log("Auto-navigation skipped: Invalid mapId");return}if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(u)){console.log("Auto-navigation skipped: Invalid mapId format");return}(()=>{const l=!p||e.title?.toLowerCase().includes(p.toLowerCase())||e.generated?.toLowerCase().includes(p.toLowerCase())||e.source?.toLowerCase().includes(p.toLowerCase())||e.event_type?.toLowerCase().includes(p.toLowerCase()),i=!y||e.source===y,n=!v||e.event_type===v,r=!j||e.countries.some(P=>P.r_code===j),o=!w||e.countries.some(P=>P.c_code===w),m=!I||e.image_type===I,_=!b||e.starred===!0,D=l&&i&&n&&r&&o&&m&&_;return console.log("Auto-navigation check:",{mapId:u,search:p,srcFilter:y,catFilter:v,regionFilter:j,countryFilter:w,imageTypeFilter:I,showReferenceExamples:b,matchesSearch:l,matchesSource:i,matchesCategory:n,matchesRegion:r,matchesCountry:o,matchesImageType:m,matchesReferenceExamples:_,matches:D}),D})()||(console.log("Current map does not match filters, looking for first matching item"),fetch("/api/images").then(l=>l.json()).then(l=>{console.log("Auto-navigation: Received images from API:",l.length),console.log("Auto-navigation: First few images:",l.slice(0,3).map(n=>({image_id:n.image_id,title:n.title})));const i=l.find(n=>{const r=!p||n.title?.toLowerCase().includes(p.toLowerCase())||n.generated?.toLowerCase().includes(p.toLowerCase())||n.source?.toLowerCase().includes(p.toLowerCase())||n.event_type?.toLowerCase().includes(p.toLowerCase()),o=!y||n.source===y,m=!v||n.event_type===v,_=!j||n.countries?.some(f=>f.r_code===j),D=!w||n.countries?.some(f=>f.c_code===w),P=!I||n.image_type===I,F=!b||n.starred===!0;return r&&o&&m&&_&&D&&P&&F});console.log("Auto-navigation: Found first matching image:",i?{image_id:i.image_id,title:i.title,source:i.source}:"No matching image found"),i&&i.image_id&&i.image_id!=="undefined"&&i.image_id!=="null"&&i.image_id.trim()!==""&&i.image_id!==u&&(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(i.image_id)?(console.log("Auto-navigating to:",i.image_id),x(`/map/${i.image_id}`)):console.error("Auto-navigation blocked: Invalid image_id format:",i.image_id))}).catch(console.error))},[e,p,y,v,j,w,I,b,u,x,z,U]);const ve=async t=>{if(!W){q(!0);try{const s=new URLSearchParams;p&&s.append("search",p),y&&s.append("source",y),v&&s.append("event_type",v),j&&s.append("region",j),w&&s.append("country",w),I&&s.append("image_type",I),N&&s.append("upload_type",N),b&&s.append("starred_only","true");const l=await fetch(`/api/images/grouped?${s.toString()}`);if(l.ok){const i=await l.json(),n=i.findIndex(m=>m.image_id===u);if(n===-1){console.error("Current image not found in filtered list");return}let r;t==="previous"?r=n>0?n-1:i.length-1:r=n<i.length-1?n+1:0;const o=i[r];o&&o.image_id&&o.image_id!=="undefined"&&o.image_id!=="null"&&o.image_id.trim()!==""&&(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(o.image_id)?(console.log("Carousel navigating to:",o.image_id),x(`/map/${o.image_id}`)):console.error("Carousel navigation blocked: Invalid image_id format:",o.image_id))}}catch(s){console.error("Failed to navigate to item:",s)}finally{q(!1)}}};d.useEffect(()=>{console.log("=== NAVIGATION USEEFFECT TRIGGERED ==="),console.log("Navigation useEffect triggered:",{map:!!e,mapId:u,loading:z,isDeleting:U,uploadTypeFilter:N,allFilters:{search:p,srcFilter:y,catFilter:v,regionFilter:j,countryFilter:w,imageTypeFilter:I,uploadTypeFilter:N,showReferenceExamples:b}}),e&&u&&!z&&!U?(console.log("Calling checkNavigationAvailability with:",u),X(u)):console.log("NOT calling checkNavigationAvailability because:",{map:!!e,mapId:!!u,loading:z,isDeleting:U})},[e,u,p,y,v,j,w,I,N,b,z,U,X]),d.useEffect(()=>{Promise.all([fetch("/api/sources").then(t=>t.json()),fetch("/api/types").then(t=>t.json()),fetch("/api/image-types").then(t=>t.json()),fetch("/api/regions").then(t=>t.json()),fetch("/api/countries").then(t=>t.json())]).then(([t,s,l,i,n])=>{Ne(t),Ce(s),be(l),Se(i),De(n)}).catch(console.error)},[]);const He=async()=>{e&&K(!0)},qe=async()=>{if(e)try{(await fetch(`/api/images/${e.image_id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({starred:!e.starred})})).ok?ae(s=>s?{...s,starred:!s.starred}:null):console.error("Failed to toggle starred status")}catch(t){console.error("Error toggling starred status:",t)}},Ke=async()=>{if(e){Q(!0);try{if(console.log("Deleting image with ID:",e.image_id),(await fetch(`/api/images/${e.image_id}`,{method:"DELETE"})).ok){ae(s=>s?{...s,starred:!s.starred}:null),K(!1);try{const s=await fetch("/api/images/grouped");if(s.ok){const i=(await s.json()).filter(r=>{const o=!p||r.title?.toLowerCase().includes(p.toLowerCase())||r.generated?.toLowerCase().includes(p.toLowerCase())||r.source?.toLowerCase().includes(p.toLowerCase())||r.event_type?.toLowerCase().includes(p.toLowerCase()),m=!y||r.source===y,_=!v||r.event_type===v,D=!j||r.countries?.some(C=>C.r_code===j),P=!w||r.countries?.some(C=>C.c_code===w),F=!I||r.image_type===I,f=!N||N==="single"&&(!r.image_count||r.image_count<=1)||N==="multiple"&&r.image_count&&r.image_count>1,L=!b||r.starred===!0;return o&&m&&_&&D&&P&&F&&f&&L}),n=i.filter(r=>r.image_id!==e.image_id);if(n.length>0){const r=i.findIndex(m=>m.image_id===e.image_id);let o;if(r===i.length-1?o=r-1:o=r,console.log("Navigation target:",{currentIndex:r,targetIndex:o,targetId:n[o]?.image_id}),o>=0&&o<n.length){const m=n[o];m&&m.image_id&&m.image_id!=="undefined"&&m.image_id!=="null"&&m.image_id.trim()!==""?/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(m.image_id)?(console.log("Navigating to:",m.image_id),x(`/map/${m.image_id}`)):(console.error("Navigation blocked: Invalid image_id format:",m.image_id),x("/explore")):(console.error("Navigation blocked: Invalid image_id:",m?.image_id),x("/explore"))}else n[0]&&n[0].image_id&&n[0].image_id!=="undefined"&&n[0].image_id!=="null"&&n[0].image_id.trim()!==""?/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(n[0].image_id)?(console.log("Fallback navigation to first item:",n[0].image_id),x(`/map/${n[0].image_id}`)):(console.error("Fallback navigation blocked: Invalid image_id format:",n[0].image_id),x("/explore")):(console.log("No valid remaining items, going to explore page"),x("/explore"))}else console.log("No remaining items, going to explore page"),x("/explore")}else x("/explore")}catch(s){console.error("Failed to navigate to next item:",s),x("/explore")}finally{Q(!1)}}else console.error("Delete failed"),Q(!1)}catch(t){console.error("Delete failed:",t),Q(!1)}}},je=d.useCallback(async()=>{R(!0);try{const t=new URLSearchParams;p&&t.append("search",p),y&&t.append("source",y),v&&t.append("event_type",v),j&&t.append("region",j),w&&t.append("country",w),I&&t.append("image_type",I),N&&t.append("upload_type",N),b&&t.append("starred_only","true");const s=await fetch(`/api/images/grouped?${t.toString()}`);if(s.ok){const l=await s.json();if(l.length>0){const i=l[0];i&&i.image_id&&x(`/map/${i.image_id}`)}else x("/explore")}}catch(t){console.error("Failed to navigate to matching image:",t),x("/explore")}finally{R(!1)}},[p,y,v,j,w,I,N,b,x]),c=d.useMemo(()=>{if(!e)return null;if(!p&&!y&&!v&&!j&&!w&&!I&&!N&&!b)return e;const t=!p||e.title?.toLowerCase().includes(p.toLowerCase())||e.generated?.toLowerCase().includes(p.toLowerCase())||e.source?.toLowerCase().includes(p.toLowerCase())||e.event_type?.toLowerCase().includes(p.toLowerCase()),s=!y||e.source===y,l=!v||e.event_type===v,i=!j||e.countries.some(D=>D.r_code===j),n=!w||e.countries.some(D=>D.c_code===w),r=!I||e.image_type===I,o=!N||N==="single"&&(!e.image_count||e.image_count<=1)&&(!e.all_image_ids||e.all_image_ids.length<=1)||N==="multiple"&&(e.image_count&&e.image_count>1||e.all_image_ids&&e.all_image_ids.length>1),m=!b||e.starred===!0,_=t&&s&&l&&i&&n&&r&&o&&m;return!_&&(p||y||v||j||w||I||N||b)?(setTimeout(()=>{je()},100),e):_?e:null},[e,p,y,v,j,w,I,N,b,je]),Ze=()=>{if(!e)return;if(!e.all_image_ids||e.all_image_ids.length<=1){const i=`/upload?step=1&contribute=true&imageIds=${[e.image_id].join(",")}`;x(i);return}const s=`/upload?step=1&contribute=true&imageIds=${e.all_image_ids.join(",")}`;x(s)},T=(t,s)=>({image:`images/${s}`,caption:t.edited||t.generated||"",metadata:{image_id:t.image_count&&t.image_count>1?t.all_image_ids||[t.image_id]:t.image_id,title:t.title,source:t.source,event_type:t.event_type,image_type:t.image_type,countries:t.countries,starred:t.starred,image_count:t.image_count||1}}),Qe=async t=>{if(e){te(!0),se(!1);try{const s=(await oa(async()=>{const{default:o}=await import("./jszip.min-zGid42TK.js").then(m=>m.j);return{default:o}},__vite__mapDeps([0,1,2]))).default,l=new s;if(e.image_type==="crisis_map"){const o=l.folder("crisis_maps_dataset"),m=o?.folder("images");if(m)try{const _=e.image_count&&e.image_count>1?e.all_image_ids||[e.image_id]:[e.image_id],D=_.map(async(f,L)=>{try{const C=await fetch(`/api/images/${f}/file`);if(!C.ok)throw new Error(`Failed to fetch image ${f}`);const S=await C.blob(),Y=e.file_key.split(".").pop()||"jpg",B=`0001_${String(L+1).padStart(2,"0")}.${Y}`;return m.file(B,S),{success:!0,fileName:B,imageId:f}}catch(C){return console.error(`Failed to process image ${f}:`,C),{success:!1,fileName:"",imageId:f}}}),F=(await Promise.all(D)).filter(f=>f.success);if(F.length===0)throw new Error("No images could be processed");if(t==="fine-tuning"){const f=[],L=[],C=[],S=F.map(Ye=>`images/${Ye.fileName}`),Y=Math.random(),B={image:S.length===1?S[0]:S,caption:e.edited||e.generated||"",metadata:{image_id:_,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};Y<$/100?f.push(B):Y<($+J)/100?L.push(B):C.push(B),o&&(o.file("train.jsonl",JSON.stringify(f,null,2)),o.file("test.jsonl",JSON.stringify(L,null,2)),o.file("val.jsonl",JSON.stringify(C,null,2)))}else{const f=F.map(C=>`images/${C.fileName}`),L={image:f.length===1?f[0]:f,caption:e.edited||e.generated||"",metadata:{image_id:_,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};o&&o.file("0001.json",JSON.stringify(L,null,2))}}catch(_){throw console.error(`Failed to process image ${e.image_id}:`,_),_}}else if(e.image_type==="drone_image"){const o=l.folder("drone_images_dataset"),m=o?.folder("images");if(m)try{const _=await fetch(`/api/images/${e.image_id}/file`);if(!_.ok)throw new Error(`Failed to fetch image ${e.image_id}`);const D=await _.blob(),F=`0001.${e.file_key.split(".").pop()||"jpg"}`;if(m.file(F,D),t==="fine-tuning"){const f=[],L=[],C=[];if(String(e?.image_type)==="crisis_map"){const S=Math.random();S<$/100?f.push(T(e,"0001")):S<($+J)/100?L.push(T(e,"0001")):C.push(T(e,"0001"))}else if(String(e?.image_type)==="drone_image"){const S=Math.random();S<$/100?f.push(T(e,"0001")):S<($+J)/100?L.push(T(e,"0001")):C.push(T(e,"0001"))}o&&(o.file("train.jsonl",JSON.stringify(f,null,2)),o.file("test.jsonl",JSON.stringify(L,null,2)),o.file("val.jsonl",JSON.stringify(C,null,2)))}else{const f={image:`images/${F}`,caption:e.edited||e.generated||"",metadata:{image_id:e.image_count&&e.image_count>1?e.all_image_ids||[e.image_id]:e.image_id,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};o&&o.file("0001.json",JSON.stringify(f,null,2))}}catch(_){throw console.error(`Failed to process image ${e.image_id}:`,_),_}}else{const o=l.folder("generic_dataset"),m=o?.folder("images");if(m)try{const _=await fetch(`/api/images/${e.image_id}/file`);if(!_.ok)throw new Error(`Failed to fetch image ${e.image_id}`);const D=await _.blob(),F=`0001.${e.file_key.split(".").pop()||"jpg"}`;if(m.file(F,D),t==="fine-tuning"){const f=[],L=[],C=[];if(String(e?.image_type)==="crisis_map"){const S=Math.random();S<$/100?f.push(T(e,"0001")):S<($+J)/100?L.push(T(e,"0001")):C.push(T(e,"0001"))}else if(String(e?.image_type)==="drone_image"){const S=Math.random();S<$/100?f.push(T(e,"0001")):S<($+J)/100?L.push(T(e,"0001")):C.push(T(e,"0001"))}o&&(o.file("train.jsonl",JSON.stringify(f,null,2)),o.file("test.jsonl",JSON.stringify(L,null,2)),o.file("val.jsonl",JSON.stringify(C,null,2)))}else{const f={image:`images/${F}`,caption:e.edited||e.generated||"",metadata:{image_id:e.image_count&&e.image_count>1?e.all_image_ids||[e.image_id]:e.image_id,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};o&&o.file("0001.json",JSON.stringify(f,null,2))}}catch(_){throw console.error(`Failed to process image ${e.image_id}:`,_),_}}const i=await l.generateAsync({type:"blob"}),n=URL.createObjectURL(i),r=document.createElement("a");r.href=n,r.download=`dataset_${e.image_type}_${e.image_id}_${t}_${new Date().toISOString().split("T")[0]}.zip`,document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(n),console.log(`Exported ${e.image_type} dataset with 1 image in ${t} mode`),se(!0)}catch(s){console.error("Export failed:",s),alert("Failed to export dataset. Please try again.")}finally{te(!1)}}},Xe=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;return!u||u==="undefined"||u==="null"||u.trim()===""||!Xe.test(u)?a.jsx(ee,{children:a.jsxs("div",{className:"flex flex-col items-center gap-4 text-center py-12",children:[a.jsx("div",{className:"text-4xl",children:"β οΈ"}),a.jsx("div",{className:"text-xl font-semibold",children:"Invalid Map ID"}),a.jsx("div",{children:"The map ID provided is not valid."}),a.jsxs("div",{className:"text-sm text-gray-500 mt-2",children:['Debug Info: mapId = "',u,'" (type: ',typeof u,")"]}),a.jsx(M,{name:"back-to-explore",variant:"secondary",onClick:()=>x("/explore"),children:"Return to Explore"})]})}):z?a.jsx(ee,{children:a.jsx("div",{className:g.loadingContainer,children:a.jsxs("div",{className:"flex flex-col items-center gap-4",children:[a.jsx(we,{className:"text-ifrcRed"}),a.jsx("div",{children:"Loading map details..."})]})})}):ce||!e?a.jsx(ee,{children:a.jsx("div",{className:g.errorContainer,children:a.jsxs("div",{className:"flex flex-col items-center gap-4 text-center",children:[a.jsx("div",{className:"text-4xl",children:"β οΈ"}),a.jsx("div",{className:"text-xl font-semibold",children:"Unable to load map"}),a.jsx("div",{children:ce||"Map not found"}),a.jsx(M,{name:"back-to-explore",variant:"secondary",onClick:()=>x("/explore"),children:"Return to Explore"})]})})}):a.jsxs(ee,{children:[a.jsxs("div",{className:"max-w-7xl mx-auto",children:[a.jsxs("div",{className:g.tabSelector,children:[a.jsx(sa,{name:"map-details-view",value:le,onChange:t=>{(t==="mapDetails"||t==="explore")&&(Ie(t),t==="explore"&&x("/explore"))},options:Be,keySelector:t=>t.key,labelSelector:t=>t.label}),a.jsxs("div",{className:"flex items-center gap-2 ml-auto",children:[a.jsx(E,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:a.jsxs(M,{name:"reference-examples",variant:b?"primary":"secondary",onClick:()=>Oe(!b),className:"whitespace-nowrap",children:[a.jsx("span",{className:"mr-2",children:b?a.jsx("span",{className:"text-yellow-400",children:"β
"}):a.jsx("span",{className:"text-yellow-400",children:"β"})}),"Reference Examples"]})}),a.jsx(M,{name:"export-dataset",variant:"secondary",onClick:()=>Z(!0),children:"Export"})]})]}),a.jsx(la,{sources:de,types:ge,regions:ue,countries:ke,imageTypes:me,isLoadingFilters:!1}),le==="mapDetails"?a.jsx("div",{className:"relative",children:c?a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:g.gridLayout,children:[a.jsxs(E,{heading:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{children:c.title||"Map Image"}),c.starred&&a.jsx("span",{className:"text-red-500 text-xl",title:"Starred image",children:"β
"})]}),headingLevel:2,withHeaderBorder:!0,withInternalPadding:!0,spacing:"comfortable",children:[a.jsx("div",{className:g.imageContainer,children:e?.image_count&&e.image_count>1||h.length>1?a.jsxs("div",{className:g.carouselContainer,children:[a.jsx("div",{className:g.carouselImageWrapper,children:H?a.jsxs("div",{className:g.imagePlaceholder,children:[a.jsx(we,{className:"text-ifrcRed"}),a.jsx("div",{children:"Loading images..."})]}):h[k]?.detail_url?a.jsx("img",{src:h[k].detail_url,alt:h[k].file_key,className:g.carouselImage,onError:t=>{console.log("MapDetailsPage: Detail image failed to load, falling back to original:",h[k].detail_url);const s=t.target;h[k].image_url&&(s.src=h[k].image_url)},onLoad:()=>console.log("MapDetailsPage: Detail image loaded successfully:",h[k].detail_url)}):h[k]?.image_url?a.jsx("img",{src:h[k].image_url,alt:h[k].file_key,className:g.carouselImage,onLoad:()=>console.log("MapDetailsPage: Original image loaded successfully:",h[k].image_url)}):a.jsx("div",{className:g.imagePlaceholder,children:"No image available"})}),a.jsxs("div",{className:g.carouselNavigation,children:[a.jsx(M,{name:"previous-image",variant:"tertiary",size:1,onClick:We,disabled:H,className:g.carouselButton,children:a.jsx(ne,{className:"w-4 h-4"})}),a.jsx("div",{className:g.carouselIndicators,children:h.map((t,s)=>a.jsx("button",{onClick:()=>Ve(s),className:`${g.carouselIndicator} ${s===k?g.carouselIndicatorActive:""}`,disabled:H,children:s+1},s))}),a.jsx(M,{name:"next-image",variant:"tertiary",size:1,onClick:Je,disabled:H,className:g.carouselButton,children:a.jsx(oe,{className:"w-4 h-4"})})]}),a.jsx("div",{className:g.viewImageButtonContainer,children:a.jsx(M,{name:"view-full-size-carousel",variant:"secondary",size:1,onClick:()=>ye(h[k]),disabled:H||!h[k]?.image_url,children:"View Image"})})]}):a.jsxs("div",{className:g.singleImageContainer,children:[c.detail_url?a.jsx("img",{src:c.detail_url,alt:c.file_key,onError:t=>{console.log("MapDetailsPage: Detail image failed to load, falling back to original:",c.detail_url);const s=t.target;c.image_url&&(s.src=c.image_url)},onLoad:()=>console.log("MapDetailsPage: Detail image loaded successfully:",c.detail_url)}):c.image_url?a.jsx("img",{src:c.image_url,alt:c.file_key,onLoad:()=>console.log("MapDetailsPage: Original image loaded successfully:",c.image_url)}):a.jsx("div",{className:g.imagePlaceholder,children:"No image available"}),a.jsx("div",{className:g.viewImageButtonContainer,children:a.jsx(M,{name:"view-full-size-single",variant:"secondary",size:1,onClick:()=>ye(c),disabled:!c.image_url,children:"View Image"})})]})}),a.jsx(E,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:a.jsxs("div",{className:g.metadataTags,children:[c.image_type!=="drone_image"&&a.jsx("span",{className:g.metadataTag,children:de.find(t=>t.s_code===c.source)?.label||c.source}),a.jsx("span",{className:g.metadataTag,children:ge.find(t=>t.t_code===c.event_type)?.label||c.event_type}),a.jsx("span",{className:g.metadataTag,children:me.find(t=>t.image_type===c.image_type)?.label||c.image_type}),c.countries&&c.countries.length>0&&a.jsxs(a.Fragment,{children:[a.jsx("span",{className:g.metadataTag,children:ue.find(t=>t.r_code===c.countries[0].r_code)?.label||"Unknown Region"}),a.jsx("span",{className:g.metadataTag,children:c.countries.map(t=>t.label).join(", ")})]}),c.image_count&&c.image_count>1&&a.jsxs("span",{className:g.metadataTag,title:`Multi-upload with ${c.image_count} images`,children:["π· ",c.image_count]}),(!c.image_count||c.image_count<=1)&&a.jsx("span",{className:g.metadataTag,title:"Single Upload",children:"Single"})]})})]}),a.jsx("div",{className:g.detailsSection,children:c.edited&&c.edited.includes("Description:")||c.generated&&c.generated.includes("Description:")?a.jsx(E,{heading:"AI Generated Content",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,spacing:"comfortable",children:a.jsx("div",{className:g.captionContainer,children:a.jsx("div",{className:g.captionText,children:(c.edited||c.generated||"").split(/(Description:|Analysis:|Recommended Actions:)/).map((l,i)=>l.trim()===""?null:l==="Description:"||l==="Analysis:"||l==="Recommended Actions:"?a.jsx("h4",{className:"font-semibold text-gray-800 mt-4 mb-2",children:l},i):a.jsx("p",{className:"mb-2",children:l.trim()},i))})})}):a.jsx(E,{heading:"Description",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,spacing:"comfortable",children:a.jsx("div",{className:g.captionContainer,children:c.generated||c.edited?a.jsx("div",{className:g.captionText,children:a.jsx("p",{children:c.edited||c.generated})}):a.jsx("p",{children:"β no caption yet β"})})})})]}),a.jsx("div",{className:"flex items-center justify-center mt-8",children:a.jsx(E,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-lg p-4",children:a.jsxs("div",{className:"flex items-center gap-4",children:[Le&&a.jsx(E,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(M,{name:"previous-item",variant:"tertiary",size:1,className:`bg-white/90 hover:bg-white shadow-lg border border-gray-200 ${W?"opacity-50 cursor-not-allowed":"hover:scale-110"}`,onClick:()=>ve("previous"),disabled:W,children:a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsxs("div",{className:"flex -space-x-1",children:[a.jsx(ne,{className:"w-4 h-4"}),a.jsx(ne,{className:"w-4 h-4"})]}),a.jsx("span",{className:"font-semibold",children:"Previous"})]})})}),re&&a.jsx(E,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(M,{name:"delete",variant:"tertiary",size:1,className:"bg-red-50 hover:bg-red-100 text-red-700 border border-red-200 hover:border-red-300",onClick:He,title:"Delete","aria-label":"Delete saved image",children:a.jsx(ia,{className:"w-4 h-4"})})}),a.jsx(E,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(M,{name:"contribute",onClick:Ze,children:"Contribute"})}),re&&a.jsx(E,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(M,{name:"toggle-star",variant:"tertiary",size:1,className:`${e?.starred?"bg-red-100 hover:bg-red-200 text-red-800 border-2 border-red-400":"bg-gray-100 hover:bg-gray-200 text-gray-600 border-2 border-gray-300"} w-16 h-8 rounded-full transition-all duration-200 flex items-center justify-center`,onClick:qe,title:e?.starred?"Unstar image":"Star image","aria-label":e?.starred?"Unstar image":"Star image",children:a.jsx("span",{className:`text-lg transition-all duration-200 ${e?.starred?"text-red-600":"text-gray-500"}`,children:e?.starred?"β
":"β"})})}),Fe&&a.jsx(E,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(M,{name:"next-item",variant:"tertiary",size:1,className:`bg-white/90 hover:bg-white shadow-lg border border-gray-200 ${W?"opacity-50 cursor-not-allowed":"hover:scale-110"}`,onClick:()=>ve("next"),disabled:W,children:a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx("span",{className:"font-semibold",children:"Next"}),a.jsxs("div",{className:"flex -space-x-1",children:[a.jsx(oe,{className:"w-4 h-4"}),a.jsx(oe,{className:"w-4 h-4"})]})]})})})]})})})]}):a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"text-xl font-semibold text-gray-600 mb-4",children:"No matches found"}),a.jsx("div",{className:"mt-4",children:a.jsx(M,{name:"clear-filters",variant:"secondary",onClick:Ue,children:"Clear Filters"})})]})}):null]}),Ee&&a.jsx("div",{className:g.fullSizeModalOverlay,onClick:()=>K(!1),children:a.jsx("div",{className:g.fullSizeModalContent,onClick:t=>t.stopPropagation(),children:a.jsxs("div",{className:g.ratingWarningContent,children:[a.jsx("h3",{className:g.ratingWarningTitle,children:"Delete Image?"}),a.jsx("p",{className:g.ratingWarningText,children:"This action cannot be undone. Are you sure you want to delete this saved image and all related data?"}),a.jsxs("div",{className:g.ratingWarningButtons,children:[a.jsx(M,{name:"confirm-delete",variant:"secondary",onClick:Ke,children:"Delete"}),a.jsx(M,{name:"cancel-delete",variant:"tertiary",onClick:()=>K(!1),children:"Cancel"})]})]})})}),pe&&a.jsx(ca,{isOpen:pe,onClose:()=>{Z(!1),se(!1),te(!1)},onExport:(t,s)=>{s.includes(e.image_type)&&Qe(t)},filteredCount:1,totalCount:1,hasFilters:!1,crisisMapsCount:e.image_type==="crisis_map"?1:0,droneImagesCount:e.image_type==="drone_image"?1:0,isLoading:$e,exportSuccess:Pe,variant:"single",onNavigateToList:()=>{Z(!1),x("/explore")},onNavigateAndExport:()=>{Z(!1),x("/explore?export=true")}}),a.jsx(na,{isOpen:Re,imageUrl:Ae?.image_url||null,preview:null,selectedImageData:null,onClose:Ge,isLoading:ze})]})}export{st as default};
         | 
    	
        py_backend/static/assets/{index-C4_t-iUv.js β index-D3L5y9N7.js}
    RENAMED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            import{r as s,y as nt,t as ht,a as q,c as gt,j as e,o as N,b as re,R as st,z,g as rt,d as ft,m as vt,e as pt,n as Z,A as xt,f as _t,h as Ct,i as yt,k as he,l as bt,p as ye,q as jt,s as Nt,E as wt,C as St,U as Mt,Q as It,u as Dt,N as je,_ as Et,L as Tt}from"./index-B0L3qvtx.js";const kt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("g",{clipPath:"url(#arrow-drop-down-line_svg__a)"},s.createElement("path",{d:"m12 15-4.243-4.243 1.415-1.414L12 12.172l2.828-2.83 1.415 1.415L12 15Z"})),s.createElement("defs",null,s.createElement("clipPath",{id:"arrow-drop-down-line_svg__a"},s.createElement("path",{d:"M0 0h24v24H0z"})))),Lt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("g",{clipPath:"url(#arrow-drop-up-line_svg__a)"},s.createElement("path",{d:"m12 11.828-2.828 2.829-1.415-1.414L12 9l4.243 4.243-1.415 1.414L12 11.828Z"})),s.createElement("defs",null,s.createElement("clipPath",{id:"arrow-drop-up-line_svg__a"},s.createElement("path",{d:"M0 0h24v24H0z"})))),Pt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("g",{clipPath:"url(#information-line_svg__a)"},s.createElement("path",{d:"M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10Zm0-2a8 8 0 1 0 0-16.001A8 8 0 0 0 12 20ZM11 7h2v2h-2V7Zm0 4h2v6h-2v-6Z"})),s.createElement("defs",null,s.createElement("clipPath",{id:"information-line_svg__a"},s.createElement("path",{d:"M0 0h24v24H0z"})))),Rt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("path",{fillRule:"evenodd",d:"m15.063 12 .937.938-4 4-4-4L8.938 12 12 15.063 15.063 12Z",clipRule:"evenodd"}),s.createElement("mask",{id:"table-sorting-line_svg__a",width:8,height:5,x:8,y:12,maskUnits:"userSpaceOnUse",style:{maskType:"luminance"}},s.createElement("path",{fillRule:"evenodd",d:"m15.063 12 .937.938-4 4-4-4L8.938 12 12 15.063 15.063 12Z",clipRule:"evenodd"})),s.createElement("g",{mask:"url(#table-sorting-line_svg__a)"},s.createElement("path",{d:"M-24-22h72v72h-72z"})),s.createElement("path",{fillRule:"evenodd",d:"M8.938 11 8 10.062l4-4 4 4-.938.938L12 7.937 8.937 11Z",clipRule:"evenodd"}),s.createElement("mask",{id:"table-sorting-line_svg__b",width:8,height:5,x:8,y:6,maskUnits:"userSpaceOnUse",style:{maskType:"luminance"}},s.createElement("path",{fillRule:"evenodd",d:"M8.938 11 8 10.062l4-4 4 4-.938.938L12 7.937 8.937 11Z",clipRule:"evenodd"})),s.createElement("g",{mask:"url(#table-sorting-line_svg__b)"},s.createElement("path",{d:"M48 45h-72v-72h72z"}))),$t="_number-output_1blvi_1",Bt={numberOutput:$t};function we(m){const{className:a,invalidText:h=nt,separatorHidden:r,compact:f,currency:g,value:b,tooltip:_,unit:y,prefix:x,suffix:w,maximumFractionDigits:C=1}=m,{currentLanguage:j}=s.useContext(ht),v=s.useMemo(()=>{if(q(b))return h;const S=gt(b,{currency:g,compact:f,separatorHidden:r,maximumFractionDigits:C,unit:y,language:j});return e.jsxs(e.Fragment,{children:[x,S,w]})},[h,b,f,r,g,y,C,x,j,w]);return e.jsx("div",{className:N(Bt.numberOutput,a),title:re(_)?String(_):void 0,children:v})}const At="_tooltip-dummy_rbf3f_1",Ft="_tooltip-content_rbf3f_7",Ot="_pointer_rbf3f_14",Ne={tooltipDummy:At,tooltipContent:Ft,pointer:Ot};function Ht(m){const{className:a,title:h,description:r,preferredWidth:f}=m,[g,b]=s.useState(!1),[_,y]=s.useState(!1),x=s.useRef(),w=s.useRef(null);return s.useEffect(()=>{const C=()=>{y(!0)},j=()=>{y(!1)};if(q(w.current))return;const{current:{parentNode:v}}=w;if(!q(v))return x.current=v,v.addEventListener("mouseover",C),v.addEventListener("mouseout",j),b(!0),()=>{v.removeEventListener("mouseover",C),v.removeEventListener("mouseout",j)}},[]),e.jsxs(e.Fragment,{children:[!g&&e.jsx("div",{className:Ne.tooltipDummy,ref:w}),_&&e.jsx(st,{className:N(Ne.tooltipContent,a),parentRef:x,pointerClassName:Ne.pointer,preferredWidth:f,children:e.jsx(z,{heading:h,withInternalPadding:!0,contentViewType:"vertical",children:r})})]})}function $(m){return m.id}const Vt="common",zt={booleanYesLabel:"Yes",booleanNoLabel:"No"},Ut={namespace:Vt,strings:zt},Wt="_boolean-output_kg1uq_1",Qt={booleanOutput:Wt};function Zt(m){const{className:a,invalidText:h,value:r}=m,f=rt(Ut);let g;return r===!0?g=f.booleanYesLabel:r===!1?g=f.booleanNoLabel:g=h,e.jsx("div",{className:N(Qt.booleanOutput,a),children:g})}const qt="_date-output_4jzjo_1",Gt={dateOutput:qt};function Yt(m){const{value:a,format:h,className:r,invalidText:f}=m,g=s.useMemo(()=>ft(a,h),[a,h]);return e.jsx("div",{className:N(Gt.dateOutput,r),children:g??f})}const Kt="_dropdown-menu_16hml_1",Xt="_icons_16hml_4",Jt="_content_16hml_5",ea="_actions_16hml_6",ta="_dropdown-icon_16hml_10",aa="_dropdown-content_16hml_16",le={dropdownMenu:Kt,icons:Xt,content:Jt,actions:ea,dropdownIcon:ta,dropdownContent:aa};function na(m){const a=s.useRef(null),{className:h,popupClassName:r,children:f,label:g,activeClassName:b,icons:_,variant:y="secondary",actions:x,withoutDropdownIcon:w,componentRef:C,elementRef:j=a,persistent:v,preferredPopupWidth:S}=m,p=s.useRef(null),[k,I]=s.useState(!1);s.useEffect(()=>{C&&(C.current={setShowDropdown:I})},[C,I]);const R=s.useCallback(()=>{I(ie=>!ie)},[I]),T=s.useCallback((ie,ae)=>{ae||ie&&v||I(!1)},[I,v]);vt(k,T,p,j);const G=s.useMemo(()=>({setShowDropdown:I}),[I]),U=!!x||!w;return e.jsxs(pt.Provider,{value:G,children:[e.jsx(Z,{name:void 0,className:N(le.dropdownMenu,k&&b,h),elementRef:j,onClick:R,variant:y,actionsContainerClassName:le.actions,iconsContainerClassName:le.icons,childrenContainerClassName:le.content,actions:U?e.jsxs(e.Fragment,{children:[x,!w&&(k?e.jsx(xt,{className:le.dropdownIcon}):e.jsx(_t,{className:le.dropdownIcon}))]}):void 0,icons:_,children:g}),k&&e.jsx(st,{elementRef:p,className:N(le.dropdownContent,r),parentRef:j,preferredWidth:S,children:f})]})}const sa="_info-popup_i3rna_1",ra="_label_i3rna_2",ia="_icon_i3rna_7",oa="_dropdown-container_i3rna_15",la="_content_i3rna_20",ge={infoPopup:sa,label:ra,icon:ia,dropdownContainer:oa,content:la};function ca(m){const{className:a,icon:h=e.jsx(Pt,{}),infoLabel:r,title:f,description:g,withoutIcon:b,popupClassName:_,descriptionClassName:y}=m;return e.jsx(na,{label:e.jsxs("div",{className:ge.label,children:[r,!b&&h&&e.jsx("div",{className:ge.icon,children:h})]}),popupClassName:N(ge.dropdownContainer,_),className:N(ge.infoPopup,a),variant:"tertiary",withoutDropdownIcon:!0,children:e.jsx(z,{heading:f,childrenContainerClassName:N(y,ge.content),withInternalPadding:!0,children:g})})}const da="_progress-wrapper_x340w_1",ua="_title_x340w_7",ma="_total_x340w_11",ha="_progress_x340w_1",fe={progressWrapper:da,title:ua,total:ma,progress:ha};function Ye(m){const{className:a,title:h,description:r,totalValue:f,value:g,showPercentageInTitle:b,children:_,color:y="var(--go-ui-color-primary-red)"}=m,x=re(g)?g:0,w=re(f)?f:0;let C;return w===0?C=0:C=x/w*100,e.jsxs("div",{className:N(fe.progressWrapper,a),children:[(h||b)&&e.jsxs("div",{className:fe.title,children:[h,b&&e.jsx(we,{value:C,suffix:"%"})]}),e.jsx("div",{className:fe.total,children:e.jsx("div",{className:fe.progress,style:{width:`${C}%`,backgroundColor:y}})}),r&&e.jsx("div",{className:fe.description,children:r}),_]})}const ga="_legend-element_1a9ic_1",fa="_color_1a9ic_7",va="_icon-container_1a9ic_14",pa="_icon_1a9ic_14",xa="_label_1a9ic_31",ve={legendElement:ga,color:fa,iconContainer:va,icon:pa,label:xa};function _a(m){const{className:a,colorClassName:h,iconClassName:r,color:f,label:g,iconSrc:b}=m;return e.jsxs("div",{className:N(ve.legendElement,a),children:[b?e.jsx("div",{style:{backgroundColor:f},className:ve.iconContainer,children:e.jsx("img",{className:N(ve.icon,r),src:b,alt:""})}):e.jsx("div",{style:{backgroundColor:f},className:N(ve.color,h)}),e.jsx("div",{className:ve.label,children:g})]})}const Ca="_text-output_10oza_1",ya="_with-background_10oza_6",ba="_label_10oza_11",ja="_with-colon_10oza_12",Na="_value_10oza_17",wa="_text-type_10oza_18",Sa="_strong_10oza_24",ee={textOutput:Ca,withBackground:ya,label:ba,withColon:ja,value:Na,textType:wa,strong:Sa};function Ke(m){const{className:a,label:h,icon:r,description:f,labelClassName:g,descriptionClassName:b,valueClassName:_,strongLabel:y,strongValue:x,strongDescription:w,withoutLabelColon:C,withBackground:j,invalidText:v=nt,...S}=m,{value:p}=m;let k=v;return S.valueType==="number"?k=e.jsx(we,{...S,invalidText:v}):S.valueType==="date"?k=e.jsx(Yt,{...S,invalidText:v}):S.valueType==="boolean"?k=e.jsx(Zt,{...S,invalidText:v}):p instanceof Date||(k=p||v),e.jsxs("div",{className:N(ee.textOutput,j&&ee.withBackground,a),children:[r,h&&e.jsx("div",{className:N(ee.label,y&&ee.strong,g,!C&&ee.withColon),children:h}),e.jsx("div",{className:N(ee.value,x&&ee.strong,S.valueType==="text"&&ee.textType,_),children:k}),f&&e.jsx("div",{className:N(ee.description,w&&ee.strong,b),children:f})]})}const Ma="_pie-chart_pyr7m_1",Ia="_legend_pyr7m_7",Da="_legend-item_pyr7m_13",pe={pieChart:Ma,legend:Ia,legendItem:Da},Ea=70,Ta=40;function Xe(m,a=1){return Math.round(m*10**a)/10**a}function Je(m,a){const h=(a-90)*Math.PI/180;return{x:Xe(m+m*Math.cos(h)),y:Xe(m+m*Math.sin(h))}}function ka(m,a,h){let r=h;const f=r-a===360;f&&(r-=1);const g=Je(m,a),b=Je(m,r),_=r-a<=180?0:1,y=["M",g.x,g.y,"A",m,m,0,_,1,b.x,b.y];return f?y.push("Z"):y.push("L",m,m,"L",g.x,g.y,"Z"),y.join(" ")}function xe(m){const{className:a,data:h,valueSelector:r,labelSelector:f,keySelector:g,colorSelector:b,colors:_,pieRadius:y=Ea,chartPadding:x=Ta,legendClassName:w,showPercentageInLegend:C}=m,j=Ct(h?.map(p=>r(p))),v=q(j)||j===0?1:j,S=s.useMemo(()=>{let p=0;const k=h?.map(I=>{const R=r(I);if(q(R))return;const T=360*(R/v);return p+=T,{key:g(I),value:R,label:f(I),startAngle:p-T,percentage:yt(R,v),endAngle:p,datum:I}}).filter(re)??[];return b?k.map(({datum:I,...R})=>({...R,color:b(I)})):k.map(({datum:I,...R},T)=>({...R,color:_[T%_.length]}))},[h,g,r,f,v,b,_]);return e.jsxs("div",{className:N(pe.pieChart,a),children:[e.jsx("svg",{className:pe.svg,style:{width:`${x+y*2}px`,height:`${x+y*2}px`},children:e.jsx("g",{style:{transform:`translate(${x/2}px, ${x/2}px)`},children:S.map(p=>e.jsx("path",{className:pe.path,d:ka(y,p.startAngle,p.endAngle),fill:p.color,children:e.jsx(Ht,{description:e.jsx(Ke,{label:p.label,value:p.value})})},p.key))})}),e.jsx("div",{className:N(pe.legend,w),children:S.map(p=>e.jsx(_a,{className:pe.legendItem,label:C?e.jsx(Ke,{label:p.label,value:p.percentage,valueType:"number",prefix:"(",suffix:"%)",withoutLabelColon:!0}):p.label,color:p.color},p.key))})]})}const La="_td_1k4cn_1",Pa={td:La};function Ra(m){const{className:a,children:h,...r}=m;return e.jsx("td",{className:N(a,Pa.td),...r,children:h})}function it(m){const{className:a,children:h,...r}=m;return e.jsx("tr",{className:a,...r,children:h})}const $a="_row_1829z_1",Ba="_cell_1829z_2",et={row:$a,cell:Ba};function Aa(m){const{data:a,keySelector:h,columns:r,rowClassName:f,cellClassName:g,rowModifier:b}=m;return e.jsx(e.Fragment,{children:a?.map((_,y)=>{const x=h(_,y),w=r.map(v=>{const{id:S,cellRenderer:p,cellRendererClassName:k,cellRendererParams:I,cellContainerClassName:R}=v,T=I(x,_,y,a),G=e.jsx(p,{className:k,...T,name:S});return e.jsx(Ra,{className:N(et.cell,R,typeof g=="function"?g(x,_,S):g),children:G},S)}),C=e.jsx(it,{className:N(et.row,typeof f=="function"?f(x,_):f),children:w});let j=C;return b&&(j=b({rowKey:x,row:C,cells:w,columns:r,datum:_})),e.jsx(s.Fragment,{children:j},x)})})}const Fa="_th_cdv41_1",Oa="_resize-handle_cdv41_8",tt={th:Fa,resizeHandle:Oa};function Ha(m){const{className:a,children:h,onResize:r,onResizeComplete:f,name:g,...b}=m,_=s.useRef(null),y=s.useRef(),x=s.useRef(),w=s.useRef(),C=s.useCallback(v=>{var S;if(re(y.current)&&_.current&&r){v.preventDefault(),v.stopPropagation();const p=v.clientX-y.current;if(re(x.current)){const k=x.current+p;w.current=k,r(k,g)}else x.current=(S=_.current)==null?void 0:S.offsetWidth}},[r,g]),j=s.useCallback(v=>{var S;v.preventDefault(),y.current=v.clientX,x.current=(S=_.current)==null?void 0:S.offsetWidth,window.addEventListener("mousemove",C,!0)},[C]);return s.useEffect(()=>{const v=()=>{y.current=void 0,x.current=void 0,f&&re(w.current)&&f(w.current,g),window.removeEventListener("mousemove",C,!0)};return window.addEventListener("mouseup",v,!0),()=>{window.removeEventListener("mouseup",v,!0),window.removeEventListener("mousemove",C,!0)}},[C,g,f]),e.jsxs("th",{ref:_,className:N(a,tt.th),...b,children:[r&&e.jsx("div",{role:"presentation",className:tt.resizeHandle,onMouseDown:j}),h]})}const Va="_table_nilhy_1",za="_table-overflow-wrapper_nilhy_8",Ua="_table-element_nilhy_13",Wa="_header-row_nilhy_23",Qa="_header-element_nilhy_24",Za="_header-component_nilhy_29",ce={table:Va,tableOverflowWrapper:za,tableElement:Ua,headerRow:Wa,headerElement:Qa,headerComponent:Za};function qa(m,a){return a??m.columnWidth??wt}function B(m){const{data:a,keySelector:h,columns:r,caption:f,className:g,captionClassName:b,headerRowClassName:_,headerCellClassName:y,rowClassName:x,cellClassName:w,rowModifier:C,fixedColumnWidth:j,resizableColumn:v,headersHidden:S,pending:p,filtered:k,errored:I=!1}=m,R=s.useRef(null),[T]=he.useState(()=>bt()),[G,U]=he.useState({});s.useEffect(()=>{U(W=>{if(q(R.current))return W;const E=R.current.getBoundingClientRect(),{width:O}=E;let A=r.map(M=>({id:M.id,stretch:!!M.columnStretch,width:qa(M,W[M.id])}));const X=ye(A.filter(M=>M.stretch).map(M=>M.width)),ne=ye(A.filter(M=>!M.stretch).map(M=>M.width)),Y=(O-ne)/X;return Y>1&&(A=A.map(M=>({...M,width:M.stretch?M.width*Y:M.width}))),jt(A,M=>M.id,M=>M.width)})},[r]);const ie=he.useCallback((W,E)=>{const O=document.getElementById(`${T}-${E}`),A=Math.max(W,80);if(q(O)||(O.style.width=`${A}px`,!j))return;const X=document.getElementById(T);if(q(X))return;const ne=ye(r.map(Y=>Y.id===E?A:G[Y.id]));X.style.width=`${ne}px`},[T,G,r,j]),ae=he.useCallback((W,E)=>{re(E)&&U(O=>({...O,[E]:Math.max(W,80)}))},[U]),be=he.useMemo(()=>ye(r.map(W=>G[W.id])),[G,r]),K=q(a)||a.length===0||Object.keys(G).length===0;return e.jsxs("div",{ref:R,className:N(ce.table,g),children:[!K&&e.jsx("div",{className:ce.tableOverflowWrapper,children:e.jsxs("table",{className:ce.tableElement,style:j?{width:`${be}px`}:void 0,id:T,children:[f&&e.jsx("caption",{className:b,children:f}),e.jsx("colgroup",{children:r.map(W=>{const{id:E,columnClassName:O}=W,A=G[E],X=j?{width:`${A}px`}:void 0;return e.jsx("col",{id:`${T}-${E}`,style:X,className:N(ce.column,O)},E)})}),!S&&e.jsx("thead",{children:e.jsx(it,{className:N(ce.headerRow,_),children:r.map((W,E)=>{const{id:O,title:A,headerCellRenderer:X,headerCellRendererClassName:ne,headerCellRendererParams:Y,headerContainerClassName:M}=W,se=e.jsx(X,{...Y,name:O,title:A,index:E,className:N(ne,ce.headerComponent)});return e.jsx(Ha,{scope:"col",name:O,onResize:v?ie:void 0,onResizeComplete:v?ae:void 0,className:N(ce.headerElement,typeof y=="function"?y(O):y,M),children:se},O)})})}),e.jsx("tbody",{children:e.jsx(Aa,{data:a,keySelector:h,columns:r,rowClassName:x,cellClassName:w,rowModifier:C})})]})}),e.jsx(Nt,{filtered:k,empty:K,errored:I,pending:p,overlayPending:!0})]})}function Ga(m){const{className:a,value:h}=m;return q(h)?null:e.jsx("div",{className:a,children:h})}const Ya="common",Ka={sortTableButtonTitle:"Sort Table"},Xa={namespace:Ya,strings:Ka},Ja="_header-cell_vn24d_1",en="_sort-button_vn24d_8",tn="_icon_vn24d_12",an="_info-popup-icon_vn24d_17",de={headerCell:Ja,sortButton:en,icon:tn,infoPopupIcon:an};function ot(m){const{className:a,titleClassName:h,title:r,name:f,sortable:g,defaultSortDirection:b="asc",infoTitle:_,infoDescription:y}=m,{sorting:x,setSorting:w}=s.useContext(St),C=rt(Xa),j=x?.name===f?x.direction:void 0,v=s.useRef(null),S=s.useCallback(()=>{if(q(w))return;let p;q(j)?p=b:j==="asc"?p="dsc":j==="dsc"&&(p="asc"),w(p?{name:f,direction:p}:void 0)},[f,w,j,b]);return e.jsxs("div",{ref:v,className:N(a,de.headerCell),children:[g&&e.jsxs(Z,{name:void 0,variant:"tertiary",onClick:S,title:C.sortTableButtonTitle,className:de.sortButton,children:[q(j)&&e.jsx(Rt,{className:de.icon}),j==="asc"&&e.jsx(Lt,{className:de.icon}),j==="dsc"&&e.jsx(kt,{className:de.icon})]}),e.jsx("div",{className:N(h,de.title),children:r}),_&&y&&e.jsx(ca,{className:de.infoPopupIcon,title:_,description:y})]})}const at={};function Q(m,a,h,r){return{id:m,title:a,columnClassName:r?.columnClassName,headerCellRenderer:ot,headerCellRendererClassName:r?.headerCellRendererClassName,headerContainerClassName:r?.headerContainerClassName,headerCellRendererParams:{sortable:r?.sortable,infoTitle:r?.headerInfoTitle,infoDescription:r?.headerInfoDescription},cellRendererClassName:r?.cellRendererClassName,cellContainerClassName:r?.cellContainerClassName,cellRenderer:Ga,cellRendererParams:(f,g)=>({value:h(g)||"--"}),valueSelector:h,valueComparator:(f,g)=>Mt(h(f),h(g)),columnWidth:r?.columnWidth,columnStretch:r?.columnStretch,columnStyle:r?.columnStyle}}function D(m,a,h,r){return{id:m,title:a,columnClassName:r?.columnClassName,headerCellRenderer:ot,headerCellRendererClassName:N(at.numberCellHeader,r?.headerCellRendererClassName),headerContainerClassName:r?.headerContainerClassName,headerCellRendererParams:{sortable:r?.sortable,infoTitle:r?.headerInfoTitle,infoDescription:r?.headerInfoDescription},cellRendererClassName:N(at.numberCell,r?.cellRendererClassName),cellContainerClassName:r?.cellContainerClassName,cellRenderer:we,cellRendererParams:(f,g)=>({value:h(g),suffix:r?.suffix,maximumFractionDigits:r?.maximumFractionDigits,invalidText:"--"}),valueSelector:h,valueComparator:(f,g)=>It(h(f),h(g)),columnWidth:r?.columnWidth,columnStretch:r?.columnStretch,columnStyle:r?.columnStyle}}const nn="_tabSelector_vlxoe_1",sn="_progressSection_vlxoe_14",rn="_progressLabel_vlxoe_20",on="_chartGrid_vlxoe_28",ln="_chartContainer_vlxoe_40",cn="_tableContainer_vlxoe_51",dn="_modelPerformance_vlxoe_59",un="_loadingContainer_vlxoe_67",mn="_errorContainer_vlxoe_77",hn="_userInteractionCards_vlxoe_96",gn="_userInteractionCard_vlxoe_96",fn="_userInteractionCardValue_vlxoe_116",vn="_userInteractionCardLabel_vlxoe_123",pn="_userInteractionCardButton_vlxoe_130",xn="_summaryStatsCards_vlxoe_148",_n="_summaryStatsCard_vlxoe_148",Cn="_summaryStatsCardValue_vlxoe_169",yn="_summaryStatsCardLabel_vlxoe_176",c={tabSelector:nn,progressSection:sn,progressLabel:rn,chartGrid:on,chartContainer:ln,tableContainer:cn,modelPerformance:dn,loadingContainer:un,errorContainer:mn,userInteractionCards:hn,userInteractionCard:gn,userInteractionCardValue:fn,userInteractionCardLabel:vn,userInteractionCardButton:pn,summaryStatsCards:xn,summaryStatsCard:_n,summaryStatsCardValue:Cn,summaryStatsCardLabel:yn};function jn(){const[m]=Dt(),[a,h]=s.useState(null),[r,f]=s.useState(!0),[g,b]=s.useState("crisis_maps"),[_,y]=s.useState([]),[x,w]=s.useState([]),[C,j]=s.useState([]),[v,S]=s.useState([]),[p,k]=s.useState(!1),[I,R]=s.useState(!1),[T,G]=s.useState(!1),[U,ie]=s.useState(!1),[ae,be]=s.useState(!1),[K,W]=s.useState(!1),E=t=>{k(t==="editTime"),R(t==="percentage"),G(t==="delete"),ie(t==="regions"),be(t==="sources"),W(t==="types")},O=[{key:"crisis_maps",label:"Crisis Maps"},{key:"drone_images",label:"Drone Images"}],A=s.useCallback((t,l)=>{if(!t||!l)return 0;const i=t.toLowerCase().replace(/[^\w\s]/g,"").split(/\s+/).filter(u=>u.length>0),n=l.toLowerCase().replace(/[^\w\s]/g,"").split(/\s+/).filter(u=>u.length>0);if(i.length===0&&n.length===0)return 1;if(i.length===0||n.length===0)return 0;const o=new Set(i),d=new Set(n),P=new Set([...o].filter(u=>d.has(u))),F=new Set([...o,...d]);return P.size/F.size},[]),X=s.useCallback(async()=>{f(!0);try{const l=await(await fetch("/api/images")).json(),i={},n=l.filter(u=>u.image_type==="crisis_map"),o=l.filter(u=>u.image_type==="drone_image"),d={totalCaptions:l.length,sources:{},types:{},regions:{},models:{},modelEditTimes:i,percentageModified:0,modelPercentageData:{},totalDeleteCount:0,deleteRate:0,crisisMaps:n,droneImages:o};l.forEach(u=>{if(u.source&&(d.sources[u.source]=(d.sources[u.source]||0)+1),u.event_type&&(d.types[u.event_type]=(d.types[u.event_type]||0)+1),u.countries&&u.countries.forEach(L=>{L.r_code&&(d.regions[L.r_code]=(d.regions[L.r_code]||0)+1)}),u.model){const L=u.model,V=d.models[L]||={count:0,avgAccuracy:0,avgContext:0,avgUsability:0,totalScore:0,deleteCount:0};if(V.count++,u.accuracy!=null&&(V.avgAccuracy+=u.accuracy),u.context!=null&&(V.avgContext+=u.context),u.usability!=null&&(V.avgUsability+=u.usability),u.created_at&&u.updated_at){const te=new Date(u.created_at).getTime(),Ce=new Date(u.updated_at).getTime()-te;Ce>0&&(i[L]||(i[L]=[]),i[L].push(Ce))}}}),_.forEach(u=>{u.s_code&&!d.sources[u.s_code]&&(d.sources[u.s_code]=0)}),x.forEach(u=>{u.t_code&&!d.types[u.t_code]&&(d.types[u.t_code]=0)}),C.forEach(u=>{u.r_code&&!d.regions[u.r_code]&&(d.regions[u.r_code]=0)}),["GPT-4","Claude","Gemini","Llama","Other"].forEach(u=>{d.models[u]||(d.models[u]={count:0,avgAccuracy:0,avgContext:0,avgUsability:0,totalScore:0,deleteCount:0})}),Object.values(d.models).forEach(u=>{u.count>0&&(u.avgAccuracy=Math.round(u.avgAccuracy/u.count),u.avgContext=Math.round(u.avgContext/u.count),u.avgUsability=Math.round(u.avgUsability/u.count),u.totalScore=Math.round((u.avgAccuracy+u.avgContext+u.avgUsability)/3))});const F=l.filter(u=>u.generated&&u.edited);if(F.length>0){const L=[...F.map(oe=>A(oe.generated,oe.edited))].sort((oe,Ce)=>oe-Ce),V=Math.floor(L.length/2),te=L.length%2===0?(L[V-1]+L[V])/2:L[V];d.percentageModified=Math.round((1-te)*100)}const H={};l.forEach(u=>{if(u.model&&u.generated&&u.edited){const L=A(u.generated,u.edited),V=Math.round((1-L)*100);H[u.model]||(H[u.model]=[]),H[u.model].push(V)}}),d.modelPercentageData=H;try{const u=await fetch("/api/models");if(u.ok){const L=await u.json();if(L.models){L.models.forEach(te=>{d.models[te.m_code]&&(d.models[te.m_code].deleteCount=te.delete_count||0)});const V=L.models.reduce((te,oe)=>te+(oe.delete_count||0),0);d.totalDeleteCount=V,d.deleteRate=V>0?Math.round(V/(V+l.length)*100):0}}}catch(u){console.log("Could not fetch model delete counts:",u)}h(d)}catch{h(null)}finally{f(!1)}},[_,x,C,A]),ne=s.useCallback(async()=>{try{const[t,l,i,n]=await Promise.all([fetch("/api/sources"),fetch("/api/types"),fetch("/api/regions"),fetch("/api/models")]),o=await t.json(),d=await l.json(),P=await i.json(),F=await n.json();y(o),w(d),j(P),S(F.models||[])}catch(t){console.log("Could not fetch lookup data:",t)}},[]);s.useEffect(()=>{const t=m.get("view");(t==="crisis_maps"||t==="drone_images")&&b(t)},[m]),s.useEffect(()=>{ne()},[ne]),s.useEffect(()=>{_.length>0&&x.length>0&&C.length>0&&v.length>0&&X()},[_,x,C,v,X]);const Y=s.useCallback(t=>{const l=_.find(i=>i.s_code===t);return l?l.label:t},[_]),M=s.useCallback(t=>{if(t.length===0)return 0;const l=[...t].sort((n,o)=>n-o),i=Math.floor(l.length/2);return l.length%2===0?Math.round((l[i-1]+l[i])/2):l[i]},[]),se=s.useCallback(t=>{const l=Math.floor(t/1e3),i=Math.floor(l/60),n=Math.floor(i/60);return n>0?`${n}h ${i%60}m`:i>0?`${i}m ${l%60}s`:`${l}s`},[]),_e=s.useCallback(t=>{const l=x.find(i=>i.t_code===t);return l?l.label:t},[x]),J=s.useCallback(t=>{const l=v.find(i=>i.m_code===t);return l?l.label:t},[v]),Se=s.useMemo(()=>a?Object.entries(a.modelEditTimes||{}).filter(([,t])=>t.length>0).sort(([,t],[,l])=>M(l)-M(t)).map(([t,l],i)=>({id:i+1,name:J(t),count:l.length,avgEditTime:M(l),minEditTime:Math.min(...l),maxEditTime:Math.max(...l)})):[],[a,M,J]),Me=s.useMemo(()=>a?Object.entries(a.modelPercentageData||{}).filter(([,t])=>t.length>0).sort(([,t],[,l])=>{const i=[...t].sort((H,u)=>H-u),n=[...l].sort((H,u)=>H-u),o=Math.floor(i.length/2),d=Math.floor(n.length/2),P=i.length%2===0?(i[o-1]+i[o])/2:i[o];return(n.length%2===0?(n[d-1]+n[d])/2:n[d])-P}).map(([t,l],i)=>{const n=[...l].sort((P,F)=>P-F),o=Math.floor(n.length/2),d=n.length%2===0?Math.round((n[o-1]+n[o])/2):n[o];return{id:i+1,name:J(t),count:l.length,avgPercentageModified:d,minPercentageModified:Math.min(...l),maxPercentageModified:Math.max(...l)}}):[],[a,J]),Ie=s.useMemo(()=>a?Object.entries(a.models).filter(([,t])=>t.count>0).map(([t,l],i)=>{const n=[l.avgAccuracy,l.avgContext,l.avgUsability],o=n.reduce((F,H)=>F+H,0)/n.length,d=n.reduce((F,H)=>F+Math.pow(H-o,2),0)/n.length,P=Math.round(100-Math.sqrt(d));return{id:i+1,name:J(t),consistency:Math.max(0,P),avgScore:Math.round(o),count:l.count}}).sort((t,l)=>l.consistency-t.consistency):[],[a,J]),De=s.useMemo(()=>[Q("name","Region",t=>t.name),D("count","Count",t=>t.count),D("percentage","% of Total",t=>t.percentage,{suffix:"%",maximumFractionDigits:0})],[]),Ee=s.useMemo(()=>[Q("name","Type",t=>t.name),D("count","Count",t=>t.count),D("percentage","% of Total",t=>t.percentage,{suffix:"%",maximumFractionDigits:0})],[]),lt=s.useMemo(()=>[Q("name","Source",t=>t.name),D("count","Count",t=>t.count),D("percentage","% of Total",t=>t.percentage,{suffix:"%",maximumFractionDigits:0})],[]),Te=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Count",t=>t.count),D("accuracy","Accuracy",t=>t.accuracy,{suffix:"%",maximumFractionDigits:0}),D("context","Context",t=>t.context,{suffix:"%",maximumFractionDigits:0}),D("usability","Usability",t=>t.usability,{suffix:"%",maximumFractionDigits:0}),D("totalScore","Total Score",t=>t.totalScore,{suffix:"%",maximumFractionDigits:0})],[]),ke=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Count",t=>t.count),Q("avgEditTime","Median Edit Time",t=>se(t.avgEditTime)),Q("minEditTime","Min Edit Time",t=>se(t.minEditTime)),Q("maxEditTime","Max Edit Time",t=>se(t.maxEditTime))],[se]),Le=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Count",t=>t.count),D("avgPercentageModified","Median % Modified",t=>t.avgPercentageModified,{suffix:"%",maximumFractionDigits:0}),D("minPercentageModified","Min % Modified",t=>t.minPercentageModified,{suffix:"%",maximumFractionDigits:0}),D("maxPercentageModified","Max % Modified",t=>t.maxPercentageModified,{suffix:"%",maximumFractionDigits:0})],[]),Pe=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Total Count",t=>t.count),D("deleteCount","Delete Count",t=>t.deleteCount),D("deleteRate","Delete Rate",t=>t.deleteRate,{suffix:"%",maximumFractionDigits:1})],[]),ct=s.useMemo(()=>[Q("source","Source",t=>t.source),D("avgQuality","Average Quality",t=>t.avgQuality,{suffix:"%",maximumFractionDigits:0}),D("count","Count",t=>t.count)],[]),Re=s.useMemo(()=>[Q("eventType","Event Type",t=>t.eventType),D("avgQuality","Average Quality",t=>t.avgQuality,{suffix:"%",maximumFractionDigits:0}),D("count","Count",t=>t.count)],[]),$e=s.useMemo(()=>[Q("name","Model",t=>t.name),D("consistency","Consistency",t=>t.consistency,{suffix:"%",maximumFractionDigits:0}),D("avgScore","Average Score",t=>t.avgScore,{suffix:"%",maximumFractionDigits:0}),D("count","Count",t=>t.count)],[]),ue=s.useCallback(t=>a?t==="crisis_map"?a.crisisMaps.length:t==="drone_image"?a.droneImages.length:0:0,[a]),Be=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.countries&&n.countries.forEach(o=>{o.r_code&&(i[o.r_code]=(i[o.r_code]||0)+1)})}),Object.entries(i).filter(([,n])=>n>0).map(([n,o])=>({name:C.find(d=>d.r_code===n)?.label||n,value:o}))},[a,C]),Ae=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};l.forEach(o=>{o.countries&&o.countries.forEach(d=>{d.r_code&&(i[d.r_code]=(i[d.r_code]||0)+1)})});const n=C.reduce((o,d)=>(d.r_code&&(o[d.r_code]={name:d.label,count:i[d.r_code]||0}),o),{});return Object.entries(n).sort(([,o],[,d])=>d.count-o.count).map(([,{name:o,count:d}],P)=>({id:P+1,name:o,count:d,percentage:l.length>0?Math.round(d/l.length*100):0}))},[a,C]),dt=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.source&&(i[n.source]=(i[n.source]||0)+1)}),Object.entries(i).filter(([,n])=>n>0).map(([n,o])=>({name:_.find(d=>d.s_code===n)?.label||n,value:o}))},[a,_]),ut=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.source&&(i[n.source]=(i[n.source]||0)+1)}),Object.entries(i).sort(([,n],[,o])=>o-n).map(([n,o],d)=>({id:d+1,name:Y(n),count:o,percentage:l.length>0?Math.round(o/l.length*100):0}))},[a,Y]),Fe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.event_type&&(i[n.event_type]=(i[n.event_type]||0)+1)}),Object.entries(i).filter(([,n])=>n>0).map(([n,o])=>({name:x.find(d=>d.t_code===n)?.label||n,value:o}))},[a,x]),Oe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.event_type&&(i[n.event_type]=(i[n.event_type]||0)+1)}),Object.entries(i).sort(([,n],[,o])=>o-n).map(([n,o],d)=>({id:d+1,name:_e(n),count:o,percentage:l.length>0?Math.round(o/l.length*100):0}))},[a,_e]),He=s.useCallback(t=>{if(!a)return"No data available";const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;l.forEach(d=>{d.model&&i.add(d.model)}),console.log(`Debug ${t}:`,{totalImages:l.length,usedModels:Array.from(i),availableEditTimes:Object.keys(a.modelEditTimes),modelEditTimesData:a.modelEditTimes});const o=Object.entries(a.modelEditTimes).filter(([d])=>i.has(d)).flatMap(([,d])=>d);return o.length===0?"No data available":se(M(o))},[a,se,M]),Ve=s.useCallback(()=>{if(!a)return"No data available";const t=a.totalCaptions||0,l=a.percentageModified||0;return t>0?Math.round(l/t*100):0},[a]),ze=s.useCallback(()=>a&&a.deleteRate>=0?`${a.deleteRate}%`:"No data available",[a]),Ue=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;return l.forEach(o=>{o.model&&i.add(o.model)}),Se.filter(o=>{const d=v.find(P=>P.label===o.name)?.m_code;return d&&i.has(d)})},[a,Se,v]),We=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;return l.forEach(o=>{o.model&&i.add(o.model)}),Me.filter(o=>{const d=v.find(P=>P.label===o.name)?.m_code;return d&&i.has(d)})},[a,Me,v]),Qe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.model&&(i[n.model]||(i[n.model]={count:0,deleteCount:0}),i[n.model].count++)}),Object.entries(i).map(([n,o],d)=>{const F=a.models?.[n]?.deleteCount||0,H=o.count>0?Math.round(F/o.count*100*10)/10:0;return{id:d+1,name:J(n),count:o.count,deleteCount:F,deleteRate:H}}).sort((n,o)=>o.count-n.count)},[a,J]),Ze=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.model&&(i[n.model]||(i[n.model]={count:0,totalAccuracy:0,totalContext:0,totalUsability:0}),i[n.model].count++,n.accuracy!=null&&(i[n.model].totalAccuracy+=n.accuracy),n.context!=null&&(i[n.model].totalContext+=n.context),n.usability!=null&&(i[n.model].totalUsability+=n.usability))}),Object.entries(i).map(([n,o],d)=>({id:d+1,name:J(n),count:o.count,accuracy:o.count>0?Math.round(o.totalAccuracy/o.count):0,context:o.count>0?Math.round(o.totalContext/o.count):0,usability:o.count>0?Math.round(o.totalUsability/o.count):0,totalScore:o.count>0?Math.round((o.totalAccuracy+o.totalContext+o.totalUsability)/(3*o.count)):0})).sort((n,o)=>o.totalScore-n.totalScore)},[a,J]),mt=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.source&&(i[n.source]||(i[n.source]={total:0,count:0,totalImages:0}),i[n.source].totalImages+=1,n.accuracy!=null&&(i[n.source].total+=n.accuracy,i[n.source].count+=1))}),Object.entries(i).map(([n,o],d)=>({id:d+1,source:Y(n),avgQuality:o.count>0?Math.round(o.total/o.count):0,count:o.totalImages}))},[a,Y]),qe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.event_type&&(i[n.event_type]||(i[n.event_type]={total:0,count:0,totalImages:0}),i[n.event_type].totalImages+=1,n.accuracy!=null&&(i[n.event_type].total+=n.accuracy,i[n.event_type].count+=1))}),Object.entries(i).map(([n,o],d)=>({id:d+1,eventType:_e(n),avgQuality:o.count>0?Math.round(o.total/o.count):0,count:o.totalImages}))},[a,_e]),Ge=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;return l.forEach(o=>{o.model&&i.add(o.model)}),Ie.filter(o=>{const d=v.find(P=>P.label===o.name)?.m_code;return d&&i.has(d)})},[a,Ie,v]);if(r)return e.jsx(je,{children:e.jsx("div",{className:c.loadingContainer,children:e.jsx(Et,{})})});if(!a)return e.jsx(je,{children:e.jsx("div",{className:c.errorContainer,children:e.jsx("div",{className:"text-red-500",children:"Failed to load analytics data. Please try again."})})});const me=["#F5333F","#F64752","#F75C65","#F87079","#F9858C","#FA999F","#FBADB2","#FCC2C5"];return e.jsx(je,{children:e.jsxs("div",{className:"max-w-7xl mx-auto",children:[e.jsx("div",{className:c.tabSelector,children:e.jsx(Tt,{name:"analytics-view",value:g,onChange:t=>{(t==="crisis_maps"||t==="drone_images")&&b(t)},options:O,keySelector:t=>t.key,labelSelector:t=>t.label})}),g==="crisis_maps"?e.jsxs("div",{className:c.chartGrid,children:[e.jsxs(z,{heading:"Summary Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.summaryStatsCards,children:[e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:ue("crisis_map")}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Total Crisis Maps"})]}),e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:"2000"}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Target Amount"})]})]}),e.jsxs("div",{className:c.progressSection,children:[e.jsxs("div",{className:c.progressLabel,children:[e.jsx("span",{children:"Progress towards target"}),e.jsxs("span",{children:[Math.round(ue("crisis_map")/2e3*100),"%"]})]}),e.jsx(Ye,{value:ue("crisis_map"),totalValue:2e3})]})]}),e.jsxs(z,{heading:"Distribution Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Regions Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Be("crisis_map"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-regions-details",variant:U?"primary":"secondary",onClick:()=>E(U?"none":"regions"),className:c.userInteractionCardButton,children:U?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Sources Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:dt("crisis_map"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-sources-details",variant:ae?"primary":"secondary",onClick:()=>E(ae?"none":"sources"),className:c.userInteractionCardButton,children:ae?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Types Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Fe("crisis_map"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-types-details",variant:K?"primary":"secondary",onClick:()=>E(K?"none":"types"),className:c.userInteractionCardButton,children:K?"Hide Details":"View Details"})]})]}),U&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ae("crisis_map"),columns:De,keySelector:$,filtered:!1,pending:!1})}),ae&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:ut("crisis_map"),columns:lt,keySelector:$,filtered:!1,pending:!1})}),K&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Oe("crisis_map"),columns:Ee,keySelector:$,filtered:!1,pending:!1})})]}),e.jsxs(z,{heading:"User Interaction Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:He("crisis_map")}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median Edit Time"}),e.jsx(Z,{name:"view-edit-time-details",variant:p?"primary":"secondary",onClick:()=>E(p?"none":"editTime"),className:c.userInteractionCardButton,children:p?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:Ve()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median % Modified"}),e.jsx(Z,{name:"view-percentage-details",variant:I?"primary":"secondary",onClick:()=>E(I?"none":"percentage"),className:c.userInteractionCardButton,children:I?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:ze()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Delete Rate"}),e.jsx(Z,{name:"view-delete-details",variant:T?"primary":"secondary",onClick:()=>E(T?"none":"delete"),className:c.userInteractionCardButton,children:T?"Hide Details":"View Details"})]})]}),p&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ue("crisis_map"),columns:ke,keySelector:$,filtered:!1,pending:!1})}),I&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:We("crisis_map"),columns:Le,keySelector:$,filtered:!1,pending:!1})}),T&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Qe("crisis_map"),columns:Pe,keySelector:$,filtered:!1,pending:!1})})]}),e.jsx(z,{heading:"Model Performance",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ze("crisis_map"),columns:Te,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Quality-Source Correlation",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:mt("crisis_map"),columns:ct,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Quality-Event Type Correlation",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:qe("crisis_map"),columns:Re,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Model Consistency Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:Ge("crisis_map"),columns:$e,keySelector:$,filtered:!1,pending:!1})})})]}):e.jsxs("div",{className:c.chartGrid,children:[e.jsxs(z,{heading:"Summary Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.summaryStatsCards,children:[e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:ue("drone_image")}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Total Drone Images"})]}),e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:"2000"}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Target Amount"})]})]}),e.jsxs("div",{className:c.progressSection,children:[e.jsxs("div",{className:c.progressLabel,children:[e.jsx("span",{children:"Progress towards target"}),e.jsxs("span",{children:[Math.round(ue("drone_image")/2e3*100),"%"]})]}),e.jsx(Ye,{value:ue("drone_image"),totalValue:2e3})]})]}),e.jsxs(z,{heading:"Distribution Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Regions Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Be("drone_image"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-regions-details",variant:U?"primary":"secondary",onClick:()=>E(U?"none":"regions"),className:c.userInteractionCardButton,children:U?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Types Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Fe("drone_image"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-types-details",variant:K?"primary":"secondary",onClick:()=>E(K?"none":"types"),className:c.userInteractionCardButton,children:K?"Hide Details":"View Details"})]})]}),U&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ae("drone_image"),columns:De,keySelector:$,filtered:!1,pending:!1})}),K&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Oe("drone_image"),columns:Ee,keySelector:$,filtered:!1,pending:!1})})]}),e.jsxs(z,{heading:"User Interaction Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:He("drone_image")}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median Edit Time"}),e.jsx(Z,{name:"view-edit-time-details",variant:p?"primary":"secondary",onClick:()=>E(p?"none":"editTime"),className:c.userInteractionCardButton,children:p?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:Ve()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median % Modified"}),e.jsx(Z,{name:"view-percentage-details",variant:I?"primary":"secondary",onClick:()=>E(I?"none":"percentage"),className:c.userInteractionCardButton,children:I?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:ze()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Delete Rate"}),e.jsx(Z,{name:"view-delete-details",variant:T?"primary":"secondary",onClick:()=>E(T?"none":"delete"),className:c.userInteractionCardButton,children:T?"Hide Details":"View Details"})]})]}),p&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ue("drone_image"),columns:ke,keySelector:$,filtered:!1,pending:!1})}),I&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:We("drone_image"),columns:Le,keySelector:$,filtered:!1,pending:!1})}),T&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Qe("drone_image"),columns:Pe,keySelector:$,filtered:!1,pending:!1})})]}),e.jsx(z,{heading:"Model Performance",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ze("drone_image"),columns:Te,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Quality-Event Type Correlation",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:qe("drone_image"),columns:Re,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Model Consistency Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:Ge("drone_image"),columns:$e,keySelector:$,filtered:!1,pending:!1})})})]})]})})}export{jn as default};
         | 
|  | |
| 1 | 
            +
            import{r as s,y as nt,t as ht,a as q,c as gt,j as e,o as N,b as re,R as st,z,g as rt,d as ft,m as vt,e as pt,n as Z,A as xt,f as _t,h as Ct,i as yt,k as he,l as bt,p as ye,q as jt,s as Nt,E as wt,C as St,U as Mt,Q as It,u as Dt,N as je,_ as Et,L as Tt}from"./index-w0OOMPwN.js";const kt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("g",{clipPath:"url(#arrow-drop-down-line_svg__a)"},s.createElement("path",{d:"m12 15-4.243-4.243 1.415-1.414L12 12.172l2.828-2.83 1.415 1.415L12 15Z"})),s.createElement("defs",null,s.createElement("clipPath",{id:"arrow-drop-down-line_svg__a"},s.createElement("path",{d:"M0 0h24v24H0z"})))),Lt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("g",{clipPath:"url(#arrow-drop-up-line_svg__a)"},s.createElement("path",{d:"m12 11.828-2.828 2.829-1.415-1.414L12 9l4.243 4.243-1.415 1.414L12 11.828Z"})),s.createElement("defs",null,s.createElement("clipPath",{id:"arrow-drop-up-line_svg__a"},s.createElement("path",{d:"M0 0h24v24H0z"})))),Pt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("g",{clipPath:"url(#information-line_svg__a)"},s.createElement("path",{d:"M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10Zm0-2a8 8 0 1 0 0-16.001A8 8 0 0 0 12 20ZM11 7h2v2h-2V7Zm0 4h2v6h-2v-6Z"})),s.createElement("defs",null,s.createElement("clipPath",{id:"information-line_svg__a"},s.createElement("path",{d:"M0 0h24v24H0z"})))),Rt=({title:m,titleId:a,...h})=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",viewBox:"0 0 24 24",fill:"currentColor",width:"1em",height:"1em","aria-labelledby":a},h),m?s.createElement("title",{id:a},m):null,s.createElement("path",{fillRule:"evenodd",d:"m15.063 12 .937.938-4 4-4-4L8.938 12 12 15.063 15.063 12Z",clipRule:"evenodd"}),s.createElement("mask",{id:"table-sorting-line_svg__a",width:8,height:5,x:8,y:12,maskUnits:"userSpaceOnUse",style:{maskType:"luminance"}},s.createElement("path",{fillRule:"evenodd",d:"m15.063 12 .937.938-4 4-4-4L8.938 12 12 15.063 15.063 12Z",clipRule:"evenodd"})),s.createElement("g",{mask:"url(#table-sorting-line_svg__a)"},s.createElement("path",{d:"M-24-22h72v72h-72z"})),s.createElement("path",{fillRule:"evenodd",d:"M8.938 11 8 10.062l4-4 4 4-.938.938L12 7.937 8.937 11Z",clipRule:"evenodd"}),s.createElement("mask",{id:"table-sorting-line_svg__b",width:8,height:5,x:8,y:6,maskUnits:"userSpaceOnUse",style:{maskType:"luminance"}},s.createElement("path",{fillRule:"evenodd",d:"M8.938 11 8 10.062l4-4 4 4-.938.938L12 7.937 8.937 11Z",clipRule:"evenodd"})),s.createElement("g",{mask:"url(#table-sorting-line_svg__b)"},s.createElement("path",{d:"M48 45h-72v-72h72z"}))),$t="_number-output_1blvi_1",Bt={numberOutput:$t};function we(m){const{className:a,invalidText:h=nt,separatorHidden:r,compact:f,currency:g,value:b,tooltip:_,unit:y,prefix:x,suffix:w,maximumFractionDigits:C=1}=m,{currentLanguage:j}=s.useContext(ht),v=s.useMemo(()=>{if(q(b))return h;const S=gt(b,{currency:g,compact:f,separatorHidden:r,maximumFractionDigits:C,unit:y,language:j});return e.jsxs(e.Fragment,{children:[x,S,w]})},[h,b,f,r,g,y,C,x,j,w]);return e.jsx("div",{className:N(Bt.numberOutput,a),title:re(_)?String(_):void 0,children:v})}const At="_tooltip-dummy_rbf3f_1",Ft="_tooltip-content_rbf3f_7",Ot="_pointer_rbf3f_14",Ne={tooltipDummy:At,tooltipContent:Ft,pointer:Ot};function Ht(m){const{className:a,title:h,description:r,preferredWidth:f}=m,[g,b]=s.useState(!1),[_,y]=s.useState(!1),x=s.useRef(),w=s.useRef(null);return s.useEffect(()=>{const C=()=>{y(!0)},j=()=>{y(!1)};if(q(w.current))return;const{current:{parentNode:v}}=w;if(!q(v))return x.current=v,v.addEventListener("mouseover",C),v.addEventListener("mouseout",j),b(!0),()=>{v.removeEventListener("mouseover",C),v.removeEventListener("mouseout",j)}},[]),e.jsxs(e.Fragment,{children:[!g&&e.jsx("div",{className:Ne.tooltipDummy,ref:w}),_&&e.jsx(st,{className:N(Ne.tooltipContent,a),parentRef:x,pointerClassName:Ne.pointer,preferredWidth:f,children:e.jsx(z,{heading:h,withInternalPadding:!0,contentViewType:"vertical",children:r})})]})}function $(m){return m.id}const Vt="common",zt={booleanYesLabel:"Yes",booleanNoLabel:"No"},Ut={namespace:Vt,strings:zt},Wt="_boolean-output_kg1uq_1",Qt={booleanOutput:Wt};function Zt(m){const{className:a,invalidText:h,value:r}=m,f=rt(Ut);let g;return r===!0?g=f.booleanYesLabel:r===!1?g=f.booleanNoLabel:g=h,e.jsx("div",{className:N(Qt.booleanOutput,a),children:g})}const qt="_date-output_4jzjo_1",Gt={dateOutput:qt};function Yt(m){const{value:a,format:h,className:r,invalidText:f}=m,g=s.useMemo(()=>ft(a,h),[a,h]);return e.jsx("div",{className:N(Gt.dateOutput,r),children:g??f})}const Kt="_dropdown-menu_16hml_1",Xt="_icons_16hml_4",Jt="_content_16hml_5",ea="_actions_16hml_6",ta="_dropdown-icon_16hml_10",aa="_dropdown-content_16hml_16",le={dropdownMenu:Kt,icons:Xt,content:Jt,actions:ea,dropdownIcon:ta,dropdownContent:aa};function na(m){const a=s.useRef(null),{className:h,popupClassName:r,children:f,label:g,activeClassName:b,icons:_,variant:y="secondary",actions:x,withoutDropdownIcon:w,componentRef:C,elementRef:j=a,persistent:v,preferredPopupWidth:S}=m,p=s.useRef(null),[k,I]=s.useState(!1);s.useEffect(()=>{C&&(C.current={setShowDropdown:I})},[C,I]);const R=s.useCallback(()=>{I(ie=>!ie)},[I]),T=s.useCallback((ie,ae)=>{ae||ie&&v||I(!1)},[I,v]);vt(k,T,p,j);const G=s.useMemo(()=>({setShowDropdown:I}),[I]),U=!!x||!w;return e.jsxs(pt.Provider,{value:G,children:[e.jsx(Z,{name:void 0,className:N(le.dropdownMenu,k&&b,h),elementRef:j,onClick:R,variant:y,actionsContainerClassName:le.actions,iconsContainerClassName:le.icons,childrenContainerClassName:le.content,actions:U?e.jsxs(e.Fragment,{children:[x,!w&&(k?e.jsx(xt,{className:le.dropdownIcon}):e.jsx(_t,{className:le.dropdownIcon}))]}):void 0,icons:_,children:g}),k&&e.jsx(st,{elementRef:p,className:N(le.dropdownContent,r),parentRef:j,preferredWidth:S,children:f})]})}const sa="_info-popup_i3rna_1",ra="_label_i3rna_2",ia="_icon_i3rna_7",oa="_dropdown-container_i3rna_15",la="_content_i3rna_20",ge={infoPopup:sa,label:ra,icon:ia,dropdownContainer:oa,content:la};function ca(m){const{className:a,icon:h=e.jsx(Pt,{}),infoLabel:r,title:f,description:g,withoutIcon:b,popupClassName:_,descriptionClassName:y}=m;return e.jsx(na,{label:e.jsxs("div",{className:ge.label,children:[r,!b&&h&&e.jsx("div",{className:ge.icon,children:h})]}),popupClassName:N(ge.dropdownContainer,_),className:N(ge.infoPopup,a),variant:"tertiary",withoutDropdownIcon:!0,children:e.jsx(z,{heading:f,childrenContainerClassName:N(y,ge.content),withInternalPadding:!0,children:g})})}const da="_progress-wrapper_x340w_1",ua="_title_x340w_7",ma="_total_x340w_11",ha="_progress_x340w_1",fe={progressWrapper:da,title:ua,total:ma,progress:ha};function Ye(m){const{className:a,title:h,description:r,totalValue:f,value:g,showPercentageInTitle:b,children:_,color:y="var(--go-ui-color-primary-red)"}=m,x=re(g)?g:0,w=re(f)?f:0;let C;return w===0?C=0:C=x/w*100,e.jsxs("div",{className:N(fe.progressWrapper,a),children:[(h||b)&&e.jsxs("div",{className:fe.title,children:[h,b&&e.jsx(we,{value:C,suffix:"%"})]}),e.jsx("div",{className:fe.total,children:e.jsx("div",{className:fe.progress,style:{width:`${C}%`,backgroundColor:y}})}),r&&e.jsx("div",{className:fe.description,children:r}),_]})}const ga="_legend-element_1a9ic_1",fa="_color_1a9ic_7",va="_icon-container_1a9ic_14",pa="_icon_1a9ic_14",xa="_label_1a9ic_31",ve={legendElement:ga,color:fa,iconContainer:va,icon:pa,label:xa};function _a(m){const{className:a,colorClassName:h,iconClassName:r,color:f,label:g,iconSrc:b}=m;return e.jsxs("div",{className:N(ve.legendElement,a),children:[b?e.jsx("div",{style:{backgroundColor:f},className:ve.iconContainer,children:e.jsx("img",{className:N(ve.icon,r),src:b,alt:""})}):e.jsx("div",{style:{backgroundColor:f},className:N(ve.color,h)}),e.jsx("div",{className:ve.label,children:g})]})}const Ca="_text-output_10oza_1",ya="_with-background_10oza_6",ba="_label_10oza_11",ja="_with-colon_10oza_12",Na="_value_10oza_17",wa="_text-type_10oza_18",Sa="_strong_10oza_24",ee={textOutput:Ca,withBackground:ya,label:ba,withColon:ja,value:Na,textType:wa,strong:Sa};function Ke(m){const{className:a,label:h,icon:r,description:f,labelClassName:g,descriptionClassName:b,valueClassName:_,strongLabel:y,strongValue:x,strongDescription:w,withoutLabelColon:C,withBackground:j,invalidText:v=nt,...S}=m,{value:p}=m;let k=v;return S.valueType==="number"?k=e.jsx(we,{...S,invalidText:v}):S.valueType==="date"?k=e.jsx(Yt,{...S,invalidText:v}):S.valueType==="boolean"?k=e.jsx(Zt,{...S,invalidText:v}):p instanceof Date||(k=p||v),e.jsxs("div",{className:N(ee.textOutput,j&&ee.withBackground,a),children:[r,h&&e.jsx("div",{className:N(ee.label,y&&ee.strong,g,!C&&ee.withColon),children:h}),e.jsx("div",{className:N(ee.value,x&&ee.strong,S.valueType==="text"&&ee.textType,_),children:k}),f&&e.jsx("div",{className:N(ee.description,w&&ee.strong,b),children:f})]})}const Ma="_pie-chart_pyr7m_1",Ia="_legend_pyr7m_7",Da="_legend-item_pyr7m_13",pe={pieChart:Ma,legend:Ia,legendItem:Da},Ea=70,Ta=40;function Xe(m,a=1){return Math.round(m*10**a)/10**a}function Je(m,a){const h=(a-90)*Math.PI/180;return{x:Xe(m+m*Math.cos(h)),y:Xe(m+m*Math.sin(h))}}function ka(m,a,h){let r=h;const f=r-a===360;f&&(r-=1);const g=Je(m,a),b=Je(m,r),_=r-a<=180?0:1,y=["M",g.x,g.y,"A",m,m,0,_,1,b.x,b.y];return f?y.push("Z"):y.push("L",m,m,"L",g.x,g.y,"Z"),y.join(" ")}function xe(m){const{className:a,data:h,valueSelector:r,labelSelector:f,keySelector:g,colorSelector:b,colors:_,pieRadius:y=Ea,chartPadding:x=Ta,legendClassName:w,showPercentageInLegend:C}=m,j=Ct(h?.map(p=>r(p))),v=q(j)||j===0?1:j,S=s.useMemo(()=>{let p=0;const k=h?.map(I=>{const R=r(I);if(q(R))return;const T=360*(R/v);return p+=T,{key:g(I),value:R,label:f(I),startAngle:p-T,percentage:yt(R,v),endAngle:p,datum:I}}).filter(re)??[];return b?k.map(({datum:I,...R})=>({...R,color:b(I)})):k.map(({datum:I,...R},T)=>({...R,color:_[T%_.length]}))},[h,g,r,f,v,b,_]);return e.jsxs("div",{className:N(pe.pieChart,a),children:[e.jsx("svg",{className:pe.svg,style:{width:`${x+y*2}px`,height:`${x+y*2}px`},children:e.jsx("g",{style:{transform:`translate(${x/2}px, ${x/2}px)`},children:S.map(p=>e.jsx("path",{className:pe.path,d:ka(y,p.startAngle,p.endAngle),fill:p.color,children:e.jsx(Ht,{description:e.jsx(Ke,{label:p.label,value:p.value})})},p.key))})}),e.jsx("div",{className:N(pe.legend,w),children:S.map(p=>e.jsx(_a,{className:pe.legendItem,label:C?e.jsx(Ke,{label:p.label,value:p.percentage,valueType:"number",prefix:"(",suffix:"%)",withoutLabelColon:!0}):p.label,color:p.color},p.key))})]})}const La="_td_1k4cn_1",Pa={td:La};function Ra(m){const{className:a,children:h,...r}=m;return e.jsx("td",{className:N(a,Pa.td),...r,children:h})}function it(m){const{className:a,children:h,...r}=m;return e.jsx("tr",{className:a,...r,children:h})}const $a="_row_1829z_1",Ba="_cell_1829z_2",et={row:$a,cell:Ba};function Aa(m){const{data:a,keySelector:h,columns:r,rowClassName:f,cellClassName:g,rowModifier:b}=m;return e.jsx(e.Fragment,{children:a?.map((_,y)=>{const x=h(_,y),w=r.map(v=>{const{id:S,cellRenderer:p,cellRendererClassName:k,cellRendererParams:I,cellContainerClassName:R}=v,T=I(x,_,y,a),G=e.jsx(p,{className:k,...T,name:S});return e.jsx(Ra,{className:N(et.cell,R,typeof g=="function"?g(x,_,S):g),children:G},S)}),C=e.jsx(it,{className:N(et.row,typeof f=="function"?f(x,_):f),children:w});let j=C;return b&&(j=b({rowKey:x,row:C,cells:w,columns:r,datum:_})),e.jsx(s.Fragment,{children:j},x)})})}const Fa="_th_cdv41_1",Oa="_resize-handle_cdv41_8",tt={th:Fa,resizeHandle:Oa};function Ha(m){const{className:a,children:h,onResize:r,onResizeComplete:f,name:g,...b}=m,_=s.useRef(null),y=s.useRef(),x=s.useRef(),w=s.useRef(),C=s.useCallback(v=>{var S;if(re(y.current)&&_.current&&r){v.preventDefault(),v.stopPropagation();const p=v.clientX-y.current;if(re(x.current)){const k=x.current+p;w.current=k,r(k,g)}else x.current=(S=_.current)==null?void 0:S.offsetWidth}},[r,g]),j=s.useCallback(v=>{var S;v.preventDefault(),y.current=v.clientX,x.current=(S=_.current)==null?void 0:S.offsetWidth,window.addEventListener("mousemove",C,!0)},[C]);return s.useEffect(()=>{const v=()=>{y.current=void 0,x.current=void 0,f&&re(w.current)&&f(w.current,g),window.removeEventListener("mousemove",C,!0)};return window.addEventListener("mouseup",v,!0),()=>{window.removeEventListener("mouseup",v,!0),window.removeEventListener("mousemove",C,!0)}},[C,g,f]),e.jsxs("th",{ref:_,className:N(a,tt.th),...b,children:[r&&e.jsx("div",{role:"presentation",className:tt.resizeHandle,onMouseDown:j}),h]})}const Va="_table_nilhy_1",za="_table-overflow-wrapper_nilhy_8",Ua="_table-element_nilhy_13",Wa="_header-row_nilhy_23",Qa="_header-element_nilhy_24",Za="_header-component_nilhy_29",ce={table:Va,tableOverflowWrapper:za,tableElement:Ua,headerRow:Wa,headerElement:Qa,headerComponent:Za};function qa(m,a){return a??m.columnWidth??wt}function B(m){const{data:a,keySelector:h,columns:r,caption:f,className:g,captionClassName:b,headerRowClassName:_,headerCellClassName:y,rowClassName:x,cellClassName:w,rowModifier:C,fixedColumnWidth:j,resizableColumn:v,headersHidden:S,pending:p,filtered:k,errored:I=!1}=m,R=s.useRef(null),[T]=he.useState(()=>bt()),[G,U]=he.useState({});s.useEffect(()=>{U(W=>{if(q(R.current))return W;const E=R.current.getBoundingClientRect(),{width:O}=E;let A=r.map(M=>({id:M.id,stretch:!!M.columnStretch,width:qa(M,W[M.id])}));const X=ye(A.filter(M=>M.stretch).map(M=>M.width)),ne=ye(A.filter(M=>!M.stretch).map(M=>M.width)),Y=(O-ne)/X;return Y>1&&(A=A.map(M=>({...M,width:M.stretch?M.width*Y:M.width}))),jt(A,M=>M.id,M=>M.width)})},[r]);const ie=he.useCallback((W,E)=>{const O=document.getElementById(`${T}-${E}`),A=Math.max(W,80);if(q(O)||(O.style.width=`${A}px`,!j))return;const X=document.getElementById(T);if(q(X))return;const ne=ye(r.map(Y=>Y.id===E?A:G[Y.id]));X.style.width=`${ne}px`},[T,G,r,j]),ae=he.useCallback((W,E)=>{re(E)&&U(O=>({...O,[E]:Math.max(W,80)}))},[U]),be=he.useMemo(()=>ye(r.map(W=>G[W.id])),[G,r]),K=q(a)||a.length===0||Object.keys(G).length===0;return e.jsxs("div",{ref:R,className:N(ce.table,g),children:[!K&&e.jsx("div",{className:ce.tableOverflowWrapper,children:e.jsxs("table",{className:ce.tableElement,style:j?{width:`${be}px`}:void 0,id:T,children:[f&&e.jsx("caption",{className:b,children:f}),e.jsx("colgroup",{children:r.map(W=>{const{id:E,columnClassName:O}=W,A=G[E],X=j?{width:`${A}px`}:void 0;return e.jsx("col",{id:`${T}-${E}`,style:X,className:N(ce.column,O)},E)})}),!S&&e.jsx("thead",{children:e.jsx(it,{className:N(ce.headerRow,_),children:r.map((W,E)=>{const{id:O,title:A,headerCellRenderer:X,headerCellRendererClassName:ne,headerCellRendererParams:Y,headerContainerClassName:M}=W,se=e.jsx(X,{...Y,name:O,title:A,index:E,className:N(ne,ce.headerComponent)});return e.jsx(Ha,{scope:"col",name:O,onResize:v?ie:void 0,onResizeComplete:v?ae:void 0,className:N(ce.headerElement,typeof y=="function"?y(O):y,M),children:se},O)})})}),e.jsx("tbody",{children:e.jsx(Aa,{data:a,keySelector:h,columns:r,rowClassName:x,cellClassName:w,rowModifier:C})})]})}),e.jsx(Nt,{filtered:k,empty:K,errored:I,pending:p,overlayPending:!0})]})}function Ga(m){const{className:a,value:h}=m;return q(h)?null:e.jsx("div",{className:a,children:h})}const Ya="common",Ka={sortTableButtonTitle:"Sort Table"},Xa={namespace:Ya,strings:Ka},Ja="_header-cell_vn24d_1",en="_sort-button_vn24d_8",tn="_icon_vn24d_12",an="_info-popup-icon_vn24d_17",de={headerCell:Ja,sortButton:en,icon:tn,infoPopupIcon:an};function ot(m){const{className:a,titleClassName:h,title:r,name:f,sortable:g,defaultSortDirection:b="asc",infoTitle:_,infoDescription:y}=m,{sorting:x,setSorting:w}=s.useContext(St),C=rt(Xa),j=x?.name===f?x.direction:void 0,v=s.useRef(null),S=s.useCallback(()=>{if(q(w))return;let p;q(j)?p=b:j==="asc"?p="dsc":j==="dsc"&&(p="asc"),w(p?{name:f,direction:p}:void 0)},[f,w,j,b]);return e.jsxs("div",{ref:v,className:N(a,de.headerCell),children:[g&&e.jsxs(Z,{name:void 0,variant:"tertiary",onClick:S,title:C.sortTableButtonTitle,className:de.sortButton,children:[q(j)&&e.jsx(Rt,{className:de.icon}),j==="asc"&&e.jsx(Lt,{className:de.icon}),j==="dsc"&&e.jsx(kt,{className:de.icon})]}),e.jsx("div",{className:N(h,de.title),children:r}),_&&y&&e.jsx(ca,{className:de.infoPopupIcon,title:_,description:y})]})}const at={};function Q(m,a,h,r){return{id:m,title:a,columnClassName:r?.columnClassName,headerCellRenderer:ot,headerCellRendererClassName:r?.headerCellRendererClassName,headerContainerClassName:r?.headerContainerClassName,headerCellRendererParams:{sortable:r?.sortable,infoTitle:r?.headerInfoTitle,infoDescription:r?.headerInfoDescription},cellRendererClassName:r?.cellRendererClassName,cellContainerClassName:r?.cellContainerClassName,cellRenderer:Ga,cellRendererParams:(f,g)=>({value:h(g)||"--"}),valueSelector:h,valueComparator:(f,g)=>Mt(h(f),h(g)),columnWidth:r?.columnWidth,columnStretch:r?.columnStretch,columnStyle:r?.columnStyle}}function D(m,a,h,r){return{id:m,title:a,columnClassName:r?.columnClassName,headerCellRenderer:ot,headerCellRendererClassName:N(at.numberCellHeader,r?.headerCellRendererClassName),headerContainerClassName:r?.headerContainerClassName,headerCellRendererParams:{sortable:r?.sortable,infoTitle:r?.headerInfoTitle,infoDescription:r?.headerInfoDescription},cellRendererClassName:N(at.numberCell,r?.cellRendererClassName),cellContainerClassName:r?.cellContainerClassName,cellRenderer:we,cellRendererParams:(f,g)=>({value:h(g),suffix:r?.suffix,maximumFractionDigits:r?.maximumFractionDigits,invalidText:"--"}),valueSelector:h,valueComparator:(f,g)=>It(h(f),h(g)),columnWidth:r?.columnWidth,columnStretch:r?.columnStretch,columnStyle:r?.columnStyle}}const nn="_tabSelector_vlxoe_1",sn="_progressSection_vlxoe_14",rn="_progressLabel_vlxoe_20",on="_chartGrid_vlxoe_28",ln="_chartContainer_vlxoe_40",cn="_tableContainer_vlxoe_51",dn="_modelPerformance_vlxoe_59",un="_loadingContainer_vlxoe_67",mn="_errorContainer_vlxoe_77",hn="_userInteractionCards_vlxoe_96",gn="_userInteractionCard_vlxoe_96",fn="_userInteractionCardValue_vlxoe_116",vn="_userInteractionCardLabel_vlxoe_123",pn="_userInteractionCardButton_vlxoe_130",xn="_summaryStatsCards_vlxoe_148",_n="_summaryStatsCard_vlxoe_148",Cn="_summaryStatsCardValue_vlxoe_169",yn="_summaryStatsCardLabel_vlxoe_176",c={tabSelector:nn,progressSection:sn,progressLabel:rn,chartGrid:on,chartContainer:ln,tableContainer:cn,modelPerformance:dn,loadingContainer:un,errorContainer:mn,userInteractionCards:hn,userInteractionCard:gn,userInteractionCardValue:fn,userInteractionCardLabel:vn,userInteractionCardButton:pn,summaryStatsCards:xn,summaryStatsCard:_n,summaryStatsCardValue:Cn,summaryStatsCardLabel:yn};function jn(){const[m]=Dt(),[a,h]=s.useState(null),[r,f]=s.useState(!0),[g,b]=s.useState("crisis_maps"),[_,y]=s.useState([]),[x,w]=s.useState([]),[C,j]=s.useState([]),[v,S]=s.useState([]),[p,k]=s.useState(!1),[I,R]=s.useState(!1),[T,G]=s.useState(!1),[U,ie]=s.useState(!1),[ae,be]=s.useState(!1),[K,W]=s.useState(!1),E=t=>{k(t==="editTime"),R(t==="percentage"),G(t==="delete"),ie(t==="regions"),be(t==="sources"),W(t==="types")},O=[{key:"crisis_maps",label:"Crisis Maps"},{key:"drone_images",label:"Drone Images"}],A=s.useCallback((t,l)=>{if(!t||!l)return 0;const i=t.toLowerCase().replace(/[^\w\s]/g,"").split(/\s+/).filter(u=>u.length>0),n=l.toLowerCase().replace(/[^\w\s]/g,"").split(/\s+/).filter(u=>u.length>0);if(i.length===0&&n.length===0)return 1;if(i.length===0||n.length===0)return 0;const o=new Set(i),d=new Set(n),P=new Set([...o].filter(u=>d.has(u))),F=new Set([...o,...d]);return P.size/F.size},[]),X=s.useCallback(async()=>{f(!0);try{const l=await(await fetch("/api/images")).json(),i={},n=l.filter(u=>u.image_type==="crisis_map"),o=l.filter(u=>u.image_type==="drone_image"),d={totalCaptions:l.length,sources:{},types:{},regions:{},models:{},modelEditTimes:i,percentageModified:0,modelPercentageData:{},totalDeleteCount:0,deleteRate:0,crisisMaps:n,droneImages:o};l.forEach(u=>{if(u.source&&(d.sources[u.source]=(d.sources[u.source]||0)+1),u.event_type&&(d.types[u.event_type]=(d.types[u.event_type]||0)+1),u.countries&&u.countries.forEach(L=>{L.r_code&&(d.regions[L.r_code]=(d.regions[L.r_code]||0)+1)}),u.model){const L=u.model,V=d.models[L]||={count:0,avgAccuracy:0,avgContext:0,avgUsability:0,totalScore:0,deleteCount:0};if(V.count++,u.accuracy!=null&&(V.avgAccuracy+=u.accuracy),u.context!=null&&(V.avgContext+=u.context),u.usability!=null&&(V.avgUsability+=u.usability),u.created_at&&u.updated_at){const te=new Date(u.created_at).getTime(),Ce=new Date(u.updated_at).getTime()-te;Ce>0&&(i[L]||(i[L]=[]),i[L].push(Ce))}}}),_.forEach(u=>{u.s_code&&!d.sources[u.s_code]&&(d.sources[u.s_code]=0)}),x.forEach(u=>{u.t_code&&!d.types[u.t_code]&&(d.types[u.t_code]=0)}),C.forEach(u=>{u.r_code&&!d.regions[u.r_code]&&(d.regions[u.r_code]=0)}),["GPT-4","Claude","Gemini","Llama","Other"].forEach(u=>{d.models[u]||(d.models[u]={count:0,avgAccuracy:0,avgContext:0,avgUsability:0,totalScore:0,deleteCount:0})}),Object.values(d.models).forEach(u=>{u.count>0&&(u.avgAccuracy=Math.round(u.avgAccuracy/u.count),u.avgContext=Math.round(u.avgContext/u.count),u.avgUsability=Math.round(u.avgUsability/u.count),u.totalScore=Math.round((u.avgAccuracy+u.avgContext+u.avgUsability)/3))});const F=l.filter(u=>u.generated&&u.edited);if(F.length>0){const L=[...F.map(oe=>A(oe.generated,oe.edited))].sort((oe,Ce)=>oe-Ce),V=Math.floor(L.length/2),te=L.length%2===0?(L[V-1]+L[V])/2:L[V];d.percentageModified=Math.round((1-te)*100)}const H={};l.forEach(u=>{if(u.model&&u.generated&&u.edited){const L=A(u.generated,u.edited),V=Math.round((1-L)*100);H[u.model]||(H[u.model]=[]),H[u.model].push(V)}}),d.modelPercentageData=H;try{const u=await fetch("/api/models");if(u.ok){const L=await u.json();if(L.models){L.models.forEach(te=>{d.models[te.m_code]&&(d.models[te.m_code].deleteCount=te.delete_count||0)});const V=L.models.reduce((te,oe)=>te+(oe.delete_count||0),0);d.totalDeleteCount=V,d.deleteRate=V>0?Math.round(V/(V+l.length)*100):0}}}catch(u){console.log("Could not fetch model delete counts:",u)}h(d)}catch{h(null)}finally{f(!1)}},[_,x,C,A]),ne=s.useCallback(async()=>{try{const[t,l,i,n]=await Promise.all([fetch("/api/sources"),fetch("/api/types"),fetch("/api/regions"),fetch("/api/models")]),o=await t.json(),d=await l.json(),P=await i.json(),F=await n.json();y(o),w(d),j(P),S(F.models||[])}catch(t){console.log("Could not fetch lookup data:",t)}},[]);s.useEffect(()=>{const t=m.get("view");(t==="crisis_maps"||t==="drone_images")&&b(t)},[m]),s.useEffect(()=>{ne()},[ne]),s.useEffect(()=>{_.length>0&&x.length>0&&C.length>0&&v.length>0&&X()},[_,x,C,v,X]);const Y=s.useCallback(t=>{const l=_.find(i=>i.s_code===t);return l?l.label:t},[_]),M=s.useCallback(t=>{if(t.length===0)return 0;const l=[...t].sort((n,o)=>n-o),i=Math.floor(l.length/2);return l.length%2===0?Math.round((l[i-1]+l[i])/2):l[i]},[]),se=s.useCallback(t=>{const l=Math.floor(t/1e3),i=Math.floor(l/60),n=Math.floor(i/60);return n>0?`${n}h ${i%60}m`:i>0?`${i}m ${l%60}s`:`${l}s`},[]),_e=s.useCallback(t=>{const l=x.find(i=>i.t_code===t);return l?l.label:t},[x]),J=s.useCallback(t=>{const l=v.find(i=>i.m_code===t);return l?l.label:t},[v]),Se=s.useMemo(()=>a?Object.entries(a.modelEditTimes||{}).filter(([,t])=>t.length>0).sort(([,t],[,l])=>M(l)-M(t)).map(([t,l],i)=>({id:i+1,name:J(t),count:l.length,avgEditTime:M(l),minEditTime:Math.min(...l),maxEditTime:Math.max(...l)})):[],[a,M,J]),Me=s.useMemo(()=>a?Object.entries(a.modelPercentageData||{}).filter(([,t])=>t.length>0).sort(([,t],[,l])=>{const i=[...t].sort((H,u)=>H-u),n=[...l].sort((H,u)=>H-u),o=Math.floor(i.length/2),d=Math.floor(n.length/2),P=i.length%2===0?(i[o-1]+i[o])/2:i[o];return(n.length%2===0?(n[d-1]+n[d])/2:n[d])-P}).map(([t,l],i)=>{const n=[...l].sort((P,F)=>P-F),o=Math.floor(n.length/2),d=n.length%2===0?Math.round((n[o-1]+n[o])/2):n[o];return{id:i+1,name:J(t),count:l.length,avgPercentageModified:d,minPercentageModified:Math.min(...l),maxPercentageModified:Math.max(...l)}}):[],[a,J]),Ie=s.useMemo(()=>a?Object.entries(a.models).filter(([,t])=>t.count>0).map(([t,l],i)=>{const n=[l.avgAccuracy,l.avgContext,l.avgUsability],o=n.reduce((F,H)=>F+H,0)/n.length,d=n.reduce((F,H)=>F+Math.pow(H-o,2),0)/n.length,P=Math.round(100-Math.sqrt(d));return{id:i+1,name:J(t),consistency:Math.max(0,P),avgScore:Math.round(o),count:l.count}}).sort((t,l)=>l.consistency-t.consistency):[],[a,J]),De=s.useMemo(()=>[Q("name","Region",t=>t.name),D("count","Count",t=>t.count),D("percentage","% of Total",t=>t.percentage,{suffix:"%",maximumFractionDigits:0})],[]),Ee=s.useMemo(()=>[Q("name","Type",t=>t.name),D("count","Count",t=>t.count),D("percentage","% of Total",t=>t.percentage,{suffix:"%",maximumFractionDigits:0})],[]),lt=s.useMemo(()=>[Q("name","Source",t=>t.name),D("count","Count",t=>t.count),D("percentage","% of Total",t=>t.percentage,{suffix:"%",maximumFractionDigits:0})],[]),Te=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Count",t=>t.count),D("accuracy","Accuracy",t=>t.accuracy,{suffix:"%",maximumFractionDigits:0}),D("context","Context",t=>t.context,{suffix:"%",maximumFractionDigits:0}),D("usability","Usability",t=>t.usability,{suffix:"%",maximumFractionDigits:0}),D("totalScore","Total Score",t=>t.totalScore,{suffix:"%",maximumFractionDigits:0})],[]),ke=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Count",t=>t.count),Q("avgEditTime","Median Edit Time",t=>se(t.avgEditTime)),Q("minEditTime","Min Edit Time",t=>se(t.minEditTime)),Q("maxEditTime","Max Edit Time",t=>se(t.maxEditTime))],[se]),Le=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Count",t=>t.count),D("avgPercentageModified","Median % Modified",t=>t.avgPercentageModified,{suffix:"%",maximumFractionDigits:0}),D("minPercentageModified","Min % Modified",t=>t.minPercentageModified,{suffix:"%",maximumFractionDigits:0}),D("maxPercentageModified","Max % Modified",t=>t.maxPercentageModified,{suffix:"%",maximumFractionDigits:0})],[]),Pe=s.useMemo(()=>[Q("name","Model",t=>t.name),D("count","Total Count",t=>t.count),D("deleteCount","Delete Count",t=>t.deleteCount),D("deleteRate","Delete Rate",t=>t.deleteRate,{suffix:"%",maximumFractionDigits:1})],[]),ct=s.useMemo(()=>[Q("source","Source",t=>t.source),D("avgQuality","Average Quality",t=>t.avgQuality,{suffix:"%",maximumFractionDigits:0}),D("count","Count",t=>t.count)],[]),Re=s.useMemo(()=>[Q("eventType","Event Type",t=>t.eventType),D("avgQuality","Average Quality",t=>t.avgQuality,{suffix:"%",maximumFractionDigits:0}),D("count","Count",t=>t.count)],[]),$e=s.useMemo(()=>[Q("name","Model",t=>t.name),D("consistency","Consistency",t=>t.consistency,{suffix:"%",maximumFractionDigits:0}),D("avgScore","Average Score",t=>t.avgScore,{suffix:"%",maximumFractionDigits:0}),D("count","Count",t=>t.count)],[]),ue=s.useCallback(t=>a?t==="crisis_map"?a.crisisMaps.length:t==="drone_image"?a.droneImages.length:0:0,[a]),Be=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.countries&&n.countries.forEach(o=>{o.r_code&&(i[o.r_code]=(i[o.r_code]||0)+1)})}),Object.entries(i).filter(([,n])=>n>0).map(([n,o])=>({name:C.find(d=>d.r_code===n)?.label||n,value:o}))},[a,C]),Ae=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};l.forEach(o=>{o.countries&&o.countries.forEach(d=>{d.r_code&&(i[d.r_code]=(i[d.r_code]||0)+1)})});const n=C.reduce((o,d)=>(d.r_code&&(o[d.r_code]={name:d.label,count:i[d.r_code]||0}),o),{});return Object.entries(n).sort(([,o],[,d])=>d.count-o.count).map(([,{name:o,count:d}],P)=>({id:P+1,name:o,count:d,percentage:l.length>0?Math.round(d/l.length*100):0}))},[a,C]),dt=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.source&&(i[n.source]=(i[n.source]||0)+1)}),Object.entries(i).filter(([,n])=>n>0).map(([n,o])=>({name:_.find(d=>d.s_code===n)?.label||n,value:o}))},[a,_]),ut=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.source&&(i[n.source]=(i[n.source]||0)+1)}),Object.entries(i).sort(([,n],[,o])=>o-n).map(([n,o],d)=>({id:d+1,name:Y(n),count:o,percentage:l.length>0?Math.round(o/l.length*100):0}))},[a,Y]),Fe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.event_type&&(i[n.event_type]=(i[n.event_type]||0)+1)}),Object.entries(i).filter(([,n])=>n>0).map(([n,o])=>({name:x.find(d=>d.t_code===n)?.label||n,value:o}))},[a,x]),Oe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.event_type&&(i[n.event_type]=(i[n.event_type]||0)+1)}),Object.entries(i).sort(([,n],[,o])=>o-n).map(([n,o],d)=>({id:d+1,name:_e(n),count:o,percentage:l.length>0?Math.round(o/l.length*100):0}))},[a,_e]),He=s.useCallback(t=>{if(!a)return"No data available";const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;l.forEach(d=>{d.model&&i.add(d.model)}),console.log(`Debug ${t}:`,{totalImages:l.length,usedModels:Array.from(i),availableEditTimes:Object.keys(a.modelEditTimes),modelEditTimesData:a.modelEditTimes});const o=Object.entries(a.modelEditTimes).filter(([d])=>i.has(d)).flatMap(([,d])=>d);return o.length===0?"No data available":se(M(o))},[a,se,M]),Ve=s.useCallback(()=>{if(!a)return"No data available";const t=a.totalCaptions||0,l=a.percentageModified||0;return t>0?Math.round(l/t*100):0},[a]),ze=s.useCallback(()=>a&&a.deleteRate>=0?`${a.deleteRate}%`:"No data available",[a]),Ue=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;return l.forEach(o=>{o.model&&i.add(o.model)}),Se.filter(o=>{const d=v.find(P=>P.label===o.name)?.m_code;return d&&i.has(d)})},[a,Se,v]),We=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;return l.forEach(o=>{o.model&&i.add(o.model)}),Me.filter(o=>{const d=v.find(P=>P.label===o.name)?.m_code;return d&&i.has(d)})},[a,Me,v]),Qe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.model&&(i[n.model]||(i[n.model]={count:0,deleteCount:0}),i[n.model].count++)}),Object.entries(i).map(([n,o],d)=>{const F=a.models?.[n]?.deleteCount||0,H=o.count>0?Math.round(F/o.count*100*10)/10:0;return{id:d+1,name:J(n),count:o.count,deleteCount:F,deleteRate:H}}).sort((n,o)=>o.count-n.count)},[a,J]),Ze=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.model&&(i[n.model]||(i[n.model]={count:0,totalAccuracy:0,totalContext:0,totalUsability:0}),i[n.model].count++,n.accuracy!=null&&(i[n.model].totalAccuracy+=n.accuracy),n.context!=null&&(i[n.model].totalContext+=n.context),n.usability!=null&&(i[n.model].totalUsability+=n.usability))}),Object.entries(i).map(([n,o],d)=>({id:d+1,name:J(n),count:o.count,accuracy:o.count>0?Math.round(o.totalAccuracy/o.count):0,context:o.count>0?Math.round(o.totalContext/o.count):0,usability:o.count>0?Math.round(o.totalUsability/o.count):0,totalScore:o.count>0?Math.round((o.totalAccuracy+o.totalContext+o.totalUsability)/(3*o.count)):0})).sort((n,o)=>o.totalScore-n.totalScore)},[a,J]),mt=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.source&&(i[n.source]||(i[n.source]={total:0,count:0,totalImages:0}),i[n.source].totalImages+=1,n.accuracy!=null&&(i[n.source].total+=n.accuracy,i[n.source].count+=1))}),Object.entries(i).map(([n,o],d)=>({id:d+1,source:Y(n),avgQuality:o.count>0?Math.round(o.total/o.count):0,count:o.totalImages}))},[a,Y]),qe=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i={};return l.forEach(n=>{n.event_type&&(i[n.event_type]||(i[n.event_type]={total:0,count:0,totalImages:0}),i[n.event_type].totalImages+=1,n.accuracy!=null&&(i[n.event_type].total+=n.accuracy,i[n.event_type].count+=1))}),Object.entries(i).map(([n,o],d)=>({id:d+1,eventType:_e(n),avgQuality:o.count>0?Math.round(o.total/o.count):0,count:o.totalImages}))},[a,_e]),Ge=s.useCallback(t=>{if(!a)return[];const l=t==="crisis_map"?a.crisisMaps:a.droneImages,i=new Set;return l.forEach(o=>{o.model&&i.add(o.model)}),Ie.filter(o=>{const d=v.find(P=>P.label===o.name)?.m_code;return d&&i.has(d)})},[a,Ie,v]);if(r)return e.jsx(je,{children:e.jsx("div",{className:c.loadingContainer,children:e.jsx(Et,{})})});if(!a)return e.jsx(je,{children:e.jsx("div",{className:c.errorContainer,children:e.jsx("div",{className:"text-red-500",children:"Failed to load analytics data. Please try again."})})});const me=["#F5333F","#F64752","#F75C65","#F87079","#F9858C","#FA999F","#FBADB2","#FCC2C5"];return e.jsx(je,{children:e.jsxs("div",{className:"max-w-7xl mx-auto",children:[e.jsx("div",{className:c.tabSelector,children:e.jsx(Tt,{name:"analytics-view",value:g,onChange:t=>{(t==="crisis_maps"||t==="drone_images")&&b(t)},options:O,keySelector:t=>t.key,labelSelector:t=>t.label})}),g==="crisis_maps"?e.jsxs("div",{className:c.chartGrid,children:[e.jsxs(z,{heading:"Summary Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.summaryStatsCards,children:[e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:ue("crisis_map")}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Total Crisis Maps"})]}),e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:"2000"}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Target Amount"})]})]}),e.jsxs("div",{className:c.progressSection,children:[e.jsxs("div",{className:c.progressLabel,children:[e.jsx("span",{children:"Progress towards target"}),e.jsxs("span",{children:[Math.round(ue("crisis_map")/2e3*100),"%"]})]}),e.jsx(Ye,{value:ue("crisis_map"),totalValue:2e3})]})]}),e.jsxs(z,{heading:"Distribution Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Regions Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Be("crisis_map"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-regions-details",variant:U?"primary":"secondary",onClick:()=>E(U?"none":"regions"),className:c.userInteractionCardButton,children:U?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Sources Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:dt("crisis_map"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-sources-details",variant:ae?"primary":"secondary",onClick:()=>E(ae?"none":"sources"),className:c.userInteractionCardButton,children:ae?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Types Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Fe("crisis_map"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-types-details",variant:K?"primary":"secondary",onClick:()=>E(K?"none":"types"),className:c.userInteractionCardButton,children:K?"Hide Details":"View Details"})]})]}),U&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ae("crisis_map"),columns:De,keySelector:$,filtered:!1,pending:!1})}),ae&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:ut("crisis_map"),columns:lt,keySelector:$,filtered:!1,pending:!1})}),K&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Oe("crisis_map"),columns:Ee,keySelector:$,filtered:!1,pending:!1})})]}),e.jsxs(z,{heading:"User Interaction Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:He("crisis_map")}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median Edit Time"}),e.jsx(Z,{name:"view-edit-time-details",variant:p?"primary":"secondary",onClick:()=>E(p?"none":"editTime"),className:c.userInteractionCardButton,children:p?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:Ve()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median % Modified"}),e.jsx(Z,{name:"view-percentage-details",variant:I?"primary":"secondary",onClick:()=>E(I?"none":"percentage"),className:c.userInteractionCardButton,children:I?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:ze()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Delete Rate"}),e.jsx(Z,{name:"view-delete-details",variant:T?"primary":"secondary",onClick:()=>E(T?"none":"delete"),className:c.userInteractionCardButton,children:T?"Hide Details":"View Details"})]})]}),p&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ue("crisis_map"),columns:ke,keySelector:$,filtered:!1,pending:!1})}),I&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:We("crisis_map"),columns:Le,keySelector:$,filtered:!1,pending:!1})}),T&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Qe("crisis_map"),columns:Pe,keySelector:$,filtered:!1,pending:!1})})]}),e.jsx(z,{heading:"Model Performance",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ze("crisis_map"),columns:Te,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Quality-Source Correlation",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:mt("crisis_map"),columns:ct,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Quality-Event Type Correlation",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:qe("crisis_map"),columns:Re,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Model Consistency Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:Ge("crisis_map"),columns:$e,keySelector:$,filtered:!1,pending:!1})})})]}):e.jsxs("div",{className:c.chartGrid,children:[e.jsxs(z,{heading:"Summary Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.summaryStatsCards,children:[e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:ue("drone_image")}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Total Drone Images"})]}),e.jsxs("div",{className:c.summaryStatsCard,children:[e.jsx("div",{className:c.summaryStatsCardValue,children:"2000"}),e.jsx("div",{className:c.summaryStatsCardLabel,children:"Target Amount"})]})]}),e.jsxs("div",{className:c.progressSection,children:[e.jsxs("div",{className:c.progressLabel,children:[e.jsx("span",{children:"Progress towards target"}),e.jsxs("span",{children:[Math.round(ue("drone_image")/2e3*100),"%"]})]}),e.jsx(Ye,{value:ue("drone_image"),totalValue:2e3})]})]}),e.jsxs(z,{heading:"Distribution Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Regions Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Be("drone_image"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-regions-details",variant:U?"primary":"secondary",onClick:()=>E(U?"none":"regions"),className:c.userInteractionCardButton,children:U?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardLabel,children:"Types Distribution"}),e.jsx("div",{className:c.chartContainer,children:e.jsx(xe,{data:Fe("drone_image"),valueSelector:t=>t.value,labelSelector:t=>t.name,keySelector:t=>t.name,colors:me,showPercentageInLegend:!0})}),e.jsx(Z,{name:"view-types-details",variant:K?"primary":"secondary",onClick:()=>E(K?"none":"types"),className:c.userInteractionCardButton,children:K?"Hide Details":"View Details"})]})]}),U&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ae("drone_image"),columns:De,keySelector:$,filtered:!1,pending:!1})}),K&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Oe("drone_image"),columns:Ee,keySelector:$,filtered:!1,pending:!1})})]}),e.jsxs(z,{heading:"User Interaction Statistics",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:[e.jsxs("div",{className:c.userInteractionCards,children:[e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:He("drone_image")}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median Edit Time"}),e.jsx(Z,{name:"view-edit-time-details",variant:p?"primary":"secondary",onClick:()=>E(p?"none":"editTime"),className:c.userInteractionCardButton,children:p?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:Ve()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Median % Modified"}),e.jsx(Z,{name:"view-percentage-details",variant:I?"primary":"secondary",onClick:()=>E(I?"none":"percentage"),className:c.userInteractionCardButton,children:I?"Hide Details":"View Details"})]}),e.jsxs("div",{className:c.userInteractionCard,children:[e.jsx("div",{className:c.userInteractionCardValue,children:ze()}),e.jsx("div",{className:c.userInteractionCardLabel,children:"Delete Rate"}),e.jsx(Z,{name:"view-delete-details",variant:T?"primary":"secondary",onClick:()=>E(T?"none":"delete"),className:c.userInteractionCardButton,children:T?"Hide Details":"View Details"})]})]}),p&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ue("drone_image"),columns:ke,keySelector:$,filtered:!1,pending:!1})}),I&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:We("drone_image"),columns:Le,keySelector:$,filtered:!1,pending:!1})}),T&&e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Qe("drone_image"),columns:Pe,keySelector:$,filtered:!1,pending:!1})})]}),e.jsx(z,{heading:"Model Performance",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.modelPerformance,children:e.jsx(B,{data:Ze("drone_image"),columns:Te,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Quality-Event Type Correlation",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:qe("drone_image"),columns:Re,keySelector:$,filtered:!1,pending:!1})})}),e.jsx(z,{heading:"Model Consistency Analysis",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,children:e.jsx("div",{className:c.tableContainer,children:e.jsx(B,{data:Ge("drone_image"),columns:$e,keySelector:$,filtered:!1,pending:!1})})})]})]})})}export{jn as default};
         | 
    	
        py_backend/static/assets/index-D5zar1LU.js
    DELETED
    
    | @@ -1,2 +0,0 @@ | |
| 1 | 
            -
            const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/jszip.min-uuN5m5dK.js","assets/index-B0L3qvtx.js","assets/index-cCOeofBN.css"])))=>i.map(i=>d[i]);
         | 
| 2 | 
            -
            import{K as ea,x as aa,j as a,N as ee,n as L,r as d,D as ta,_ as we,L as sa,z as P,v as ne,w as oe,F as ia,M as na,G as oa}from"./index-B0L3qvtx.js";import{u as ra}from"./useAdmin-DN0iYnPm.js";import{F as la,E as ca}from"./ExportModal-BI_JlHOZ.js";const da="_tabSelector_usssr_1",ga="_imageContainer_usssr_12",ma="_imagePlaceholder_usssr_33",ua="_metadataTags_usssr_45",pa="_metadataTag_usssr_45",fa="_captionContainer_usssr_67",_a="_captionText_usssr_74",ha="_gridLayout_usssr_131",xa="_detailsSection_usssr_155",ya="_loadingContainer_usssr_161",va="_errorContainer_usssr_171",ja="_fullSizeModalOverlay_usssr_205",wa="_fullSizeModalContent_usssr_219",Ia="_ratingWarningContent_usssr_230",Na="_ratingWarningTitle_usssr_236",Ca="_ratingWarningText_usssr_243",ba="_ratingWarningButtons_usssr_250",Sa="_carouselContainer_usssr_365",Da="_carouselImageWrapper_usssr_370",ka="_carouselImage_usssr_370",Ma="_carouselNavigation_usssr_393",La="_carouselButton_usssr_405",Fa="_carouselIndicators_usssr_429",Ta="_carouselIndicator_usssr_429",Pa="_carouselIndicatorActive_usssr_458",Ea="_singleImageContainer_usssr_488",$a="_viewImageButtonContainer_usssr_494",m={tabSelector:da,imageContainer:ga,imagePlaceholder:ma,metadataTags:ua,metadataTag:pa,captionContainer:fa,captionText:_a,gridLayout:ha,detailsSection:xa,loadingContainer:ya,errorContainer:va,fullSizeModalOverlay:ja,fullSizeModalContent:wa,ratingWarningContent:Ia,ratingWarningTitle:Na,ratingWarningText:Ca,ratingWarningButtons:ba,carouselContainer:Sa,carouselImageWrapper:Da,carouselImage:ka,carouselNavigation:Ma,carouselButton:La,carouselIndicators:Fa,carouselIndicator:Ta,carouselIndicatorActive:Pa,singleImageContainer:Ea,viewImageButtonContainer:$a};function st(){const{mapId:g}=ea(),x=aa(),{isAuthenticated:re}=ra();console.log("MapDetailsPage: Current URL:",window.location.href),console.log("MapDetailsPage: Hash:",window.location.hash),console.log("MapDetailsPage: mapId from useParams:",g),console.log("MapDetailsPage: mapId type:",typeof g),console.log("MapDetailsPage: mapId length:",g?.length),console.log("MapDetailsPage: mapId value:",JSON.stringify(g));const Ie=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!g||g==="undefined"||g==="null"||g.trim()===""||!Ie.test(g))return a.jsx(ee,{children:a.jsxs("div",{className:"flex flex-col items-center gap-4 text-center py-12",children:[a.jsx("div",{className:"text-4xl",children:"β οΈ"}),a.jsx("div",{className:"text-xl font-semibold",children:"Invalid Map ID"}),a.jsx("div",{children:"The map ID provided is not valid."}),a.jsxs("div",{className:"text-sm text-gray-500 mt-2",children:['Debug Info: mapId = "',g,'" (type: ',typeof g,")"]}),a.jsx(L,{name:"back-to-explore",variant:"secondary",onClick:()=>x("/explore"),children:"Return to Explore"})]})});const[le,Ne]=d.useState("mapDetails"),[e,ae]=d.useState(null),[z,R]=d.useState(!0),[ce,O]=d.useState(null),[de,Ce]=d.useState([]),[ge,be]=d.useState([]),[me,Se]=d.useState([]),[ue,De]=d.useState([]),[ke,Me]=d.useState([]),[Le,Fe]=d.useState(!1),[Te,Pe]=d.useState(!1),[W,q]=d.useState(!1),[Ee,K]=d.useState(!1),[pe,Z]=d.useState(!1),[$e,te]=d.useState(!1),[Re,se]=d.useState(!1),[Ra,Aa]=d.useState("standard"),[E,za]=d.useState(80),[J,Oa]=d.useState(10),[Ua,Ba]=d.useState(10),[Wa,Ja]=d.useState(!0),[Va,Ha]=d.useState(!0),[U,Q]=d.useState(!1),[Ae,fe]=d.useState(!1),[ze,_e]=d.useState(null),[Oe,V]=d.useState(!1),[h,H]=d.useState([]),[D,A]=d.useState(0),[G,he]=d.useState(!1),{search:p,setSearch:Ga,srcFilter:y,setSrcFilter:qa,catFilter:v,setCatFilter:Ka,regionFilter:j,setRegionFilter:Za,countryFilter:w,setCountryFilter:Qa,imageTypeFilter:I,setImageTypeFilter:Xa,uploadTypeFilter:N,setUploadTypeFilter:Ya,showReferenceExamples:b,setShowReferenceExamples:Ue,clearAllFilters:Be}=ta(),We=[{key:"explore",label:"List"},{key:"mapDetails",label:"Carousel"}],xe=d.useCallback(async t=>{if(console.log("fetchMapData called with id:",t),console.log("fetchMapData id type:",typeof t),!t||t==="undefined"||t==="null"||t.trim()===""){console.log("fetchMapData: Invalid ID detected:",t),O("Invalid Map ID"),R(!1);return}if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)){console.log("fetchMapData: Invalid UUID format:",t),O("Invalid Map ID format"),R(!1);return}console.log("fetchMapData: Making API call for id:",t),q(!0),R(!0);try{const l=await fetch(`/api/images/${t}`);if(!l.ok)throw new Error("Map not found");const i=await l.json();if(ae(i),i.all_image_ids&&i.all_image_ids.length>1)await ie(i.all_image_ids);else if(i.image_count&&i.image_count>1){console.log("Multi-upload detected but no all_image_ids, trying grouped endpoint");try{const n=await fetch("/api/images/grouped");if(n.ok){const o=(await n.json()).find(u=>u.all_image_ids&&u.all_image_ids.includes(i.image_id));o&&o.all_image_ids?await ie(o.all_image_ids):(H([i]),A(0))}else H([i]),A(0)}catch(n){console.error("Failed to fetch from grouped endpoint:",n),H([i]),A(0)}}else H([i]),A(0);await X(t)}catch(l){O(l instanceof Error?l.message:"Unknown error occurred")}finally{R(!1),q(!1)}},[X,ie]),ie=d.useCallback(async t=>{console.log("fetchAllImages called with imageIds:",t),he(!0);try{const s=t.map(async i=>{const n=await fetch(`/api/images/${i}`);if(!n.ok)throw new Error(`Failed to fetch image ${i}`);return n.json()}),l=await Promise.all(s);H(l),A(0),console.log("fetchAllImages: Loaded",l.length,"images")}catch(s){console.error("fetchAllImages error:",s),O(s instanceof Error?s.message:"Failed to load all images")}finally{he(!1)}},[]),Je=d.useCallback(()=>{h.length>1&&A(t=>t>0?t-1:h.length-1)},[h.length]),Ve=d.useCallback(()=>{h.length>1&&A(t=>t<h.length-1?t+1:0)},[h.length]),He=d.useCallback(t=>{t>=0&&t<h.length&&A(t)},[h.length]),ye=d.useCallback(async t=>{const s=t||(h.length>0?h[D]:e);if(s){V(!0),_e(s),fe(!0);try{const l=new Image;l.onload=()=>{V(!1)},l.onerror=()=>{V(!1)},l.src=s.image_url}catch(l){console.error("Error preloading full-size image:",l),V(!1)}}},[h,D,e]),Ge=d.useCallback(()=>{fe(!1),_e(null),V(!1)},[]);d.useEffect(()=>{if(console.log("MapDetailsPage: mapId from useParams:",g),console.log("MapDetailsPage: mapId type:",typeof g),console.log("MapDetailsPage: mapId value:",g),!g||g==="undefined"||g==="null"||g.trim()===""||g===void 0||g===null){console.log("MapDetailsPage: Invalid mapId, setting error"),O("Map ID is required"),R(!1);return}if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(g)){console.log("MapDetailsPage: Invalid UUID format:",g),O("Invalid Map ID format"),R(!1);return}console.log("MapDetailsPage: Fetching data for mapId:",g),xe(g)},[g,xe]),d.useEffect(()=>{if(!e||z||U)return;if(!g||g==="undefined"||g==="null"||g.trim()===""){console.log("Auto-navigation skipped: Invalid mapId");return}if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(g)){console.log("Auto-navigation skipped: Invalid mapId format");return}(()=>{const l=!p||e.title?.toLowerCase().includes(p.toLowerCase())||e.generated?.toLowerCase().includes(p.toLowerCase())||e.source?.toLowerCase().includes(p.toLowerCase())||e.event_type?.toLowerCase().includes(p.toLowerCase()),i=!y||e.source===y,n=!v||e.event_type===v,r=!j||e.countries.some($=>$.r_code===j),o=!w||e.countries.some($=>$.c_code===w),u=!I||e.image_type===I,_=!b||e.starred===!0,k=l&&i&&n&&r&&o&&u&&_;return console.log("Auto-navigation check:",{mapId:g,search:p,srcFilter:y,catFilter:v,regionFilter:j,countryFilter:w,imageTypeFilter:I,showReferenceExamples:b,matchesSearch:l,matchesSource:i,matchesCategory:n,matchesRegion:r,matchesCountry:o,matchesImageType:u,matchesReferenceExamples:_,matches:k}),k})()||(console.log("Current map does not match filters, looking for first matching item"),fetch("/api/images").then(l=>l.json()).then(l=>{console.log("Auto-navigation: Received images from API:",l.length),console.log("Auto-navigation: First few images:",l.slice(0,3).map(n=>({image_id:n.image_id,title:n.title})));const i=l.find(n=>{const r=!p||n.title?.toLowerCase().includes(p.toLowerCase())||n.generated?.toLowerCase().includes(p.toLowerCase())||n.source?.toLowerCase().includes(p.toLowerCase())||n.event_type?.toLowerCase().includes(p.toLowerCase()),o=!y||n.source===y,u=!v||n.event_type===v,_=!j||n.countries?.some(f=>f.r_code===j),k=!w||n.countries?.some(f=>f.c_code===w),$=!I||n.image_type===I,F=!b||n.starred===!0;return r&&o&&u&&_&&k&&$&&F});console.log("Auto-navigation: Found first matching image:",i?{image_id:i.image_id,title:i.title,source:i.source}:"No matching image found"),i&&i.image_id&&i.image_id!=="undefined"&&i.image_id!=="null"&&i.image_id.trim()!==""&&i.image_id!==g&&(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(i.image_id)?(console.log("Auto-navigating to:",i.image_id),x(`/map/${i.image_id}`)):console.error("Auto-navigation blocked: Invalid image_id format:",i.image_id))}).catch(console.error))},[e,p,y,v,j,w,I,b,g,x,z,U]);const X=d.useCallback(async t=>{if(!(!t||t==="undefined"||t==="null"||t.trim()===""))try{const s=new URLSearchParams;p&&s.append("search",p),y&&s.append("source",y),v&&s.append("event_type",v),j&&s.append("region",j),w&&s.append("country",w),I&&s.append("image_type",I),N&&s.append("upload_type",N),b&&s.append("starred_only","true");const l=await fetch(`/api/images/grouped?${s.toString()}`);if(l.ok){const i=await l.json();console.log("Server response for upload_type=multiple:",{url:`/api/images/grouped?${s.toString()}`,count:i.length,images:i.map(r=>({image_id:r.image_id,image_count:r.image_count,all_image_ids:r.all_image_ids,all_image_ids_length:r.all_image_ids?.length}))});const n=i.findIndex(r=>r.image_id===t);console.log("Navigation availability check (server-side):",{filteredImagesCount:i.length,currentIndex:n,currentId:t,uploadTypeFilter:N,hasPrevious:i.length>1&&n>0,hasNext:i.length>1&&n<i.length-1,filteredImages:i.map(r=>({image_id:r.image_id,image_count:r.image_count,all_image_ids:r.all_image_ids,image_type:r.image_type}))}),Fe(i.length>1&&n>0),Pe(i.length>1&&n<i.length-1)}}catch(s){console.error("Failed to check navigation availability:",s)}},[p,y,v,j,w,I,N,b]),ve=async t=>{if(!W){q(!0);try{const s=new URLSearchParams;p&&s.append("search",p),y&&s.append("source",y),v&&s.append("event_type",v),j&&s.append("region",j),w&&s.append("country",w),I&&s.append("image_type",I),N&&s.append("upload_type",N),b&&s.append("starred_only","true");const l=await fetch(`/api/images/grouped?${s.toString()}`);if(l.ok){const i=await l.json(),n=i.findIndex(u=>u.image_id===g);if(n===-1){console.error("Current image not found in filtered list");return}let r;t==="previous"?r=n>0?n-1:i.length-1:r=n<i.length-1?n+1:0;const o=i[r];o&&o.image_id&&o.image_id!=="undefined"&&o.image_id!=="null"&&o.image_id.trim()!==""&&(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(o.image_id)?(console.log("Carousel navigating to:",o.image_id),x(`/map/${o.image_id}`)):console.error("Carousel navigation blocked: Invalid image_id format:",o.image_id))}}catch(s){console.error("Failed to navigate to item:",s)}finally{q(!1)}}};d.useEffect(()=>{console.log("=== NAVIGATION USEEFFECT TRIGGERED ==="),console.log("Navigation useEffect triggered:",{map:!!e,mapId:g,loading:z,isDeleting:U,uploadTypeFilter:N,allFilters:{search:p,srcFilter:y,catFilter:v,regionFilter:j,countryFilter:w,imageTypeFilter:I,uploadTypeFilter:N,showReferenceExamples:b}}),e&&g&&!z&&!U?(console.log("Calling checkNavigationAvailability with:",g),X(g)):console.log("NOT calling checkNavigationAvailability because:",{map:!!e,mapId:!!g,loading:z,isDeleting:U})},[e,g,p,y,v,j,w,I,N,b,z,U,X]),d.useEffect(()=>{Promise.all([fetch("/api/sources").then(t=>t.json()),fetch("/api/types").then(t=>t.json()),fetch("/api/image-types").then(t=>t.json()),fetch("/api/regions").then(t=>t.json()),fetch("/api/countries").then(t=>t.json())]).then(([t,s,l,i,n])=>{Ce(t),be(s),Se(l),De(i),Me(n)}).catch(console.error)},[]);const qe=async()=>{e&&K(!0)},Ke=async()=>{if(e)try{(await fetch(`/api/images/${e.image_id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({starred:!e.starred})})).ok?ae(s=>s?{...s,starred:!s.starred}:null):console.error("Failed to toggle starred status")}catch(t){console.error("Error toggling starred status:",t)}},Ze=async()=>{if(e){Q(!0);try{if(console.log("Deleting image with ID:",e.image_id),(await fetch(`/api/images/${e.image_id}`,{method:"DELETE"})).ok){ae(s=>s?{...s,starred:!s.starred}:null),K(!1);try{const s=await fetch("/api/images/grouped");if(s.ok){const i=(await s.json()).filter(r=>{const o=!p||r.title?.toLowerCase().includes(p.toLowerCase())||r.generated?.toLowerCase().includes(p.toLowerCase())||r.source?.toLowerCase().includes(p.toLowerCase())||r.event_type?.toLowerCase().includes(p.toLowerCase()),u=!y||r.source===y,_=!v||r.event_type===v,k=!j||r.countries?.some(C=>C.r_code===j),$=!w||r.countries?.some(C=>C.c_code===w),F=!I||r.image_type===I,f=!N||N==="single"&&(!r.image_count||r.image_count<=1)||N==="multiple"&&r.image_count&&r.image_count>1,M=!b||r.starred===!0;return o&&u&&_&&k&&$&&F&&f&&M}),n=i.filter(r=>r.image_id!==e.image_id);if(n.length>0){const r=i.findIndex(u=>u.image_id===e.image_id);let o;if(r===i.length-1?o=r-1:o=r,console.log("Navigation target:",{currentIndex:r,targetIndex:o,targetId:n[o]?.image_id}),o>=0&&o<n.length){const u=n[o];u&&u.image_id&&u.image_id!=="undefined"&&u.image_id!=="null"&&u.image_id.trim()!==""?/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(u.image_id)?(console.log("Navigating to:",u.image_id),x(`/map/${u.image_id}`)):(console.error("Navigation blocked: Invalid image_id format:",u.image_id),x("/explore")):(console.error("Navigation blocked: Invalid image_id:",u?.image_id),x("/explore"))}else n[0]&&n[0].image_id&&n[0].image_id!=="undefined"&&n[0].image_id!=="null"&&n[0].image_id.trim()!==""?/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(n[0].image_id)?(console.log("Fallback navigation to first item:",n[0].image_id),x(`/map/${n[0].image_id}`)):(console.error("Fallback navigation blocked: Invalid image_id format:",n[0].image_id),x("/explore")):(console.log("No valid remaining items, going to explore page"),x("/explore"))}else console.log("No remaining items, going to explore page"),x("/explore")}else x("/explore")}catch(s){console.error("Failed to navigate to next item:",s),x("/explore")}finally{Q(!1)}}else console.error("Delete failed"),Q(!1)}catch(t){console.error("Delete failed:",t),Q(!1)}}},c=d.useMemo(()=>{if(!e)return null;if(!p&&!y&&!v&&!j&&!w&&!I&&!N&&!b)return e;const t=!p||e.title?.toLowerCase().includes(p.toLowerCase())||e.generated?.toLowerCase().includes(p.toLowerCase())||e.source?.toLowerCase().includes(p.toLowerCase())||e.event_type?.toLowerCase().includes(p.toLowerCase()),s=!y||e.source===y,l=!v||e.event_type===v,i=!j||e.countries.some(k=>k.r_code===j),n=!w||e.countries.some(k=>k.c_code===w),r=!I||e.image_type===I,o=!N||N==="single"&&(!e.image_count||e.image_count<=1)&&(!e.all_image_ids||e.all_image_ids.length<=1)||N==="multiple"&&(e.image_count&&e.image_count>1||e.all_image_ids&&e.all_image_ids.length>1),u=!b||e.starred===!0,_=t&&s&&l&&i&&n&&r&&o&&u;return!_&&(p||y||v||j||w||I||N||b)?(setTimeout(()=>{je()},100),e):_?e:null},[e,p,y,v,j,w,I,N,b,je]),je=d.useCallback(async()=>{R(!0);try{const t=new URLSearchParams;p&&t.append("search",p),y&&t.append("source",y),v&&t.append("event_type",v),j&&t.append("region",j),w&&t.append("country",w),I&&t.append("image_type",I),N&&t.append("upload_type",N),b&&t.append("starred_only","true");const s=await fetch(`/api/images/grouped?${t.toString()}`);if(s.ok){const l=await s.json();if(l.length>0){const i=l[0];i&&i.image_id&&x(`/map/${i.image_id}`)}else x("/explore")}}catch(t){console.error("Failed to navigate to matching image:",t),x("/explore")}finally{R(!1)}},[p,y,v,j,w,I,N,b,x]),Qe=()=>{if(!e)return;if(!e.all_image_ids||e.all_image_ids.length<=1){const i=`/upload?step=1&contribute=true&imageIds=${[e.image_id].join(",")}`;x(i);return}const s=`/upload?step=1&contribute=true&imageIds=${e.all_image_ids.join(",")}`;x(s)},T=(t,s)=>({image:`images/${s}`,caption:t.edited||t.generated||"",metadata:{image_id:t.image_count&&t.image_count>1?t.all_image_ids||[t.image_id]:t.image_id,title:t.title,source:t.source,event_type:t.event_type,image_type:t.image_type,countries:t.countries,starred:t.starred,image_count:t.image_count||1}}),Xe=async t=>{if(e){te(!0),se(!1);try{const s=(await oa(async()=>{const{default:o}=await import("./jszip.min-uuN5m5dK.js").then(u=>u.j);return{default:o}},__vite__mapDeps([0,1,2]))).default,l=new s;if(e.image_type==="crisis_map"){const o=l.folder("crisis_maps_dataset"),u=o?.folder("images");if(u)try{const _=e.image_count&&e.image_count>1?e.all_image_ids||[e.image_id]:[e.image_id],k=_.map(async(f,M)=>{try{const C=await fetch(`/api/images/${f}/file`);if(!C.ok)throw new Error(`Failed to fetch image ${f}`);const S=await C.blob(),Y=e.file_key.split(".").pop()||"jpg",B=`0001_${String(M+1).padStart(2,"0")}.${Y}`;return u.file(B,S),{success:!0,fileName:B,imageId:f}}catch(C){return console.error(`Failed to process image ${f}:`,C),{success:!1,fileName:"",imageId:f}}}),F=(await Promise.all(k)).filter(f=>f.success);if(F.length===0)throw new Error("No images could be processed");if(t==="fine-tuning"){const f=[],M=[],C=[],S=F.map(Ye=>`images/${Ye.fileName}`),Y=Math.random(),B={image:S.length===1?S[0]:S,caption:e.edited||e.generated||"",metadata:{image_id:_,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};Y<E/100?f.push(B):Y<(E+J)/100?M.push(B):C.push(B),o&&(o.file("train.jsonl",JSON.stringify(f,null,2)),o.file("test.jsonl",JSON.stringify(M,null,2)),o.file("val.jsonl",JSON.stringify(C,null,2)))}else{const f=F.map(C=>`images/${C.fileName}`),M={image:f.length===1?f[0]:f,caption:e.edited||e.generated||"",metadata:{image_id:_,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};o&&o.file("0001.json",JSON.stringify(M,null,2))}}catch(_){throw console.error(`Failed to process image ${e.image_id}:`,_),_}}else if(e.image_type==="drone_image"){const o=l.folder("drone_images_dataset"),u=o?.folder("images");if(u)try{const _=await fetch(`/api/images/${e.image_id}/file`);if(!_.ok)throw new Error(`Failed to fetch image ${e.image_id}`);const k=await _.blob(),F=`0001.${e.file_key.split(".").pop()||"jpg"}`;if(u.file(F,k),t==="fine-tuning"){const f=[],M=[],C=[];if(String(e?.image_type)==="crisis_map"){const S=Math.random();S<E/100?f.push(T(e,"0001")):S<(E+J)/100?M.push(T(e,"0001")):C.push(T(e,"0001"))}else if(String(e?.image_type)==="drone_image"){const S=Math.random();S<E/100?f.push(T(e,"0001")):S<(E+J)/100?M.push(T(e,"0001")):C.push(T(e,"0001"))}o&&(o.file("train.jsonl",JSON.stringify(f,null,2)),o.file("test.jsonl",JSON.stringify(M,null,2)),o.file("val.jsonl",JSON.stringify(C,null,2)))}else{const f={image:`images/${F}`,caption:e.edited||e.generated||"",metadata:{image_id:e.image_count&&e.image_count>1?e.all_image_ids||[e.image_id]:e.image_id,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};o&&o.file("0001.json",JSON.stringify(f,null,2))}}catch(_){throw console.error(`Failed to process image ${e.image_id}:`,_),_}}else{const o=l.folder("generic_dataset"),u=o?.folder("images");if(u)try{const _=await fetch(`/api/images/${e.image_id}/file`);if(!_.ok)throw new Error(`Failed to fetch image ${e.image_id}`);const k=await _.blob(),F=`0001.${e.file_key.split(".").pop()||"jpg"}`;if(u.file(F,k),t==="fine-tuning"){const f=[],M=[],C=[];if(String(e?.image_type)==="crisis_map"){const S=Math.random();S<E/100?f.push(T(e,"0001")):S<(E+J)/100?M.push(T(e,"0001")):C.push(T(e,"0001"))}else if(String(e?.image_type)==="drone_image"){const S=Math.random();S<E/100?f.push(T(e,"0001")):S<(E+J)/100?M.push(T(e,"0001")):C.push(T(e,"0001"))}o&&(o.file("train.jsonl",JSON.stringify(f,null,2)),o.file("test.jsonl",JSON.stringify(M,null,2)),o.file("val.jsonl",JSON.stringify(C,null,2)))}else{const f={image:`images/${F}`,caption:e.edited||e.generated||"",metadata:{image_id:e.image_count&&e.image_count>1?e.all_image_ids||[e.image_id]:e.image_id,title:e.title,source:e.source,event_type:e.event_type,image_type:e.image_type,countries:e.countries,starred:e.starred,image_count:e.image_count||1}};o&&o.file("0001.json",JSON.stringify(f,null,2))}}catch(_){throw console.error(`Failed to process image ${e.image_id}:`,_),_}}const i=await l.generateAsync({type:"blob"}),n=URL.createObjectURL(i),r=document.createElement("a");r.href=n,r.download=`dataset_${e.image_type}_${e.image_id}_${t}_${new Date().toISOString().split("T")[0]}.zip`,document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(n),console.log(`Exported ${e.image_type} dataset with 1 image in ${t} mode`),se(!0)}catch(s){console.error("Export failed:",s),alert("Failed to export dataset. Please try again.")}finally{te(!1)}}};return z?a.jsx(ee,{children:a.jsx("div",{className:m.loadingContainer,children:a.jsxs("div",{className:"flex flex-col items-center gap-4",children:[a.jsx(we,{className:"text-ifrcRed"}),a.jsx("div",{children:"Loading map details..."})]})})}):ce||!e?a.jsx(ee,{children:a.jsx("div",{className:m.errorContainer,children:a.jsxs("div",{className:"flex flex-col items-center gap-4 text-center",children:[a.jsx("div",{className:"text-4xl",children:"β οΈ"}),a.jsx("div",{className:"text-xl font-semibold",children:"Unable to load map"}),a.jsx("div",{children:ce||"Map not found"}),a.jsx(L,{name:"back-to-explore",variant:"secondary",onClick:()=>x("/explore"),children:"Return to Explore"})]})})}):a.jsxs(ee,{children:[a.jsxs("div",{className:"max-w-7xl mx-auto",children:[a.jsxs("div",{className:m.tabSelector,children:[a.jsx(sa,{name:"map-details-view",value:le,onChange:t=>{(t==="mapDetails"||t==="explore")&&(Ne(t),t==="explore"&&x("/explore"))},options:We,keySelector:t=>t.key,labelSelector:t=>t.label}),a.jsxs("div",{className:"flex items-center gap-2 ml-auto",children:[a.jsx(P,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:a.jsxs(L,{name:"reference-examples",variant:b?"primary":"secondary",onClick:()=>Ue(!b),className:"whitespace-nowrap",children:[a.jsx("span",{className:"mr-2",children:b?a.jsx("span",{className:"text-yellow-400",children:"β
"}):a.jsx("span",{className:"text-yellow-400",children:"β"})}),"Reference Examples"]})}),a.jsx(L,{name:"export-dataset",variant:"secondary",onClick:()=>Z(!0),children:"Export"})]})]}),a.jsx(la,{sources:de,types:ge,regions:ue,countries:ke,imageTypes:me,isLoadingFilters:!1}),le==="mapDetails"?a.jsx("div",{className:"relative",children:c?a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:m.gridLayout,children:[a.jsxs(P,{heading:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{children:c.title||"Map Image"}),c.starred&&a.jsx("span",{className:"text-red-500 text-xl",title:"Starred image",children:"β
"})]}),headingLevel:2,withHeaderBorder:!0,withInternalPadding:!0,spacing:"comfortable",children:[a.jsx("div",{className:m.imageContainer,children:e?.image_count&&e.image_count>1||h.length>1?a.jsxs("div",{className:m.carouselContainer,children:[a.jsx("div",{className:m.carouselImageWrapper,children:G?a.jsxs("div",{className:m.imagePlaceholder,children:[a.jsx(we,{className:"text-ifrcRed"}),a.jsx("div",{children:"Loading images..."})]}):h[D]?.detail_url?a.jsx("img",{src:h[D].detail_url,alt:h[D].file_key,className:m.carouselImage,onError:t=>{console.log("MapDetailsPage: Detail image failed to load, falling back to original:",h[D].detail_url);const s=t.target;h[D].image_url&&(s.src=h[D].image_url)},onLoad:()=>console.log("MapDetailsPage: Detail image loaded successfully:",h[D].detail_url)}):h[D]?.image_url?a.jsx("img",{src:h[D].image_url,alt:h[D].file_key,className:m.carouselImage,onLoad:()=>console.log("MapDetailsPage: Original image loaded successfully:",h[D].image_url)}):a.jsx("div",{className:m.imagePlaceholder,children:"No image available"})}),a.jsxs("div",{className:m.carouselNavigation,children:[a.jsx(L,{name:"previous-image",variant:"tertiary",size:1,onClick:Je,disabled:G,className:m.carouselButton,children:a.jsx(ne,{className:"w-4 h-4"})}),a.jsx("div",{className:m.carouselIndicators,children:h.map((t,s)=>a.jsx("button",{onClick:()=>He(s),className:`${m.carouselIndicator} ${s===D?m.carouselIndicatorActive:""}`,disabled:G,children:s+1},s))}),a.jsx(L,{name:"next-image",variant:"tertiary",size:1,onClick:Ve,disabled:G,className:m.carouselButton,children:a.jsx(oe,{className:"w-4 h-4"})})]}),a.jsx("div",{className:m.viewImageButtonContainer,children:a.jsx(L,{name:"view-full-size-carousel",variant:"secondary",size:1,onClick:()=>ye(h[D]),disabled:G||!h[D]?.image_url,children:"View Image"})})]}):a.jsxs("div",{className:m.singleImageContainer,children:[c.detail_url?a.jsx("img",{src:c.detail_url,alt:c.file_key,onError:t=>{console.log("MapDetailsPage: Detail image failed to load, falling back to original:",c.detail_url);const s=t.target;c.image_url&&(s.src=c.image_url)},onLoad:()=>console.log("MapDetailsPage: Detail image loaded successfully:",c.detail_url)}):c.image_url?a.jsx("img",{src:c.image_url,alt:c.file_key,onLoad:()=>console.log("MapDetailsPage: Original image loaded successfully:",c.image_url)}):a.jsx("div",{className:m.imagePlaceholder,children:"No image available"}),a.jsx("div",{className:m.viewImageButtonContainer,children:a.jsx(L,{name:"view-full-size-single",variant:"secondary",size:1,onClick:()=>ye(c),disabled:!c.image_url,children:"View Image"})})]})}),a.jsx(P,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:a.jsxs("div",{className:m.metadataTags,children:[c.image_type!=="drone_image"&&a.jsx("span",{className:m.metadataTag,children:de.find(t=>t.s_code===c.source)?.label||c.source}),a.jsx("span",{className:m.metadataTag,children:ge.find(t=>t.t_code===c.event_type)?.label||c.event_type}),a.jsx("span",{className:m.metadataTag,children:me.find(t=>t.image_type===c.image_type)?.label||c.image_type}),c.countries&&c.countries.length>0&&a.jsxs(a.Fragment,{children:[a.jsx("span",{className:m.metadataTag,children:ue.find(t=>t.r_code===c.countries[0].r_code)?.label||"Unknown Region"}),a.jsx("span",{className:m.metadataTag,children:c.countries.map(t=>t.label).join(", ")})]}),c.image_count&&c.image_count>1&&a.jsxs("span",{className:m.metadataTag,title:`Multi-upload with ${c.image_count} images`,children:["π· ",c.image_count]}),(!c.image_count||c.image_count<=1)&&a.jsx("span",{className:m.metadataTag,title:"Single Upload",children:"Single"})]})})]}),a.jsx("div",{className:m.detailsSection,children:c.edited&&c.edited.includes("Description:")||c.generated&&c.generated.includes("Description:")?a.jsx(P,{heading:"AI Generated Content",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,spacing:"comfortable",children:a.jsx("div",{className:m.captionContainer,children:a.jsx("div",{className:m.captionText,children:(c.edited||c.generated||"").split(/(Description:|Analysis:|Recommended Actions:)/).map((l,i)=>l.trim()===""?null:l==="Description:"||l==="Analysis:"||l==="Recommended Actions:"?a.jsx("h4",{className:"font-semibold text-gray-800 mt-4 mb-2",children:l},i):a.jsx("p",{className:"mb-2",children:l.trim()},i))})})}):a.jsx(P,{heading:"Description",headingLevel:3,withHeaderBorder:!0,withInternalPadding:!0,spacing:"comfortable",children:a.jsx("div",{className:m.captionContainer,children:c.generated||c.edited?a.jsx("div",{className:m.captionText,children:a.jsx("p",{children:c.edited||c.generated})}):a.jsx("p",{children:"β no caption yet β"})})})})]}),a.jsx("div",{className:"flex items-center justify-center mt-8",children:a.jsx(P,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-lg p-4",children:a.jsxs("div",{className:"flex items-center gap-4",children:[Le&&a.jsx(P,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(L,{name:"previous-item",variant:"tertiary",size:1,className:`bg-white/90 hover:bg-white shadow-lg border border-gray-200 ${W?"opacity-50 cursor-not-allowed":"hover:scale-110"}`,onClick:()=>ve("previous"),disabled:W,children:a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsxs("div",{className:"flex -space-x-1",children:[a.jsx(ne,{className:"w-4 h-4"}),a.jsx(ne,{className:"w-4 h-4"})]}),a.jsx("span",{className:"font-semibold",children:"Previous"})]})})}),re&&a.jsx(P,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(L,{name:"delete",variant:"tertiary",size:1,className:"bg-red-50 hover:bg-red-100 text-red-700 border border-red-200 hover:border-red-300",onClick:qe,title:"Delete","aria-label":"Delete saved image",children:a.jsx(ia,{className:"w-4 h-4"})})}),a.jsx(P,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(L,{name:"contribute",onClick:Qe,children:"Contribute"})}),re&&a.jsx(P,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(L,{name:"toggle-star",variant:"tertiary",size:1,className:`${e?.starred?"bg-red-100 hover:bg-red-200 text-red-800 border-2 border-red-400":"bg-gray-100 hover:bg-gray-200 text-gray-600 border-2 border-gray-300"} w-16 h-8 rounded-full transition-all duration-200 flex items-center justify-center`,onClick:Ke,title:e?.starred?"Unstar image":"Star image","aria-label":e?.starred?"Unstar image":"Star image",children:a.jsx("span",{className:`text-lg transition-all duration-200 ${e?.starred?"text-red-600":"text-gray-500"}`,children:e?.starred?"β
":"β"})})}),Te&&a.jsx(P,{withInternalPadding:!0,className:"rounded-md p-2",children:a.jsx(L,{name:"next-item",variant:"tertiary",size:1,className:`bg-white/90 hover:bg-white shadow-lg border border-gray-200 ${W?"opacity-50 cursor-not-allowed":"hover:scale-110"}`,onClick:()=>ve("next"),disabled:W,children:a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx("span",{className:"font-semibold",children:"Next"}),a.jsxs("div",{className:"flex -space-x-1",children:[a.jsx(oe,{className:"w-4 h-4"}),a.jsx(oe,{className:"w-4 h-4"})]})]})})})]})})})]}):a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"text-xl font-semibold text-gray-600 mb-4",children:"No matches found"}),a.jsx("div",{className:"mt-4",children:a.jsx(L,{name:"clear-filters",variant:"secondary",onClick:Be,children:"Clear Filters"})})]})}):null]}),Ee&&a.jsx("div",{className:m.fullSizeModalOverlay,onClick:()=>K(!1),children:a.jsx("div",{className:m.fullSizeModalContent,onClick:t=>t.stopPropagation(),children:a.jsxs("div",{className:m.ratingWarningContent,children:[a.jsx("h3",{className:m.ratingWarningTitle,children:"Delete Image?"}),a.jsx("p",{className:m.ratingWarningText,children:"This action cannot be undone. Are you sure you want to delete this saved image and all related data?"}),a.jsxs("div",{className:m.ratingWarningButtons,children:[a.jsx(L,{name:"confirm-delete",variant:"secondary",onClick:Ze,children:"Delete"}),a.jsx(L,{name:"cancel-delete",variant:"tertiary",onClick:()=>K(!1),children:"Cancel"})]})]})})}),pe&&a.jsx(ca,{isOpen:pe,onClose:()=>{Z(!1),se(!1),te(!1)},onExport:(t,s)=>{s.includes(e.image_type)&&Xe(t)},filteredCount:1,totalCount:1,hasFilters:!1,crisisMapsCount:e.image_type==="crisis_map"?1:0,droneImagesCount:e.image_type==="drone_image"?1:0,isLoading:$e,exportSuccess:Re,variant:"single",onNavigateToList:()=>{Z(!1),x("/explore")},onNavigateAndExport:()=>{Z(!1),x("/explore?export=true")}}),a.jsx(na,{isOpen:Ae,imageUrl:ze?.image_url||null,preview:null,selectedImageData:null,onClose:Ge,isLoading:Oe})]})}export{st as default};
         | 
|  | |
|  | |
|  | 
    	
        py_backend/static/assets/{index-CWQiqOU3.js β index-hD4RFL5O.js}
    RENAMED
    
    | @@ -1,2 +1,2 @@ | |
| 1 | 
            -
            const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/jszip.min- | 
| 2 | 
            -
            import{j as t,z as b,n as w,v as Fe,w as De,x as Oe,B as Le,r,D as Me,N as Re,_ as me,L as Je,F as ze,G as We}from"./index-B0L3qvtx.js";import{u as Ue}from"./useAdmin-DN0iYnPm.js";import{F as Be,E as He}from"./ExportModal-BI_JlHOZ.js";const Ve="_paginatorContainer_1l5ti_1",Ae="_paginationControls_1l5ti_19",ge={paginatorContainer:Ve,paginationControls:Ae};function Ge({currentPage:N,totalPages:u,onPageChange:$,className:z=""}){if(u<=1)return null;const f=(()=>{const h=[];if(u<=5)for(let p=1;p<=u;p++)h.push(p);else{let p=Math.max(1,N-2);const _=Math.min(u,p+5-1);_===u&&(p=Math.max(1,_-5+1));for(let y=p;y<=_;y++)h.push(y)}return h})();return t.jsx("div",{className:`${ge.paginatorContainer} ${z}`,children:t.jsxs("div",{className:ge.paginationControls,children:[t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsxs(w,{name:"prev-page",variant:"tertiary",size:1,onClick:()=>$(Math.max(1,N-1)),disabled:N===1,title:"Previous page",children:[t.jsx(Fe,{className:"w-4 h-4"}),t.jsx("span",{className:"hidden sm:inline",children:"Previous"})]})}),t.jsxs("div",{className:"flex items-center gap-1",children:[f[0]>1&&t.jsxs(t.Fragment,{children:[t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:"page-1",variant:"tertiary",size:1,onClick:()=>$(1),children:"1"})}),f[0]>2&&t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx("span",{className:"px-2 text-gray-500",children:"..."})})]}),f.map(h=>t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:`page-${h}`,variant:N===h?"primary":"tertiary",size:1,onClick:()=>$(h),children:h})},h)),f[f.length-1]<u&&t.jsxs(t.Fragment,{children:[f[f.length-1]<u-1&&t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx("span",{className:"px-2 text-gray-500",children:"..."})}),t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:`page-${u}`,variant:"tertiary",size:1,onClick:()=>$(u),children:u})})]})]}),t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsxs(w,{name:"next-page",variant:"tertiary",size:1,onClick:()=>$(Math.min(u,N+1)),disabled:N===u,title:"Next page",children:[t.jsx("span",{className:"hidden sm:inline",children:"Next"}),t.jsx(De,{className:"w-4 h-4"})]})})]})})}const Ze="_tabSelector_o9y1f_1",qe="_metadataTags_o9y1f_8",Ke="_metadataTag_o9y1f_8",Qe="_metadataTagSource_o9y1f_32",Xe="_metadataTagType_o9y1f_43",Ye="_mapItem_o9y1f_54",et="_mapItemImage_o9y1f_72",tt="_mapItemContent_o9y1f_92",at="_mapItemTitle_o9y1f_97",st="_mapItemMetadata_o9y1f_105",it="_fullSizeModalOverlay_o9y1f_134",nt="_fullSizeModalContent_o9y1f_148",lt="_ratingWarningContent_o9y1f_159",rt="_ratingWarningTitle_o9y1f_165",ot="_ratingWarningText_o9y1f_172",ct="_ratingWarningButtons_o9y1f_179",m={tabSelector:Ze,metadataTags:qe,metadataTag:Ke,metadataTagSource:Qe,metadataTagType:Xe,mapItem:Ye,mapItemImage:et,mapItemContent:tt,mapItemTitle:at,mapItemMetadata:st,fullSizeModalOverlay:it,fullSizeModalContent:nt,ratingWarningContent:lt,ratingWarningTitle:rt,ratingWarningText:ot,ratingWarningButtons:ct};function pt(){const N=Oe(),u=Le(),{isAuthenticated:$}=Ue(),[z,Y]=r.useState("explore"),[f,h]=r.useState([]),{search:S,srcFilter:p,catFilter:_,regionFilter:y,countryFilter:E,imageTypeFilter:I,uploadTypeFilter:k,showReferenceExamples:T,setShowReferenceExamples:pe}=Me(),[V,ue]=r.useState([]),[A,fe]=r.useState([]),[ee,he]=r.useState([]),[xe,_e]=r.useState([]),[te,ye]=r.useState([]),[je,ae]=r.useState(!0),[W,se]=r.useState(!0),[Ne,G]=r.useState(!1),[ve,Z]=r.useState(!1),[be,q]=r.useState(!1),[we,U]=r.useState(!1),[B,ie]=r.useState(""),[K,ne]=r.useState(!1),[L,le]=r.useState(1),[M]=r.useState(10),[Q,re]=r.useState(0),[Se,oe]=r.useState(0),Te=[{key:"explore",label:"List"},{key:"mapDetails",label:"Carousel"}],H=r.useCallback(()=>{se(!0);const e=new URLSearchParams({page:L.toString(),limit:M.toString()});S&&e.append("search",S),p&&e.append("source",p),_&&e.append("event_type",_),y&&e.append("region",y),E&&e.append("country",E),I&&e.append("image_type",I),k&&e.append("upload_type",k),T&&e.append("starred_only","true"),fetch(`/api/images/grouped?${e.toString()}`).then(s=>s.ok?s.json():(console.error("ExplorePage: Grouped endpoint failed, trying legacy endpoint"),fetch("/api/captions/legacy").then(c=>c.ok?c.json():(console.error("ExplorePage: Legacy endpoint failed, trying regular images endpoint"),fetch("/api/images").then(j=>{if(!j.ok)throw new Error(`HTTP ${j.status}: ${j.statusText}`);return j.json()}))))).then(s=>{console.log("ExplorePage: Fetched captions:",s),h(s)}).catch(s=>{console.error("ExplorePage: Error fetching captions:",s),h([])}).finally(()=>{se(!1)})},[L,S,p,_,y,E,I,k,T,M]),ce=r.useCallback(()=>{const e=new URLSearchParams;S&&e.append("search",S),p&&e.append("source",p),_&&e.append("event_type",_),y&&e.append("region",y),E&&e.append("country",E),I&&e.append("image_type",I),k&&e.append("upload_type",k),T&&e.append("starred_only","true"),fetch(`/api/images/grouped/count?${e.toString()}`).then(s=>s.ok?s.json():(console.error("ExplorePage: Count endpoint failed"),{total_count:0})).then(s=>{console.log("ExplorePage: Total count:",s.total_count),re(s.total_count),oe(Math.ceil(s.total_count/M))}).catch(s=>{console.error("ExplorePage: Error fetching total count:",s),re(0),oe(0)})},[S,p,_,y,E,I,k,T,M]);r.useEffect(()=>{H(),ce()},[H,ce]),r.useEffect(()=>{L!==1&&le(1)},[L]),r.useEffect(()=>{const e=()=>{document.hidden||H()};return document.addEventListener("visibilitychange",e),()=>{document.removeEventListener("visibilitychange",e)}},[H]),r.useEffect(()=>{new URLSearchParams(u.search).get("export")==="true"&&(G(!0),N("/explore",{replace:!0}))},[u.search,N,S,p,_,y,E,I,T]),r.useEffect(()=>{ae(!0),Promise.all([fetch("/api/sources").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/types").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/regions").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/countries").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/image-types").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()})]).then(([e,s,c,j,P])=>{ue(e),fe(s),he(c),_e(j),ye(P)}).catch(()=>{}).finally(()=>{ae(!1)})},[]);const C=f,Ee=async(e,s="fine-tuning")=>{if(e.length===0){alert("No images to export");return}Z(!0),q(!1);try{const c=(await We(async()=>{const{default:i}=await import("./jszip.min-uuN5m5dK.js").then(F=>F.j);return{default:i}},__vite__mapDeps([0,1,2]))).default,j=new c,P=e.filter(i=>i.image_type==="crisis_map"),R=e.filter(i=>i.image_type==="drone_image");if(P.length>0){const i=j.folder("crisis_maps_dataset"),F=i?.folder("images");if(F){let D=1;for(const a of P)try{const v=a.image_count&&a.image_count>1?a.all_image_ids||[a.image_id]:[a.image_id],X=v.map(async(n,x)=>{try{const l=await fetch(`/api/images/${n}/file`);if(!l.ok)throw new Error(`Failed to fetch image ${n}`);const o=await l.blob(),d=a.file_key.split(".").pop()||"jpg",g=`${String(D).padStart(4,"0")}_${String(x+1).padStart(2,"0")}.${d}`;return F.file(g,o),{success:!0,fileName:g,imageId:n}}catch(l){return console.error(`Failed to process image ${n}:`,l),{success:!1,fileName:"",imageId:n}}}),O=(await Promise.all(X)).filter(n=>n.success);if(O.length>0){if(s==="fine-tuning"){const n=O.map(o=>`images/${o.fileName}`),x=Math.random(),l={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};if(!i)continue;if(x<.8){const o=i.file("train.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("train.jsonl",JSON.stringify(d,null,2))}else i.file("train.jsonl",JSON.stringify([l],null,2))}else if(x<.9){const o=i.file("test.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("test.jsonl",JSON.stringify(d,null,2))}else i.file("test.jsonl",JSON.stringify([l],null,2))}else{const o=i.file("val.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("val.jsonl",JSON.stringify(d,null,2))}else i.file("val.jsonl",JSON.stringify([l],null,2))}}else{const n=O.map(l=>`images/${l.fileName}`),x={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};i&&i.file(`${String(D).padStart(4,"0")}.json`,JSON.stringify(x,null,2))}D++}}catch(v){console.error(`Failed to process caption ${a.image_id}:`,v)}}}if(R.length>0){const i=j.folder("drone_images_dataset"),F=i?.folder("images");if(F){let D=1;for(const a of R)try{const v=a.image_count&&a.image_count>1?a.all_image_ids||[a.image_id]:[a.image_id],X=v.map(async(n,x)=>{try{const l=await fetch(`/api/images/${n}/file`);if(!l.ok)throw new Error(`Failed to fetch image ${n}`);const o=await l.blob(),d=a.file_key.split(".").pop()||"jpg",g=`${String(D).padStart(4,"0")}_${String(x+1).padStart(2,"0")}.${d}`;return F.file(g,o),{success:!0,fileName:g,imageId:n}}catch(l){return console.error(`Failed to process image ${n}:`,l),{success:!1,fileName:"",imageId:n}}}),O=(await Promise.all(X)).filter(n=>n.success);if(O.length>0){if(s==="fine-tuning"){const n=O.map(o=>`images/${o.fileName}`),x=Math.random(),l={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};if(!i)continue;if(x<.8){const o=i.file("train.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("train.jsonl",JSON.stringify(d,null,2))}else i.file("train.jsonl",JSON.stringify([l],null,2))}else if(x<.9){const o=i.file("test.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("test.jsonl",JSON.stringify(d,null,2))}else i.file("test.jsonl",JSON.stringify([l],null,2))}else{const o=i.file("val.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("val.jsonl",JSON.stringify(d,null,2))}else i.file("val.jsonl",JSON.stringify([l],null,2))}}else{const n=O.map(l=>`images/${l.fileName}`),x={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};i&&i.file(`${String(D).padStart(4,"0")}.json`,JSON.stringify(x,null,2))}D++}}catch(v){console.error(`Failed to process caption ${a.image_id}:`,v)}}}const $e=await j.generateAsync({type:"blob"}),de=URL.createObjectURL($e),J=document.createElement("a");J.href=de,J.download=`datasets_${s}_${new Date().toISOString().split("T")[0]}.zip`,document.body.appendChild(J),J.click(),document.body.removeChild(J),URL.revokeObjectURL(de);const ke=(P.length||0)+(R.length||0);console.log(`Exported ${s} datasets with ${ke} total images:`),P.length>0&&console.log(`- Crisis maps: ${P.length} images`),R.length>0&&console.log(`- Drone images: ${R.length} images`),q(!0)}catch(c){console.error("Export failed:",c),alert("Failed to export dataset. Please try again.")}finally{Z(!1)}},Ie=e=>{ie(e),U(!0)},Ce=async()=>{if(B){ne(!0);try{console.log("Deleting image with ID:",B),(await fetch(`/api/images/${B}`,{method:"DELETE"})).ok?(h(s=>s.filter(c=>c.image_id!==B)),U(!1),ie("")):(console.error("Delete failed"),alert("Failed to delete image. Please try again."))}catch(e){console.error("Delete failed:",e),alert("Failed to delete image. Please try again.")}finally{ne(!1)}}};return t.jsxs(Re,{children:[W?t.jsx("div",{className:"flex flex-col items-center justify-center min-h-[60vh]",children:t.jsxs("div",{className:"flex flex-col items-center gap-4",children:[t.jsx(me,{className:"text-ifrcRed"}),t.jsx("div",{children:"Loading examples..."})]})}):t.jsxs("div",{className:"max-w-7xl mx-auto",children:[t.jsxs("div",{className:m.tabSelector,children:[t.jsx(Je,{name:"explore-view",value:z,onChange:e=>{(e==="explore"||e==="mapDetails")&&(Y(e),e==="mapDetails"&&f.length>0&&(f[0]?.image_id&&f[0].image_id!=="undefined"&&f[0].image_id!=="null"?N(`/map/${f[0].image_id}`):console.error("Invalid image_id for navigation:",f[0]?.image_id)))},options:Te,keySelector:e=>e.key,labelSelector:e=>e.label}),t.jsxs("div",{className:"flex items-center gap-2 ml-auto",children:[t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsxs(w,{name:"reference-examples",variant:T?"primary":"secondary",onClick:()=>pe(!T),className:"whitespace-nowrap",children:[t.jsx("span",{className:"mr-2",children:T?t.jsx("span",{className:"text-yellow-400",children:"β
"}):t.jsx("span",{className:"text-yellow-400",children:"β"})}),"Reference Examples"]})}),t.jsx(w,{name:"export-dataset",variant:"secondary",onClick:()=>G(!0),children:"Export"})]})]}),z==="explore"?t.jsxs("div",{className:"space-y-6",children:[t.jsx("div",{className:"mb-6 space-y-4",children:t.jsx("div",{className:"flex flex-wrap items-center gap-4",children:t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]",children:t.jsx(Be,{sources:V,types:A,regions:ee,countries:xe,imageTypes:te,isLoadingFilters:je})})})}),t.jsxs("div",{className:"space-y-4",children:[t.jsx("div",{className:"flex justify-between items-center",children:t.jsxs("p",{className:"text-sm text-gray-600",children:[C.length," of ",Q," examples"]})}),W&&t.jsx("div",{className:"text-center py-12",children:t.jsxs("div",{className:"flex flex-col items-center gap-4",children:[t.jsx(me,{className:"text-ifrcRed"}),t.jsx("div",{children:"Loading examples..."})]})}),!W&&t.jsxs("div",{className:"space-y-4",children:[C.map(e=>t.jsxs("div",{className:"flex items-center gap-4",children:[t.jsxs("div",{className:`${m.mapItem} flex-1`,onClick:()=>{console.log("ExplorePage: Clicking on image with ID:",e.image_id),console.log("ExplorePage: Image data:",e),e.image_id&&e.image_id!=="undefined"&&e.image_id!=="null"?(console.log("ExplorePage: Navigating to:",`/map/${e.image_id}`),console.log("ExplorePage: Full navigation URL:",`/#/map/${e.image_id}`),N(`/map/${e.image_id}`)):(console.error("Invalid image_id for navigation:",e.image_id),console.error("Full item data:",JSON.stringify(e,null,2)),alert(`Cannot navigate: Invalid image ID (${e.image_id})`))},children:[t.jsx("div",{className:m.mapItemImage,style:{width:"120px",height:"80px"},children:e.thumbnail_url?t.jsxs(t.Fragment,{children:[console.log("ExplorePage: Using thumbnail for fast loading:",e.thumbnail_url),t.jsx("img",{src:e.thumbnail_url,alt:e.file_key,onError:s=>{console.error("ExplorePage: Thumbnail failed to load, falling back to original:",e.thumbnail_url);const c=s.target;e.image_url?c.src=e.image_url:(c.style.display="none",c.parentElement.innerHTML="Img")},onLoad:()=>console.log("ExplorePage: Thumbnail loaded successfully:",e.thumbnail_url)})]}):e.image_url?t.jsxs(t.Fragment,{children:[console.log("ExplorePage: No thumbnail available, using original image:",e.image_url),t.jsx("img",{src:e.image_url,alt:e.file_key,onError:s=>{console.error("ExplorePage: Original image failed to load:",e.image_url);const c=s.target;c.style.display="none",c.parentElement.innerHTML="Img"},onLoad:()=>console.log("ExplorePage: Original image loaded successfully:",e.image_url)})]}):t.jsxs(t.Fragment,{children:[console.log("ExplorePage: No image_url or thumbnail provided for item:",e),"'Img'"]})}),t.jsxs("div",{className:m.mapItemContent,children:[t.jsx("h3",{className:m.mapItemTitle,children:t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{children:e.title||"Untitled"}),e.starred&&t.jsx("span",{className:"text-red-500 text-lg",title:"Starred image",children:"β
"})]})}),t.jsx("div",{className:m.mapItemMetadata,children:t.jsxs("div",{className:m.metadataTags,children:[e.image_type!=="drone_image"&&t.jsx("span",{className:m.metadataTagSource,children:e.source&&e.source.includes(", ")?e.source.split(", ").map(s=>V.find(c=>c.s_code===s.trim())?.label||s.trim()).join(", "):V.find(s=>s.s_code===e.source)?.label||e.source}),t.jsx("span",{className:m.metadataTagType,children:e.event_type&&e.event_type.includes(", ")?e.event_type.split(", ").map(s=>A.find(c=>c.t_code===s.trim())?.label||s.trim()).join(", "):A.find(s=>s.t_code===e.event_type)?.label||e.event_type}),t.jsx("span",{className:m.metadataTag,children:te.find(s=>s.image_type===e.image_type)?.label||e.image_type}),e.image_count&&e.image_count>1&&t.jsxs("span",{className:m.metadataTag,title:`Multi-upload with ${e.image_count} images`,children:["π· ",e.image_count]}),(!e.image_count||e.image_count<=1)&&t.jsx("span",{className:m.metadataTag,title:"Single Upload",children:"Single"}),e.countries&&e.countries.length>0&&t.jsxs(t.Fragment,{children:[t.jsx("span",{className:m.metadataTag,children:ee.find(s=>s.r_code===e.countries[0].r_code)?.label||"Unknown Region"}),t.jsx("span",{className:m.metadataTag,children:e.countries.map(s=>s.label).join(", ")})]})]})})]})]}),$&&t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:`delete-${e.image_id}`,variant:"tertiary",size:1,className:"bg-red-50 hover:bg-red-100 text-red-700 border border-red-200 hover:border-red-300",onClick:()=>Ie(e.image_id),title:"Delete","aria-label":"Delete saved image",children:t.jsx(ze,{className:"w-4 h-4"})})})]},e.image_id)),!C.length&&t.jsx("div",{className:"text-center py-12",children:t.jsx("p",{className:"text-gray-500",children:"No examples found."})}),!W&&C.length>0&&t.jsx(Ge,{currentPage:L,totalPages:Se,totalItems:Q,itemsPerPage:M,onPageChange:le})]})]})]}):t.jsx("div",{className:"space-y-6",children:t.jsxs("div",{className:"text-center py-12",children:[t.jsx("p",{className:"text-gray-500",children:"Map Details view coming soon..."}),t.jsx("p",{className:"text-sm text-gray-400 mt-2",children:"This will show detailed information about individual maps"})]})})]}),we&&t.jsx("div",{className:m.fullSizeModalOverlay,onClick:()=>U(!1),children:t.jsx("div",{className:m.fullSizeModalContent,onClick:e=>e.stopPropagation(),children:t.jsxs("div",{className:m.ratingWarningContent,children:[t.jsx("h3",{className:m.ratingWarningTitle,children:"Delete Image?"}),t.jsx("p",{className:m.ratingWarningText,children:"This action cannot be undone. Are you sure you want to delete this saved image and all related data?"}),t.jsxs("div",{className:m.ratingWarningButtons,children:[t.jsx(w,{name:"confirm-delete",variant:"secondary",onClick:Ce,disabled:K,children:K?"Deleting...":"Delete"}),t.jsx(w,{name:"cancel-delete",variant:"tertiary",onClick:()=>U(!1),disabled:K,children:"Cancel"})]})]})})}),t.jsx(He,{isOpen:Ne,onClose:()=>{G(!1),q(!1),Z(!1)},onExport:(e,s)=>{const c=C.filter(j=>s.includes(j.image_type));Ee(c,e)},filteredCount:C.length,totalCount:Q,hasFilters:!!(S||p||_||y||E||I||k||T),crisisMapsCount:C.filter(e=>e.image_type==="crisis_map").length,droneImagesCount:C.filter(e=>e.image_type==="drone_image").length,isLoading:ve,exportSuccess:be})]})}export{pt as default};
         | 
|  | |
| 1 | 
            +
            const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/jszip.min-zGid42TK.js","assets/index-w0OOMPwN.js","assets/index-cCOeofBN.css"])))=>i.map(i=>d[i]);
         | 
| 2 | 
            +
            import{j as t,z as b,n as w,v as Fe,w as De,x as Oe,B as Le,r,D as Me,N as Re,_ as me,L as Je,F as ze,G as We}from"./index-w0OOMPwN.js";import{u as Ue}from"./useAdmin-avReXN15.js";import{F as Be,E as He}from"./ExportModal-C5df6JIW.js";const Ve="_paginatorContainer_1l5ti_1",Ae="_paginationControls_1l5ti_19",ge={paginatorContainer:Ve,paginationControls:Ae};function Ge({currentPage:N,totalPages:u,onPageChange:$,className:z=""}){if(u<=1)return null;const f=(()=>{const h=[];if(u<=5)for(let p=1;p<=u;p++)h.push(p);else{let p=Math.max(1,N-2);const _=Math.min(u,p+5-1);_===u&&(p=Math.max(1,_-5+1));for(let y=p;y<=_;y++)h.push(y)}return h})();return t.jsx("div",{className:`${ge.paginatorContainer} ${z}`,children:t.jsxs("div",{className:ge.paginationControls,children:[t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsxs(w,{name:"prev-page",variant:"tertiary",size:1,onClick:()=>$(Math.max(1,N-1)),disabled:N===1,title:"Previous page",children:[t.jsx(Fe,{className:"w-4 h-4"}),t.jsx("span",{className:"hidden sm:inline",children:"Previous"})]})}),t.jsxs("div",{className:"flex items-center gap-1",children:[f[0]>1&&t.jsxs(t.Fragment,{children:[t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:"page-1",variant:"tertiary",size:1,onClick:()=>$(1),children:"1"})}),f[0]>2&&t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx("span",{className:"px-2 text-gray-500",children:"..."})})]}),f.map(h=>t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:`page-${h}`,variant:N===h?"primary":"tertiary",size:1,onClick:()=>$(h),children:h})},h)),f[f.length-1]<u&&t.jsxs(t.Fragment,{children:[f[f.length-1]<u-1&&t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx("span",{className:"px-2 text-gray-500",children:"..."})}),t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:`page-${u}`,variant:"tertiary",size:1,onClick:()=>$(u),children:u})})]})]}),t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsxs(w,{name:"next-page",variant:"tertiary",size:1,onClick:()=>$(Math.min(u,N+1)),disabled:N===u,title:"Next page",children:[t.jsx("span",{className:"hidden sm:inline",children:"Next"}),t.jsx(De,{className:"w-4 h-4"})]})})]})})}const Ze="_tabSelector_o9y1f_1",qe="_metadataTags_o9y1f_8",Ke="_metadataTag_o9y1f_8",Qe="_metadataTagSource_o9y1f_32",Xe="_metadataTagType_o9y1f_43",Ye="_mapItem_o9y1f_54",et="_mapItemImage_o9y1f_72",tt="_mapItemContent_o9y1f_92",at="_mapItemTitle_o9y1f_97",st="_mapItemMetadata_o9y1f_105",it="_fullSizeModalOverlay_o9y1f_134",nt="_fullSizeModalContent_o9y1f_148",lt="_ratingWarningContent_o9y1f_159",rt="_ratingWarningTitle_o9y1f_165",ot="_ratingWarningText_o9y1f_172",ct="_ratingWarningButtons_o9y1f_179",m={tabSelector:Ze,metadataTags:qe,metadataTag:Ke,metadataTagSource:Qe,metadataTagType:Xe,mapItem:Ye,mapItemImage:et,mapItemContent:tt,mapItemTitle:at,mapItemMetadata:st,fullSizeModalOverlay:it,fullSizeModalContent:nt,ratingWarningContent:lt,ratingWarningTitle:rt,ratingWarningText:ot,ratingWarningButtons:ct};function pt(){const N=Oe(),u=Le(),{isAuthenticated:$}=Ue(),[z,Y]=r.useState("explore"),[f,h]=r.useState([]),{search:S,srcFilter:p,catFilter:_,regionFilter:y,countryFilter:E,imageTypeFilter:I,uploadTypeFilter:k,showReferenceExamples:T,setShowReferenceExamples:pe}=Me(),[V,ue]=r.useState([]),[A,fe]=r.useState([]),[ee,he]=r.useState([]),[xe,_e]=r.useState([]),[te,ye]=r.useState([]),[je,ae]=r.useState(!0),[W,se]=r.useState(!0),[Ne,G]=r.useState(!1),[ve,Z]=r.useState(!1),[be,q]=r.useState(!1),[we,U]=r.useState(!1),[B,ie]=r.useState(""),[K,ne]=r.useState(!1),[L,le]=r.useState(1),[M]=r.useState(10),[Q,re]=r.useState(0),[Se,oe]=r.useState(0),Te=[{key:"explore",label:"List"},{key:"mapDetails",label:"Carousel"}],H=r.useCallback(()=>{se(!0);const e=new URLSearchParams({page:L.toString(),limit:M.toString()});S&&e.append("search",S),p&&e.append("source",p),_&&e.append("event_type",_),y&&e.append("region",y),E&&e.append("country",E),I&&e.append("image_type",I),k&&e.append("upload_type",k),T&&e.append("starred_only","true"),fetch(`/api/images/grouped?${e.toString()}`).then(s=>s.ok?s.json():(console.error("ExplorePage: Grouped endpoint failed, trying legacy endpoint"),fetch("/api/captions/legacy").then(c=>c.ok?c.json():(console.error("ExplorePage: Legacy endpoint failed, trying regular images endpoint"),fetch("/api/images").then(j=>{if(!j.ok)throw new Error(`HTTP ${j.status}: ${j.statusText}`);return j.json()}))))).then(s=>{console.log("ExplorePage: Fetched captions:",s),h(s)}).catch(s=>{console.error("ExplorePage: Error fetching captions:",s),h([])}).finally(()=>{se(!1)})},[L,S,p,_,y,E,I,k,T,M]),ce=r.useCallback(()=>{const e=new URLSearchParams;S&&e.append("search",S),p&&e.append("source",p),_&&e.append("event_type",_),y&&e.append("region",y),E&&e.append("country",E),I&&e.append("image_type",I),k&&e.append("upload_type",k),T&&e.append("starred_only","true"),fetch(`/api/images/grouped/count?${e.toString()}`).then(s=>s.ok?s.json():(console.error("ExplorePage: Count endpoint failed"),{total_count:0})).then(s=>{console.log("ExplorePage: Total count:",s.total_count),re(s.total_count),oe(Math.ceil(s.total_count/M))}).catch(s=>{console.error("ExplorePage: Error fetching total count:",s),re(0),oe(0)})},[S,p,_,y,E,I,k,T,M]);r.useEffect(()=>{H(),ce()},[H,ce]),r.useEffect(()=>{L!==1&&le(1)},[L]),r.useEffect(()=>{const e=()=>{document.hidden||H()};return document.addEventListener("visibilitychange",e),()=>{document.removeEventListener("visibilitychange",e)}},[H]),r.useEffect(()=>{new URLSearchParams(u.search).get("export")==="true"&&(G(!0),N("/explore",{replace:!0}))},[u.search,N,S,p,_,y,E,I,T]),r.useEffect(()=>{ae(!0),Promise.all([fetch("/api/sources").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/types").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/regions").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/countries").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()}),fetch("/api/image-types").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.json()})]).then(([e,s,c,j,P])=>{ue(e),fe(s),he(c),_e(j),ye(P)}).catch(()=>{}).finally(()=>{ae(!1)})},[]);const C=f,Ee=async(e,s="fine-tuning")=>{if(e.length===0){alert("No images to export");return}Z(!0),q(!1);try{const c=(await We(async()=>{const{default:i}=await import("./jszip.min-zGid42TK.js").then(F=>F.j);return{default:i}},__vite__mapDeps([0,1,2]))).default,j=new c,P=e.filter(i=>i.image_type==="crisis_map"),R=e.filter(i=>i.image_type==="drone_image");if(P.length>0){const i=j.folder("crisis_maps_dataset"),F=i?.folder("images");if(F){let D=1;for(const a of P)try{const v=a.image_count&&a.image_count>1?a.all_image_ids||[a.image_id]:[a.image_id],X=v.map(async(n,x)=>{try{const l=await fetch(`/api/images/${n}/file`);if(!l.ok)throw new Error(`Failed to fetch image ${n}`);const o=await l.blob(),d=a.file_key.split(".").pop()||"jpg",g=`${String(D).padStart(4,"0")}_${String(x+1).padStart(2,"0")}.${d}`;return F.file(g,o),{success:!0,fileName:g,imageId:n}}catch(l){return console.error(`Failed to process image ${n}:`,l),{success:!1,fileName:"",imageId:n}}}),O=(await Promise.all(X)).filter(n=>n.success);if(O.length>0){if(s==="fine-tuning"){const n=O.map(o=>`images/${o.fileName}`),x=Math.random(),l={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};if(!i)continue;if(x<.8){const o=i.file("train.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("train.jsonl",JSON.stringify(d,null,2))}else i.file("train.jsonl",JSON.stringify([l],null,2))}else if(x<.9){const o=i.file("test.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("test.jsonl",JSON.stringify(d,null,2))}else i.file("test.jsonl",JSON.stringify([l],null,2))}else{const o=i.file("val.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("val.jsonl",JSON.stringify(d,null,2))}else i.file("val.jsonl",JSON.stringify([l],null,2))}}else{const n=O.map(l=>`images/${l.fileName}`),x={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};i&&i.file(`${String(D).padStart(4,"0")}.json`,JSON.stringify(x,null,2))}D++}}catch(v){console.error(`Failed to process caption ${a.image_id}:`,v)}}}if(R.length>0){const i=j.folder("drone_images_dataset"),F=i?.folder("images");if(F){let D=1;for(const a of R)try{const v=a.image_count&&a.image_count>1?a.all_image_ids||[a.image_id]:[a.image_id],X=v.map(async(n,x)=>{try{const l=await fetch(`/api/images/${n}/file`);if(!l.ok)throw new Error(`Failed to fetch image ${n}`);const o=await l.blob(),d=a.file_key.split(".").pop()||"jpg",g=`${String(D).padStart(4,"0")}_${String(x+1).padStart(2,"0")}.${d}`;return F.file(g,o),{success:!0,fileName:g,imageId:n}}catch(l){return console.error(`Failed to process image ${n}:`,l),{success:!1,fileName:"",imageId:n}}}),O=(await Promise.all(X)).filter(n=>n.success);if(O.length>0){if(s==="fine-tuning"){const n=O.map(o=>`images/${o.fileName}`),x=Math.random(),l={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};if(!i)continue;if(x<.8){const o=i.file("train.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("train.jsonl",JSON.stringify(d,null,2))}else i.file("train.jsonl",JSON.stringify([l],null,2))}else if(x<.9){const o=i.file("test.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("test.jsonl",JSON.stringify(d,null,2))}else i.file("test.jsonl",JSON.stringify([l],null,2))}else{const o=i.file("val.jsonl");if(o){const d=await o.async("string").then(g=>JSON.parse(g||"[]")).catch(()=>[]);d.push(l),i.file("val.jsonl",JSON.stringify(d,null,2))}else i.file("val.jsonl",JSON.stringify([l],null,2))}}else{const n=O.map(l=>`images/${l.fileName}`),x={image:n.length===1?n[0]:n,caption:a.edited||a.generated||"",metadata:{image_id:v,title:a.title,source:a.source,event_type:a.event_type,image_type:a.image_type,countries:a.countries,starred:a.starred,image_count:a.image_count||1}};i&&i.file(`${String(D).padStart(4,"0")}.json`,JSON.stringify(x,null,2))}D++}}catch(v){console.error(`Failed to process caption ${a.image_id}:`,v)}}}const $e=await j.generateAsync({type:"blob"}),de=URL.createObjectURL($e),J=document.createElement("a");J.href=de,J.download=`datasets_${s}_${new Date().toISOString().split("T")[0]}.zip`,document.body.appendChild(J),J.click(),document.body.removeChild(J),URL.revokeObjectURL(de);const ke=(P.length||0)+(R.length||0);console.log(`Exported ${s} datasets with ${ke} total images:`),P.length>0&&console.log(`- Crisis maps: ${P.length} images`),R.length>0&&console.log(`- Drone images: ${R.length} images`),q(!0)}catch(c){console.error("Export failed:",c),alert("Failed to export dataset. Please try again.")}finally{Z(!1)}},Ie=e=>{ie(e),U(!0)},Ce=async()=>{if(B){ne(!0);try{console.log("Deleting image with ID:",B),(await fetch(`/api/images/${B}`,{method:"DELETE"})).ok?(h(s=>s.filter(c=>c.image_id!==B)),U(!1),ie("")):(console.error("Delete failed"),alert("Failed to delete image. Please try again."))}catch(e){console.error("Delete failed:",e),alert("Failed to delete image. Please try again.")}finally{ne(!1)}}};return t.jsxs(Re,{children:[W?t.jsx("div",{className:"flex flex-col items-center justify-center min-h-[60vh]",children:t.jsxs("div",{className:"flex flex-col items-center gap-4",children:[t.jsx(me,{className:"text-ifrcRed"}),t.jsx("div",{children:"Loading examples..."})]})}):t.jsxs("div",{className:"max-w-7xl mx-auto",children:[t.jsxs("div",{className:m.tabSelector,children:[t.jsx(Je,{name:"explore-view",value:z,onChange:e=>{(e==="explore"||e==="mapDetails")&&(Y(e),e==="mapDetails"&&f.length>0&&(f[0]?.image_id&&f[0].image_id!=="undefined"&&f[0].image_id!=="null"?N(`/map/${f[0].image_id}`):console.error("Invalid image_id for navigation:",f[0]?.image_id)))},options:Te,keySelector:e=>e.key,labelSelector:e=>e.label}),t.jsxs("div",{className:"flex items-center gap-2 ml-auto",children:[t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsxs(w,{name:"reference-examples",variant:T?"primary":"secondary",onClick:()=>pe(!T),className:"whitespace-nowrap",children:[t.jsx("span",{className:"mr-2",children:T?t.jsx("span",{className:"text-yellow-400",children:"β
"}):t.jsx("span",{className:"text-yellow-400",children:"β"})}),"Reference Examples"]})}),t.jsx(w,{name:"export-dataset",variant:"secondary",onClick:()=>G(!0),children:"Export"})]})]}),z==="explore"?t.jsxs("div",{className:"space-y-6",children:[t.jsx("div",{className:"mb-6 space-y-4",children:t.jsx("div",{className:"flex flex-wrap items-center gap-4",children:t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]",children:t.jsx(Be,{sources:V,types:A,regions:ee,countries:xe,imageTypes:te,isLoadingFilters:je})})})}),t.jsxs("div",{className:"space-y-4",children:[t.jsx("div",{className:"flex justify-between items-center",children:t.jsxs("p",{className:"text-sm text-gray-600",children:[C.length," of ",Q," examples"]})}),W&&t.jsx("div",{className:"text-center py-12",children:t.jsxs("div",{className:"flex flex-col items-center gap-4",children:[t.jsx(me,{className:"text-ifrcRed"}),t.jsx("div",{children:"Loading examples..."})]})}),!W&&t.jsxs("div",{className:"space-y-4",children:[C.map(e=>t.jsxs("div",{className:"flex items-center gap-4",children:[t.jsxs("div",{className:`${m.mapItem} flex-1`,onClick:()=>{console.log("ExplorePage: Clicking on image with ID:",e.image_id),console.log("ExplorePage: Image data:",e),e.image_id&&e.image_id!=="undefined"&&e.image_id!=="null"?(console.log("ExplorePage: Navigating to:",`/map/${e.image_id}`),console.log("ExplorePage: Full navigation URL:",`/#/map/${e.image_id}`),N(`/map/${e.image_id}`)):(console.error("Invalid image_id for navigation:",e.image_id),console.error("Full item data:",JSON.stringify(e,null,2)),alert(`Cannot navigate: Invalid image ID (${e.image_id})`))},children:[t.jsx("div",{className:m.mapItemImage,style:{width:"120px",height:"80px"},children:e.thumbnail_url?t.jsxs(t.Fragment,{children:[console.log("ExplorePage: Using thumbnail for fast loading:",e.thumbnail_url),t.jsx("img",{src:e.thumbnail_url,alt:e.file_key,onError:s=>{console.error("ExplorePage: Thumbnail failed to load, falling back to original:",e.thumbnail_url);const c=s.target;e.image_url?c.src=e.image_url:(c.style.display="none",c.parentElement.innerHTML="Img")},onLoad:()=>console.log("ExplorePage: Thumbnail loaded successfully:",e.thumbnail_url)})]}):e.image_url?t.jsxs(t.Fragment,{children:[console.log("ExplorePage: No thumbnail available, using original image:",e.image_url),t.jsx("img",{src:e.image_url,alt:e.file_key,onError:s=>{console.error("ExplorePage: Original image failed to load:",e.image_url);const c=s.target;c.style.display="none",c.parentElement.innerHTML="Img"},onLoad:()=>console.log("ExplorePage: Original image loaded successfully:",e.image_url)})]}):t.jsxs(t.Fragment,{children:[console.log("ExplorePage: No image_url or thumbnail provided for item:",e),"'Img'"]})}),t.jsxs("div",{className:m.mapItemContent,children:[t.jsx("h3",{className:m.mapItemTitle,children:t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{children:e.title||"Untitled"}),e.starred&&t.jsx("span",{className:"text-red-500 text-lg",title:"Starred image",children:"β
"})]})}),t.jsx("div",{className:m.mapItemMetadata,children:t.jsxs("div",{className:m.metadataTags,children:[e.image_type!=="drone_image"&&t.jsx("span",{className:m.metadataTagSource,children:e.source&&e.source.includes(", ")?e.source.split(", ").map(s=>V.find(c=>c.s_code===s.trim())?.label||s.trim()).join(", "):V.find(s=>s.s_code===e.source)?.label||e.source}),t.jsx("span",{className:m.metadataTagType,children:e.event_type&&e.event_type.includes(", ")?e.event_type.split(", ").map(s=>A.find(c=>c.t_code===s.trim())?.label||s.trim()).join(", "):A.find(s=>s.t_code===e.event_type)?.label||e.event_type}),t.jsx("span",{className:m.metadataTag,children:te.find(s=>s.image_type===e.image_type)?.label||e.image_type}),e.image_count&&e.image_count>1&&t.jsxs("span",{className:m.metadataTag,title:`Multi-upload with ${e.image_count} images`,children:["π· ",e.image_count]}),(!e.image_count||e.image_count<=1)&&t.jsx("span",{className:m.metadataTag,title:"Single Upload",children:"Single"}),e.countries&&e.countries.length>0&&t.jsxs(t.Fragment,{children:[t.jsx("span",{className:m.metadataTag,children:ee.find(s=>s.r_code===e.countries[0].r_code)?.label||"Unknown Region"}),t.jsx("span",{className:m.metadataTag,children:e.countries.map(s=>s.label).join(", ")})]})]})})]})]}),$&&t.jsx(b,{withInternalPadding:!0,className:"bg-white/20 backdrop-blur-sm rounded-md p-2",children:t.jsx(w,{name:`delete-${e.image_id}`,variant:"tertiary",size:1,className:"bg-red-50 hover:bg-red-100 text-red-700 border border-red-200 hover:border-red-300",onClick:()=>Ie(e.image_id),title:"Delete","aria-label":"Delete saved image",children:t.jsx(ze,{className:"w-4 h-4"})})})]},e.image_id)),!C.length&&t.jsx("div",{className:"text-center py-12",children:t.jsx("p",{className:"text-gray-500",children:"No examples found."})}),!W&&C.length>0&&t.jsx(Ge,{currentPage:L,totalPages:Se,totalItems:Q,itemsPerPage:M,onPageChange:le})]})]})]}):t.jsx("div",{className:"space-y-6",children:t.jsxs("div",{className:"text-center py-12",children:[t.jsx("p",{className:"text-gray-500",children:"Map Details view coming soon..."}),t.jsx("p",{className:"text-sm text-gray-400 mt-2",children:"This will show detailed information about individual maps"})]})})]}),we&&t.jsx("div",{className:m.fullSizeModalOverlay,onClick:()=>U(!1),children:t.jsx("div",{className:m.fullSizeModalContent,onClick:e=>e.stopPropagation(),children:t.jsxs("div",{className:m.ratingWarningContent,children:[t.jsx("h3",{className:m.ratingWarningTitle,children:"Delete Image?"}),t.jsx("p",{className:m.ratingWarningText,children:"This action cannot be undone. Are you sure you want to delete this saved image and all related data?"}),t.jsxs("div",{className:m.ratingWarningButtons,children:[t.jsx(w,{name:"confirm-delete",variant:"secondary",onClick:Ce,disabled:K,children:K?"Deleting...":"Delete"}),t.jsx(w,{name:"cancel-delete",variant:"tertiary",onClick:()=>U(!1),disabled:K,children:"Cancel"})]})]})})}),t.jsx(He,{isOpen:Ne,onClose:()=>{G(!1),q(!1),Z(!1)},onExport:(e,s)=>{const c=C.filter(j=>s.includes(j.image_type));Ee(c,e)},filteredCount:C.length,totalCount:Q,hasFilters:!!(S||p||_||y||E||I||k||T),crisisMapsCount:C.filter(e=>e.image_type==="crisis_map").length,droneImagesCount:C.filter(e=>e.image_type==="drone_image").length,isLoading:ve,exportSuccess:be})]})}export{pt as default};
         | 
    	
        py_backend/static/assets/{index-B0L3qvtx.js β index-w0OOMPwN.js}
    RENAMED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index- | 
| 2 | 
             
            function zg(e,n){for(var i=0;i<n.length;i++){const o=n[i];if(typeof o!="string"&&!Array.isArray(o)){for(const s in o)if(s!=="default"&&!(s in e)){const u=Object.getOwnPropertyDescriptor(o,s);u&&Object.defineProperty(e,s,u.get?u:{enumerable:!0,get:()=>o[s]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))o(s);new MutationObserver(s=>{for(const u of s)if(u.type==="childList")for(const d of u.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function i(s){const u={};return s.integrity&&(u.integrity=s.integrity),s.referrerPolicy&&(u.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?u.credentials="include":s.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function o(s){if(s.ep)return;s.ep=!0;const u=i(s);fetch(s.href,u)}})();var kP=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Bg(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Kc={exports:{}},Ra={},Yc={exports:{}},qe={};/**
         | 
| 3 | 
             
             * @license React
         | 
| 4 | 
             
             * react.production.min.js
         | 
| @@ -158,4 +158,4 @@ ${i||"AI-generated recommended actions will appear here..."}`,onChange:m,rows:12 | |
| 158 |  | 
| 159 | 
             
            Analysis: ${Tr}
         | 
| 160 |  | 
| 161 | 
            -
            Recommended Actions: ${jr}`,Ne={title:U,edited:Pe,accuracy:rn.accuracy,context:rn.context,usability:rn.usability};console.log("Updating caption:",Ne);const ke=await fetch(`/api/images/${lt}/caption`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ne)}),pe=await $i(ke);if(!ke.ok)throw new Error(pe.error||"Caption update failed");Dt(null),pt([]),Ai(3)}catch(k){go(k,"Submit")}finally{y(!1)}}async function Ts(){if(console.log("handleDelete called with uploadedImageId:",lt),!lt){alert("No image to delete. Please try refreshing the page.");return}z(!0)}async function Bi(){try{console.log("Deleting image with ID:",lt);const k=await fetch(`/api/images/${lt}`,{method:"DELETE"});if(!k.ok){const Pe=await $i(k);throw new Error(Pe.error||`Delete failed with status ${k.status}`)}z(!1),oi()}catch(k){go(k,"Delete")}}const oi=()=>{Pr(!1),o(1),b(null),P([]),x(null),Dt(null),pt([]),dn(null),W(""),$(""),Z(""),O(""),I([]),he(""),_e(""),ue(""),H(""),Se(""),re(""),Q(""),et(""),Wt(!1),bt(""),wt(""),Mi({accuracy:50,context:50,usability:50}),Pi(""),Nr(""),$n(""),fn(""),ht([]),we(!1),ve(null),ze(!1),Ye(null),Xe(!1),pn(null),Ve(!1),At(""),mn(!1),He(null),_t(!1),Ze(null),n("/upload",{replace:!0})},tr=v.useCallback(k=>{k==="/upload"||k==="/"||(C.current?(Me(k),le(!0)):n(k))},[n]);async function vo(){if(Ee&&C.current)try{await fetch(`/api/images/${C.current}`,{method:"DELETE"}),le(!1),Me(null),n(Ee)}catch(k){console.error("Failed to delete image before navigation:",k),le(!1),Me(null),n(Ee)}}const ku=async()=>{if(Pn){Ve(!0),At("Starting file conversion...");try{const k=new FormData;k.append("file",Pn),k.append("preprocess_only","true"),At("Converting file format...");const Pe=await fetch("/api/images/preprocess",{method:"POST",body:k});if(!Pe.ok)throw new Error("Preprocessing failed");const Ne=await Pe.json();At("Finalizing conversion...");const ke=atob(Ne.processed_content),pe=new Uint8Array(ke.length);for(let Nt=0;Nt<ke.length;Nt++)pe[Nt]=ke.charCodeAt(Nt);const Re=new File([pe],Ne.processed_filename,{type:Ne.processed_mime_type}),Ot=URL.createObjectURL(Re);N.length===0?(b(Re),P([Re])):P(Nt=>[...Nt,Re]),x(Ot),At("Conversion complete!"),setTimeout(()=>{Xe(!1),pn(null),Ve(!1),At("")},1e3)}catch(k){console.error("Preprocessing error:",k),At("Conversion failed. Please try again."),setTimeout(()=>{Xe(!1),pn(null),Ve(!1),At("")},2e3)}}},Nu=()=>{Xe(!1),pn(null),Ve(!1),At("")},ca=async k=>{f(!0);try{const Pe=k.map(async Re=>{const Ot=await fetch(`/api/images/${Re}`);if(!Ot.ok)throw new Error(`Failed to fetch image ${Re}`);const Nt=await Ot.json(),Bn=await fetch(`/api/images/${Re}/file`);if(!Bn.ok)throw new Error(`Failed to fetch image file ${Re}`);const Un=await Bn.blob(),gt=Nt.file_key.split("/").pop()||`contributed_${Re}.png`;return{file:new File([Un],gt,{type:Un.type}),imageData:Nt}}),Ne=await Promise.all(Pe),ke=Ne.map(Re=>Re.file),pe=Ne[0]?.imageData;P(ke),pt(k),k.length===1&&Dt(k[0]),ke.length>=1&&b(ke[0]),pe?.image_type&&ce(pe.image_type)}catch(Pe){console.error("Failed to fetch contributed images:",Pe),alert(`Failed to load contributed images: ${Pe instanceof Error?Pe.message:"Unknown error"}`)}finally{f(!1)}};return v.useEffect(()=>{Promise.all([fetch("/api/sources").then(k=>k.json()),fetch("/api/types").then(k=>k.json()),fetch("/api/spatial-references").then(k=>k.json()),fetch("/api/image-types").then(k=>k.json()),fetch("/api/countries").then(k=>k.json()),fetch("/api/models").then(k=>k.json())]).then(([k,Pe,Ne,ke,pe,Re])=>{!localStorage.getItem(Bl)&&Re?.length&&localStorage.setItem(Bl,Re[0].m_code),nn(k),ln(Pe),Jr(Ne),cn(ke),Kn(pe),k.length>0&&$(k[0].s_code),Z("OTHER"),O("OTHER"),ke.length>0&&!e.get("imageType")&&!K&&ce(ke[0].image_type)})},[e,K]),v.useEffect(()=>(window.confirmNavigationIfNeeded=k=>{tr(k)},()=>{delete window.confirmNavigationIfNeeded}),[tr]),v.useEffect(()=>{const k=ke=>{if(C.current){const pe="You have an uploaded image that will be deleted if you leave this page. Are you sure you want to leave?";return ke.preventDefault(),ke.returnValue=pe,pe}},Pe=()=>{C.current&&fetch(`/api/images/${C.current}`,{method:"DELETE"}).catch(console.error)},Ne=ke=>{const pe=ke.target,Re=pe.closest("a[href]")||pe.closest("[data-navigate]");if(Re&&C.current){const Ot=Re.getAttribute("href")||Re.getAttribute("data-navigate");Ot&&Ot!=="#"&&!Ot.startsWith("javascript:")&&!Ot.startsWith("mailto:")&&(ke.preventDefault(),ke.stopPropagation(),tr(Ot))}};return window.addEventListener("beforeunload",k),document.addEventListener("click",Ne,!0),()=>{window.removeEventListener("beforeunload",k),document.removeEventListener("click",Ne,!0),Pe()}},[tr]),v.useEffect(()=>{if(!E){x(null);return}const k=URL.createObjectURL(E);return x(k),()=>URL.revokeObjectURL(k)},[E]),v.useEffect(()=>{const k=e.get("contribute"),Pe=e.get("imageIds");if(k==="true"&&Pe){const Ne=Pe.split(",").filter(ke=>ke.trim());Ne.length>0&&ca(Ne)}},[e]),v.useEffect(()=>{i==="2b"&&Dr(0)},[i]),g.jsxs(Qf,{children:[i!==3&&g.jsx("div",{className:"max-w-7xl mx-auto",children:g.jsxs("div",{className:V.uploadContainer,"data-step":i,children:[i===1&&!e.get("step")&&!d&&g.jsx(Ag,{files:N,file:E,preview:m,imageType:K,onFileChange:Rr,onRemoveImage:Xn,onAddImage:Fi,onImageTypeChange:ni,onChangeFile:er}),i===1&&e.get("contribute")==="true"&&!d&&N.length>0&&g.jsx(Ag,{files:N,file:E,preview:m,imageType:K,onFileChange:Rr,onRemoveImage:Xn,onAddImage:Fi,onImageTypeChange:ni,onChangeFile:er}),s&&g.jsxs("div",{className:V.loadingContainer,children:[g.jsx(Wo,{className:"text-ifrcRed"}),g.jsx("p",{className:V.loadingText,children:"Generating..."})]}),d&&g.jsxs("div",{className:V.loadingContainer,children:[g.jsx(Wo,{className:"text-ifrcRed"}),g.jsx("p",{className:V.loadingText,children:"Loading contribution..."})]}),(i===1&&!s&&!d||i===1&&e.get("contribute")==="true"&&!s&&!d&&N.length>0)&&g.jsx("div",{className:V.generateButtonContainer,children:Kt?g.jsx(Ue,{name:"generate-from-url",onClick:ii,children:"Generate Caption"}):g.jsx(Ue,{name:"generate",disabled:N.length===0,onClick:ii,children:"Generate"})}),i==="2a"&&g.jsxs("div",{className:V.step2Layout,children:[g.jsx("div",{className:V.mapColumn,children:g.jsx(Fg,{files:N,imageUrl:Kt,preview:m,onViewFullSize:k=>{Yn(k||null),ei(!0)}})}),g.jsx("div",{className:V.contentColumn,children:g.jsxs("div",{className:V.metadataSectionCard,children:[g.jsx(Qj,{files:N,imageType:K,title:U,source:L,eventType:q,epsg:fe,countries:de,centerLon:ne,centerLat:Ce,amslM:G,aglM:T,headingDeg:oe,yawDeg:se,pitchDeg:ee,rollDeg:Y,rtkFix:tn,stdHM:ot,stdVM:st,metadataArray:ft,sources:xt,types:Tn,spatialReferences:jn,imageTypes:un,countriesOptions:Er,onTitleChange:k=>W(k||""),onSourceChange:po,onEventTypeChange:mo,onEpsgChange:Ri,onCountriesChange:Su,onCenterLonChange:bu,onCenterLatChange:oa,onAmslMChange:Cs,onAglMChange:aa,onHeadingDegChange:Ii,onYawDegChange:_u,onPitchDegChange:sa,onRollDegChange:tt,onRtkFixChange:Ss,onStdHMChange:bs,onStdVMChange:la,onImageTypeChange:ni,updateMetadataForImage:mr}),g.jsxs("div",{className:V.confirmSection,children:[g.jsx(hf,{name:"delete",variant:"tertiary",onClick:Ts,title:"Delete",ariaLabel:"Delete uploaded image",children:g.jsx(df,{})}),g.jsx(Ue,{name:"confirm-metadata",onClick:()=>Ai("2b"),children:"Next"})]})]})})]}),i==="2b"&&g.jsxs("div",{className:V.step2bLayout,children:[p&&g.jsxs("div",{className:V.loadingContainer,children:[g.jsx(Wo,{className:"text-ifrcRed"}),g.jsx("p",{className:V.loadingText,children:"Submitting..."})]}),g.jsxs("div",{className:`${V.topRow} ${hn?V.ratingHidden:""}`,children:[g.jsx("div",{className:V.imageSection,children:g.jsx(Fg,{files:N,imageUrl:Kt,preview:m,onViewFullSize:k=>{Yn(k||null),ei(!0)},currentImageIndex:pr,onGoToPrevious:_s,onGoToNext:Es,onGoToImage:ua,showCarousel:!0})}),!hn&&g.jsx("div",{className:V.metadataSectionCard,children:g.jsx(Kj,{isPerformanceConfirmed:hn,scores:rn,onScoreChange:(k,Pe)=>Mi(Ne=>({...Ne,[k]:Pe})),onConfirmRatings:()=>Pr(!0),onEditRatings:()=>Pr(!1)})})]}),g.jsx("div",{className:V.metadataSectionCard,children:g.jsx(Yj,{description:kr,analysis:Tr,recommendedActions:jr,onDescriptionChange:k=>Nr(k||""),onAnalysisChange:k=>$n(k||""),onRecommendedActionsChange:k=>fn(k||""),onBack:()=>Ai("2a"),onDelete:Ts,onSubmit:Ns,onEditRatings:()=>Pr(!1),isPerformanceConfirmed:hn,isSubmitting:p})})]})]})}),i===3&&g.jsxs("div",{className:V.successContainer,children:[g.jsx(ro,{level:2,className:V.successHeading,children:"Saved!"}),g.jsx("p",{className:V.successText,children:e.get("contribute")==="true"?"Your contribution has been successfully saved.":"Your caption has been successfully saved."}),g.jsx("div",{className:V.successButton,children:g.jsx(Ue,{name:"upload-another",onClick:()=>{oi()},children:"Upload Another"})})]}),g.jsx(Xj,{isOpen:Li,imageUrl:Kt,preview:m,selectedImageData:Di,onClose:()=>{ei(!1),Yn(null)}}),g.jsx(Jj,{isOpen:Mr,onClose:()=>j(!1)}),g.jsx(eP,{isOpen:R,onConfirm:Bi,onCancel:()=>z(!1)}),g.jsx(tP,{isOpen:X,onConfirm:vo,onCancel:()=>le(!1)}),g.jsx(nP,{isOpen:ye,fallbackInfo:ge,onClose:()=>we(!1)}),g.jsx(rP,{isOpen:Fe,preprocessingInfo:Pt,onClose:()=>ze(!1)}),g.jsx(iP,{isOpen:Ge,preprocessingFile:Pn,isPreprocessing:Mn,preprocessingProgress:Oi,onConfirm:ku,onCancel:Nu}),g.jsx(oP,{isOpen:zn,unsupportedFile:ti,onClose:()=>mn(!1)}),g.jsx(aP,{isOpen:nt,oversizedFile:Lr,onClose:()=>_t(!1),onCancel:()=>_t(!1)})]})}const sP="_helpContainer_1wavj_1",lP="_helpSection_1wavj_13",uP="_sectionHeader_1wavj_49",cP="_sectionTitle_1wavj_91",dP="_sectionContent_1wavj_105",fP="_guidelinesList_1wavj_119",hP="_buttonContainer_1wavj_181",dt={helpContainer:sP,helpSection:lP,sectionHeader:uP,sectionTitle:cP,sectionContent:dP,guidelinesList:fP,buttonContainer:hP};function pP(){const e=hs(),{setShowReferenceExamples:n}=Zj(),i=()=>{e("/upload")},o=()=>{n(!0),e("/explore")},s=()=>{e("/analytics?view=crisis_maps")};return g.jsx(Qf,{className:"py-10",children:g.jsx("div",{className:dt.helpContainer,children:g.jsxs("div",{className:"space-y-8",children:[g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Introduction"})}),g.jsx("div",{className:dt.sectionContent,children:"In collaboration with the IFRC, PromptAid Vision is a tool that generates textual descriptions of crisis maps/crisis drone images utiliing Visual language models. This prototype is for collecting data for the fine-tuning of our own models. We aim to utilize AI tools to support national societies with rapid decision making during emergencies."}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"upload-now",variant:"secondary",onClick:i,children:"Upload now β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Guidelines"})}),g.jsxs("div",{className:dt.sectionContent,children:["To make the process smoother, please follow the guidelines below:",g.jsxs("ul",{className:dt.guidelinesList,children:[g.jsx("li",{children:"Avoid uploading images that are not crisis maps/crisis drone images."}),g.jsx("li",{children:"Confirm the image details prior to modifying the description."}),g.jsx("li",{children:"Before the modification, please read the description generated and provide a rating via the rating sliders."}),g.jsx("li",{children:'Click the "Submit" button to save the description.'})]})]}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"see-examples",variant:"secondary",onClick:o,children:"See examples β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"VLMs"})}),g.jsx("div",{className:dt.sectionContent,children:"PromptAid Vision uses a variety of Visual Language Models (VLMs). A random VLM is selected for each upload. Therefore feel free to delete and reupload. You can view performance details here:"}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"view-vlm-details",variant:"secondary",onClick:s,children:"View VLM details β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Dataset"})}),g.jsx("div",{className:dt.sectionContent,children:"All users are able to export the dataset. You could apply filters when exporting, and it have the option to organize based on model fine-tuning formats."}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"export-dataset",variant:"secondary",onClick:()=>{n(!1),e("/explore"),setTimeout(()=>{const u=document.querySelector('[name="export-dataset"]');u&&u.click()},100)},children:"Export dataset β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Contact us"})}),g.jsx("div",{className:dt.sectionContent,children:"Need help or have questions about PromptAid Vision? Our team is here to support you."}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"contact-support",variant:"secondary",disabled:!0,children:"Get in touch β"})})]})]})})})}const mP=v.createContext(void 0),gP=({children:e})=>{const[n,i]=v.useState(!1),[o,s]=v.useState(!0),u=async()=>{const y=localStorage.getItem("adminToken");if(!y){i(!1),s(!1);return}try{(await fetch("/api/admin/verify",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${y}`}})).ok?i(!0):(localStorage.removeItem("adminToken"),i(!1))}catch(w){console.error("Error verifying admin token:",w),localStorage.removeItem("adminToken"),i(!1)}finally{s(!1)}},d=async y=>{try{const w=await fetch("/api/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({password:y})});if(w.ok){const C=await w.json();return localStorage.setItem("adminToken",C.access_token),i(!0),!0}else return!1}catch(w){return console.error("Login error:",w),!1}},f=()=>{localStorage.removeItem("adminToken"),i(!1)};v.useEffect(()=>{u()},[]);const p={isAuthenticated:n,isLoading:o,login:d,logout:f,verifyToken:u};return g.jsx(mP.Provider,{value:p,children:e})},vP=v.lazy(()=>Qn(()=>import("./index-C4_t-iUv.js"),__vite__mapDeps([0,1]))),yP=v.lazy(()=>Qn(()=>import("./index-CWQiqOU3.js"),__vite__mapDeps([2,3,4,5,6]))),wP=v.lazy(()=>Qn(()=>import("./AdminPage-0qyl8wHV.js"),__vite__mapDeps([7,3,8]))),xP=v.lazy(()=>Qn(()=>import("./index-D5zar1LU.js"),__vite__mapDeps([9,3,4,5,10])));class CP extends v.Component{constructor(n){super(n),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(n,i){console.error("Error caught by boundary:",n,i)}render(){return this.state.hasError?g.jsxs("div",{style:{padding:"20px",textAlign:"center"},children:[g.jsx("h2",{children:"Something went wrong"}),g.jsx("p",{children:"Please refresh the page to try again."}),g.jsx("button",{onClick:()=>window.location.reload(),children:"Refresh Page"})]}):this.props.children}}const bi=e=>{const n=e();return n.catch(()=>{}),n},SP=()=>{"requestIdleCallback"in window?requestIdleCallback(()=>{bi(()=>Qn(()=>import("./index-C4_t-iUv.js"),__vite__mapDeps([0,1]))),bi(()=>Qn(()=>import("./index-CWQiqOU3.js"),__vite__mapDeps([2,3,4,5,6]))),bi(()=>Qn(()=>import("./AdminPage-0qyl8wHV.js"),__vite__mapDeps([7,3,8]))),bi(()=>Qn(()=>import("./index-D5zar1LU.js"),__vite__mapDeps([9,3,4,5,10])))}):setTimeout(()=>{bi(()=>Qn(()=>import("./index-C4_t-iUv.js"),__vite__mapDeps([0,1]))),bi(()=>Qn(()=>import("./index-CWQiqOU3.js"),__vite__mapDeps([2,3,4,5,6]))),bi(()=>Qn(()=>import("./AdminPage-0qyl8wHV.js"),__vite__mapDeps([7,3,8]))),bi(()=>Qn(()=>import("./index-D5zar1LU.js"),__vite__mapDeps([9,3,4,5,10])))},1e3)},bP=vx([{element:g.jsx(LT,{}),children:[{path:"/",element:g.jsx($g,{})},{path:"/upload",element:g.jsx($g,{})},{path:"/analytics",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Analytics..."}),children:g.jsx(vP,{})})},{path:"/explore",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Explore..."}),children:g.jsx(yP,{})})},{path:"/help",element:g.jsx(pP,{})},{path:"/admin",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Admin..."}),children:g.jsx(wP,{})})},{path:"/map/:mapId",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Map Details..."}),children:g.jsx(xP,{})})}]}],{basename:"/"});function _P(){const[e,n]=v.useState([]);v.useEffect(()=>{SP()},[]);const i=v.useCallback(f=>{n(p=>Bx([...p,f],y=>y.name)??p)},[n]),o=v.useCallback(f=>{n(p=>{const y=p.findIndex(C=>C.name===f);if(y===-1)return p;const w=[...p];return w.splice(y,1),w})},[n]),s=v.useCallback((f,p)=>{n(y=>{const w=y.findIndex(m=>m.name===f);if(w===-1)return y;const C=[...y];return C[w]={...C[w],...p},C})},[n]),u=v.useMemo(()=>({alerts:e,addAlert:i,removeAlert:o,updateAlert:s}),[e,i,o,s]),d=v.useMemo(()=>({languageNamespaceStatus:{},setLanguageNamespaceStatus:()=>{},currentLanguage:"en",setCurrentLanguage:()=>{},strings:{},setStrings:()=>{},registerNamespace:()=>{}}),[]);return g.jsx(CP,{children:g.jsx(Fx.Provider,{value:u,children:g.jsx(o0.Provider,{value:d,children:g.jsx(gP,{children:g.jsx(Gj,{children:g.jsx(kx,{router:bP})})})})})})}function EP(){return g.jsx(_P,{})}console.log("React version at runtime:",v.version,"createContext exists?",!!v.createContext);V1.createRoot(document.getElementById("root")).render(g.jsx(v.StrictMode,{children:g.jsx(EP,{})}));export{_C as A,ra as B,TP as C,Zj as D,MP as E,df as F,Qn as G,ro as H,V as I,eo as J,NP as K,_T as L,Xj as M,Qf as N,jt as O,jC as P,DP as Q,c0 as R,TC as S,d0 as T,LP as U,Lg as V,mP as W,kP as X,Bg as Y,Wo as _,$e as a,Lt as b,IP as c,AP as d,jP as e,SC as f,l0 as g,RP as h,OP as i,g as j,fr as k,qx as l,FE as m,Ue as n,Oe as o,Zx as p,Ka as q,v as r,u0 as s,o0 as t,Dx as u,PC as v,MC as w,hs as x,PP as y,dr as z};
         | 
|  | |
| 1 | 
            +
            const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-D3L5y9N7.js","assets/index-B1uKTNcW.css","assets/index-hD4RFL5O.js","assets/useAdmin-avReXN15.js","assets/ExportModal-C5df6JIW.js","assets/ExportModal-BoB3JpqO.css","assets/index-C85X4vtS.css","assets/AdminPage-DLWomD88.js","assets/AdminPage-VgSjQ50W.css","assets/index-BTevbmbp.js","assets/index-tDgjKyWF.css"])))=>i.map(i=>d[i]);
         | 
| 2 | 
             
            function zg(e,n){for(var i=0;i<n.length;i++){const o=n[i];if(typeof o!="string"&&!Array.isArray(o)){for(const s in o)if(s!=="default"&&!(s in e)){const u=Object.getOwnPropertyDescriptor(o,s);u&&Object.defineProperty(e,s,u.get?u:{enumerable:!0,get:()=>o[s]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))o(s);new MutationObserver(s=>{for(const u of s)if(u.type==="childList")for(const d of u.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function i(s){const u={};return s.integrity&&(u.integrity=s.integrity),s.referrerPolicy&&(u.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?u.credentials="include":s.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function o(s){if(s.ep)return;s.ep=!0;const u=i(s);fetch(s.href,u)}})();var kP=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Bg(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Kc={exports:{}},Ra={},Yc={exports:{}},qe={};/**
         | 
| 3 | 
             
             * @license React
         | 
| 4 | 
             
             * react.production.min.js
         | 
|  | |
| 158 |  | 
| 159 | 
             
            Analysis: ${Tr}
         | 
| 160 |  | 
| 161 | 
            +
            Recommended Actions: ${jr}`,Ne={title:U,edited:Pe,accuracy:rn.accuracy,context:rn.context,usability:rn.usability};console.log("Updating caption:",Ne);const ke=await fetch(`/api/images/${lt}/caption`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ne)}),pe=await $i(ke);if(!ke.ok)throw new Error(pe.error||"Caption update failed");Dt(null),pt([]),Ai(3)}catch(k){go(k,"Submit")}finally{y(!1)}}async function Ts(){if(console.log("handleDelete called with uploadedImageId:",lt),!lt){alert("No image to delete. Please try refreshing the page.");return}z(!0)}async function Bi(){try{console.log("Deleting image with ID:",lt);const k=await fetch(`/api/images/${lt}`,{method:"DELETE"});if(!k.ok){const Pe=await $i(k);throw new Error(Pe.error||`Delete failed with status ${k.status}`)}z(!1),oi()}catch(k){go(k,"Delete")}}const oi=()=>{Pr(!1),o(1),b(null),P([]),x(null),Dt(null),pt([]),dn(null),W(""),$(""),Z(""),O(""),I([]),he(""),_e(""),ue(""),H(""),Se(""),re(""),Q(""),et(""),Wt(!1),bt(""),wt(""),Mi({accuracy:50,context:50,usability:50}),Pi(""),Nr(""),$n(""),fn(""),ht([]),we(!1),ve(null),ze(!1),Ye(null),Xe(!1),pn(null),Ve(!1),At(""),mn(!1),He(null),_t(!1),Ze(null),n("/upload",{replace:!0})},tr=v.useCallback(k=>{k==="/upload"||k==="/"||(C.current?(Me(k),le(!0)):n(k))},[n]);async function vo(){if(Ee&&C.current)try{await fetch(`/api/images/${C.current}`,{method:"DELETE"}),le(!1),Me(null),n(Ee)}catch(k){console.error("Failed to delete image before navigation:",k),le(!1),Me(null),n(Ee)}}const ku=async()=>{if(Pn){Ve(!0),At("Starting file conversion...");try{const k=new FormData;k.append("file",Pn),k.append("preprocess_only","true"),At("Converting file format...");const Pe=await fetch("/api/images/preprocess",{method:"POST",body:k});if(!Pe.ok)throw new Error("Preprocessing failed");const Ne=await Pe.json();At("Finalizing conversion...");const ke=atob(Ne.processed_content),pe=new Uint8Array(ke.length);for(let Nt=0;Nt<ke.length;Nt++)pe[Nt]=ke.charCodeAt(Nt);const Re=new File([pe],Ne.processed_filename,{type:Ne.processed_mime_type}),Ot=URL.createObjectURL(Re);N.length===0?(b(Re),P([Re])):P(Nt=>[...Nt,Re]),x(Ot),At("Conversion complete!"),setTimeout(()=>{Xe(!1),pn(null),Ve(!1),At("")},1e3)}catch(k){console.error("Preprocessing error:",k),At("Conversion failed. Please try again."),setTimeout(()=>{Xe(!1),pn(null),Ve(!1),At("")},2e3)}}},Nu=()=>{Xe(!1),pn(null),Ve(!1),At("")},ca=async k=>{f(!0);try{const Pe=k.map(async Re=>{const Ot=await fetch(`/api/images/${Re}`);if(!Ot.ok)throw new Error(`Failed to fetch image ${Re}`);const Nt=await Ot.json(),Bn=await fetch(`/api/images/${Re}/file`);if(!Bn.ok)throw new Error(`Failed to fetch image file ${Re}`);const Un=await Bn.blob(),gt=Nt.file_key.split("/").pop()||`contributed_${Re}.png`;return{file:new File([Un],gt,{type:Un.type}),imageData:Nt}}),Ne=await Promise.all(Pe),ke=Ne.map(Re=>Re.file),pe=Ne[0]?.imageData;P(ke),pt(k),k.length===1&&Dt(k[0]),ke.length>=1&&b(ke[0]),pe?.image_type&&ce(pe.image_type)}catch(Pe){console.error("Failed to fetch contributed images:",Pe),alert(`Failed to load contributed images: ${Pe instanceof Error?Pe.message:"Unknown error"}`)}finally{f(!1)}};return v.useEffect(()=>{Promise.all([fetch("/api/sources").then(k=>k.json()),fetch("/api/types").then(k=>k.json()),fetch("/api/spatial-references").then(k=>k.json()),fetch("/api/image-types").then(k=>k.json()),fetch("/api/countries").then(k=>k.json()),fetch("/api/models").then(k=>k.json())]).then(([k,Pe,Ne,ke,pe,Re])=>{!localStorage.getItem(Bl)&&Re?.length&&localStorage.setItem(Bl,Re[0].m_code),nn(k),ln(Pe),Jr(Ne),cn(ke),Kn(pe),k.length>0&&$(k[0].s_code),Z("OTHER"),O("OTHER"),ke.length>0&&!e.get("imageType")&&!K&&ce(ke[0].image_type)})},[e,K]),v.useEffect(()=>(window.confirmNavigationIfNeeded=k=>{tr(k)},()=>{delete window.confirmNavigationIfNeeded}),[tr]),v.useEffect(()=>{const k=ke=>{if(C.current){const pe="You have an uploaded image that will be deleted if you leave this page. Are you sure you want to leave?";return ke.preventDefault(),ke.returnValue=pe,pe}},Pe=()=>{C.current&&fetch(`/api/images/${C.current}`,{method:"DELETE"}).catch(console.error)},Ne=ke=>{const pe=ke.target,Re=pe.closest("a[href]")||pe.closest("[data-navigate]");if(Re&&C.current){const Ot=Re.getAttribute("href")||Re.getAttribute("data-navigate");Ot&&Ot!=="#"&&!Ot.startsWith("javascript:")&&!Ot.startsWith("mailto:")&&(ke.preventDefault(),ke.stopPropagation(),tr(Ot))}};return window.addEventListener("beforeunload",k),document.addEventListener("click",Ne,!0),()=>{window.removeEventListener("beforeunload",k),document.removeEventListener("click",Ne,!0),Pe()}},[tr]),v.useEffect(()=>{if(!E){x(null);return}const k=URL.createObjectURL(E);return x(k),()=>URL.revokeObjectURL(k)},[E]),v.useEffect(()=>{const k=e.get("contribute"),Pe=e.get("imageIds");if(k==="true"&&Pe){const Ne=Pe.split(",").filter(ke=>ke.trim());Ne.length>0&&ca(Ne)}},[e]),v.useEffect(()=>{i==="2b"&&Dr(0)},[i]),g.jsxs(Qf,{children:[i!==3&&g.jsx("div",{className:"max-w-7xl mx-auto",children:g.jsxs("div",{className:V.uploadContainer,"data-step":i,children:[i===1&&!e.get("step")&&!d&&g.jsx(Ag,{files:N,file:E,preview:m,imageType:K,onFileChange:Rr,onRemoveImage:Xn,onAddImage:Fi,onImageTypeChange:ni,onChangeFile:er}),i===1&&e.get("contribute")==="true"&&!d&&N.length>0&&g.jsx(Ag,{files:N,file:E,preview:m,imageType:K,onFileChange:Rr,onRemoveImage:Xn,onAddImage:Fi,onImageTypeChange:ni,onChangeFile:er}),s&&g.jsxs("div",{className:V.loadingContainer,children:[g.jsx(Wo,{className:"text-ifrcRed"}),g.jsx("p",{className:V.loadingText,children:"Generating..."})]}),d&&g.jsxs("div",{className:V.loadingContainer,children:[g.jsx(Wo,{className:"text-ifrcRed"}),g.jsx("p",{className:V.loadingText,children:"Loading contribution..."})]}),(i===1&&!s&&!d||i===1&&e.get("contribute")==="true"&&!s&&!d&&N.length>0)&&g.jsx("div",{className:V.generateButtonContainer,children:Kt?g.jsx(Ue,{name:"generate-from-url",onClick:ii,children:"Generate Caption"}):g.jsx(Ue,{name:"generate",disabled:N.length===0,onClick:ii,children:"Generate"})}),i==="2a"&&g.jsxs("div",{className:V.step2Layout,children:[g.jsx("div",{className:V.mapColumn,children:g.jsx(Fg,{files:N,imageUrl:Kt,preview:m,onViewFullSize:k=>{Yn(k||null),ei(!0)}})}),g.jsx("div",{className:V.contentColumn,children:g.jsxs("div",{className:V.metadataSectionCard,children:[g.jsx(Qj,{files:N,imageType:K,title:U,source:L,eventType:q,epsg:fe,countries:de,centerLon:ne,centerLat:Ce,amslM:G,aglM:T,headingDeg:oe,yawDeg:se,pitchDeg:ee,rollDeg:Y,rtkFix:tn,stdHM:ot,stdVM:st,metadataArray:ft,sources:xt,types:Tn,spatialReferences:jn,imageTypes:un,countriesOptions:Er,onTitleChange:k=>W(k||""),onSourceChange:po,onEventTypeChange:mo,onEpsgChange:Ri,onCountriesChange:Su,onCenterLonChange:bu,onCenterLatChange:oa,onAmslMChange:Cs,onAglMChange:aa,onHeadingDegChange:Ii,onYawDegChange:_u,onPitchDegChange:sa,onRollDegChange:tt,onRtkFixChange:Ss,onStdHMChange:bs,onStdVMChange:la,onImageTypeChange:ni,updateMetadataForImage:mr}),g.jsxs("div",{className:V.confirmSection,children:[g.jsx(hf,{name:"delete",variant:"tertiary",onClick:Ts,title:"Delete",ariaLabel:"Delete uploaded image",children:g.jsx(df,{})}),g.jsx(Ue,{name:"confirm-metadata",onClick:()=>Ai("2b"),children:"Next"})]})]})})]}),i==="2b"&&g.jsxs("div",{className:V.step2bLayout,children:[p&&g.jsxs("div",{className:V.loadingContainer,children:[g.jsx(Wo,{className:"text-ifrcRed"}),g.jsx("p",{className:V.loadingText,children:"Submitting..."})]}),g.jsxs("div",{className:`${V.topRow} ${hn?V.ratingHidden:""}`,children:[g.jsx("div",{className:V.imageSection,children:g.jsx(Fg,{files:N,imageUrl:Kt,preview:m,onViewFullSize:k=>{Yn(k||null),ei(!0)},currentImageIndex:pr,onGoToPrevious:_s,onGoToNext:Es,onGoToImage:ua,showCarousel:!0})}),!hn&&g.jsx("div",{className:V.metadataSectionCard,children:g.jsx(Kj,{isPerformanceConfirmed:hn,scores:rn,onScoreChange:(k,Pe)=>Mi(Ne=>({...Ne,[k]:Pe})),onConfirmRatings:()=>Pr(!0),onEditRatings:()=>Pr(!1)})})]}),g.jsx("div",{className:V.metadataSectionCard,children:g.jsx(Yj,{description:kr,analysis:Tr,recommendedActions:jr,onDescriptionChange:k=>Nr(k||""),onAnalysisChange:k=>$n(k||""),onRecommendedActionsChange:k=>fn(k||""),onBack:()=>Ai("2a"),onDelete:Ts,onSubmit:Ns,onEditRatings:()=>Pr(!1),isPerformanceConfirmed:hn,isSubmitting:p})})]})]})}),i===3&&g.jsxs("div",{className:V.successContainer,children:[g.jsx(ro,{level:2,className:V.successHeading,children:"Saved!"}),g.jsx("p",{className:V.successText,children:e.get("contribute")==="true"?"Your contribution has been successfully saved.":"Your caption has been successfully saved."}),g.jsx("div",{className:V.successButton,children:g.jsx(Ue,{name:"upload-another",onClick:()=>{oi()},children:"Upload Another"})})]}),g.jsx(Xj,{isOpen:Li,imageUrl:Kt,preview:m,selectedImageData:Di,onClose:()=>{ei(!1),Yn(null)}}),g.jsx(Jj,{isOpen:Mr,onClose:()=>j(!1)}),g.jsx(eP,{isOpen:R,onConfirm:Bi,onCancel:()=>z(!1)}),g.jsx(tP,{isOpen:X,onConfirm:vo,onCancel:()=>le(!1)}),g.jsx(nP,{isOpen:ye,fallbackInfo:ge,onClose:()=>we(!1)}),g.jsx(rP,{isOpen:Fe,preprocessingInfo:Pt,onClose:()=>ze(!1)}),g.jsx(iP,{isOpen:Ge,preprocessingFile:Pn,isPreprocessing:Mn,preprocessingProgress:Oi,onConfirm:ku,onCancel:Nu}),g.jsx(oP,{isOpen:zn,unsupportedFile:ti,onClose:()=>mn(!1)}),g.jsx(aP,{isOpen:nt,oversizedFile:Lr,onClose:()=>_t(!1),onCancel:()=>_t(!1)})]})}const sP="_helpContainer_1wavj_1",lP="_helpSection_1wavj_13",uP="_sectionHeader_1wavj_49",cP="_sectionTitle_1wavj_91",dP="_sectionContent_1wavj_105",fP="_guidelinesList_1wavj_119",hP="_buttonContainer_1wavj_181",dt={helpContainer:sP,helpSection:lP,sectionHeader:uP,sectionTitle:cP,sectionContent:dP,guidelinesList:fP,buttonContainer:hP};function pP(){const e=hs(),{setShowReferenceExamples:n}=Zj(),i=()=>{e("/upload")},o=()=>{n(!0),e("/explore")},s=()=>{e("/analytics?view=crisis_maps")};return g.jsx(Qf,{className:"py-10",children:g.jsx("div",{className:dt.helpContainer,children:g.jsxs("div",{className:"space-y-8",children:[g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Introduction"})}),g.jsx("div",{className:dt.sectionContent,children:"In collaboration with the IFRC, PromptAid Vision is a tool that generates textual descriptions of crisis maps/crisis drone images utiliing Visual language models. This prototype is for collecting data for the fine-tuning of our own models. We aim to utilize AI tools to support national societies with rapid decision making during emergencies."}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"upload-now",variant:"secondary",onClick:i,children:"Upload now β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Guidelines"})}),g.jsxs("div",{className:dt.sectionContent,children:["To make the process smoother, please follow the guidelines below:",g.jsxs("ul",{className:dt.guidelinesList,children:[g.jsx("li",{children:"Avoid uploading images that are not crisis maps/crisis drone images."}),g.jsx("li",{children:"Confirm the image details prior to modifying the description."}),g.jsx("li",{children:"Before the modification, please read the description generated and provide a rating via the rating sliders."}),g.jsx("li",{children:'Click the "Submit" button to save the description.'})]})]}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"see-examples",variant:"secondary",onClick:o,children:"See examples β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"VLMs"})}),g.jsx("div",{className:dt.sectionContent,children:"PromptAid Vision uses a variety of Visual Language Models (VLMs). A random VLM is selected for each upload. Therefore feel free to delete and reupload. You can view performance details here:"}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"view-vlm-details",variant:"secondary",onClick:s,children:"View VLM details β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Dataset"})}),g.jsx("div",{className:dt.sectionContent,children:"All users are able to export the dataset. You could apply filters when exporting, and it have the option to organize based on model fine-tuning formats."}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"export-dataset",variant:"secondary",onClick:()=>{n(!1),e("/explore"),setTimeout(()=>{const u=document.querySelector('[name="export-dataset"]');u&&u.click()},100)},children:"Export dataset β"})})]}),g.jsxs("div",{className:dt.helpSection,children:[g.jsx("div",{className:dt.sectionHeader,children:g.jsx(ro,{level:3,className:dt.sectionTitle,children:"Contact us"})}),g.jsx("div",{className:dt.sectionContent,children:"Need help or have questions about PromptAid Vision? Our team is here to support you."}),g.jsx("div",{className:dt.buttonContainer,children:g.jsx(Ue,{name:"contact-support",variant:"secondary",disabled:!0,children:"Get in touch β"})})]})]})})})}const mP=v.createContext(void 0),gP=({children:e})=>{const[n,i]=v.useState(!1),[o,s]=v.useState(!0),u=async()=>{const y=localStorage.getItem("adminToken");if(!y){i(!1),s(!1);return}try{(await fetch("/api/admin/verify",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${y}`}})).ok?i(!0):(localStorage.removeItem("adminToken"),i(!1))}catch(w){console.error("Error verifying admin token:",w),localStorage.removeItem("adminToken"),i(!1)}finally{s(!1)}},d=async y=>{try{const w=await fetch("/api/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({password:y})});if(w.ok){const C=await w.json();return localStorage.setItem("adminToken",C.access_token),i(!0),!0}else return!1}catch(w){return console.error("Login error:",w),!1}},f=()=>{localStorage.removeItem("adminToken"),i(!1)};v.useEffect(()=>{u()},[]);const p={isAuthenticated:n,isLoading:o,login:d,logout:f,verifyToken:u};return g.jsx(mP.Provider,{value:p,children:e})},vP=v.lazy(()=>Qn(()=>import("./index-D3L5y9N7.js"),__vite__mapDeps([0,1]))),yP=v.lazy(()=>Qn(()=>import("./index-hD4RFL5O.js"),__vite__mapDeps([2,3,4,5,6]))),wP=v.lazy(()=>Qn(()=>import("./AdminPage-DLWomD88.js"),__vite__mapDeps([7,3,8]))),xP=v.lazy(()=>Qn(()=>import("./index-BTevbmbp.js"),__vite__mapDeps([9,3,4,5,10])));class CP extends v.Component{constructor(n){super(n),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(n,i){console.error("Error caught by boundary:",n,i)}render(){return this.state.hasError?g.jsxs("div",{style:{padding:"20px",textAlign:"center"},children:[g.jsx("h2",{children:"Something went wrong"}),g.jsx("p",{children:"Please refresh the page to try again."}),g.jsx("button",{onClick:()=>window.location.reload(),children:"Refresh Page"})]}):this.props.children}}const bi=e=>{const n=e();return n.catch(()=>{}),n},SP=()=>{"requestIdleCallback"in window?requestIdleCallback(()=>{bi(()=>Qn(()=>import("./index-D3L5y9N7.js"),__vite__mapDeps([0,1]))),bi(()=>Qn(()=>import("./index-hD4RFL5O.js"),__vite__mapDeps([2,3,4,5,6]))),bi(()=>Qn(()=>import("./AdminPage-DLWomD88.js"),__vite__mapDeps([7,3,8]))),bi(()=>Qn(()=>import("./index-BTevbmbp.js"),__vite__mapDeps([9,3,4,5,10])))}):setTimeout(()=>{bi(()=>Qn(()=>import("./index-D3L5y9N7.js"),__vite__mapDeps([0,1]))),bi(()=>Qn(()=>import("./index-hD4RFL5O.js"),__vite__mapDeps([2,3,4,5,6]))),bi(()=>Qn(()=>import("./AdminPage-DLWomD88.js"),__vite__mapDeps([7,3,8]))),bi(()=>Qn(()=>import("./index-BTevbmbp.js"),__vite__mapDeps([9,3,4,5,10])))},1e3)},bP=vx([{element:g.jsx(LT,{}),children:[{path:"/",element:g.jsx($g,{})},{path:"/upload",element:g.jsx($g,{})},{path:"/analytics",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Analytics..."}),children:g.jsx(vP,{})})},{path:"/explore",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Explore..."}),children:g.jsx(yP,{})})},{path:"/help",element:g.jsx(pP,{})},{path:"/admin",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Admin..."}),children:g.jsx(wP,{})})},{path:"/map/:mapId",element:g.jsx(v.Suspense,{fallback:g.jsx("div",{children:"Loading Map Details..."}),children:g.jsx(xP,{})})}]}],{basename:"/"});function _P(){const[e,n]=v.useState([]);v.useEffect(()=>{SP()},[]);const i=v.useCallback(f=>{n(p=>Bx([...p,f],y=>y.name)??p)},[n]),o=v.useCallback(f=>{n(p=>{const y=p.findIndex(C=>C.name===f);if(y===-1)return p;const w=[...p];return w.splice(y,1),w})},[n]),s=v.useCallback((f,p)=>{n(y=>{const w=y.findIndex(m=>m.name===f);if(w===-1)return y;const C=[...y];return C[w]={...C[w],...p},C})},[n]),u=v.useMemo(()=>({alerts:e,addAlert:i,removeAlert:o,updateAlert:s}),[e,i,o,s]),d=v.useMemo(()=>({languageNamespaceStatus:{},setLanguageNamespaceStatus:()=>{},currentLanguage:"en",setCurrentLanguage:()=>{},strings:{},setStrings:()=>{},registerNamespace:()=>{}}),[]);return g.jsx(CP,{children:g.jsx(Fx.Provider,{value:u,children:g.jsx(o0.Provider,{value:d,children:g.jsx(gP,{children:g.jsx(Gj,{children:g.jsx(kx,{router:bP})})})})})})}function EP(){return g.jsx(_P,{})}console.log("React version at runtime:",v.version,"createContext exists?",!!v.createContext);V1.createRoot(document.getElementById("root")).render(g.jsx(v.StrictMode,{children:g.jsx(EP,{})}));export{_C as A,ra as B,TP as C,Zj as D,MP as E,df as F,Qn as G,ro as H,V as I,eo as J,NP as K,_T as L,Xj as M,Qf as N,jt as O,jC as P,DP as Q,c0 as R,TC as S,d0 as T,LP as U,Lg as V,mP as W,kP as X,Bg as Y,Wo as _,$e as a,Lt as b,IP as c,AP as d,jP as e,SC as f,l0 as g,RP as h,OP as i,g as j,fr as k,qx as l,FE as m,Ue as n,Oe as o,Zx as p,Ka as q,v as r,u0 as s,o0 as t,Dx as u,PC as v,MC as w,hs as x,PP as y,dr as z};
         | 
    	
        py_backend/static/assets/{jszip.min-uuN5m5dK.js β jszip.min-zGid42TK.js}
    RENAMED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            import{X as bt,Y as It}from"./index- | 
| 2 |  | 
| 3 | 
             
            JSZip v3.10.1 - A JavaScript class for generating and reading zip files
         | 
| 4 | 
             
            <http://stuartk.com/jszip>
         | 
|  | |
| 1 | 
            +
            import{X as bt,Y as It}from"./index-w0OOMPwN.js";function vt(yt){throw new Error('Could not dynamically require "'+yt+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var kt={exports:{}};/*!
         | 
| 2 |  | 
| 3 | 
             
            JSZip v3.10.1 - A JavaScript class for generating and reading zip files
         | 
| 4 | 
             
            <http://stuartk.com/jszip>
         | 
    	
        py_backend/static/assets/{useAdmin-DN0iYnPm.js β useAdmin-avReXN15.js}
    RENAMED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            import{r,W as e}from"./index- | 
|  | |
| 1 | 
            +
            import{r,W as e}from"./index-w0OOMPwN.js";const o=()=>{const t=r.useContext(e);if(t===void 0)throw new Error("useAdmin must be used within an AdminProvider");return t};export{o as u};
         | 
    	
        py_backend/static/index.html
    CHANGED
    
    | @@ -42,7 +42,7 @@ | |
| 42 | 
             
                    });
         | 
| 43 | 
             
                  }
         | 
| 44 | 
             
                </script>
         | 
| 45 | 
            -
                <script type="module" crossorigin src="/assets/index- | 
| 46 | 
             
                <link rel="stylesheet" crossorigin href="/assets/index-cCOeofBN.css">
         | 
| 47 | 
             
              </head>
         | 
| 48 | 
             
              <body>
         | 
|  | |
| 42 | 
             
                    });
         | 
| 43 | 
             
                  }
         | 
| 44 | 
             
                </script>
         | 
| 45 | 
            +
                <script type="module" crossorigin src="/assets/index-w0OOMPwN.js"></script>
         | 
| 46 | 
             
                <link rel="stylesheet" crossorigin href="/assets/index-cCOeofBN.css">
         | 
| 47 | 
             
              </head>
         | 
| 48 | 
             
              <body>
         | 
