feat(crossword): generated crosswords with clues
Browse filesSigned-off-by: Vimal Kumar <vimal78@gmail.com>
This view is limited to 50 files because it contains too many changes.  
							See raw diff
- .gitignore +18 -1
- CLAUDE.md +3 -1
- Dockerfile +5 -5
- VOCABULARY_OPTIMIZATION.md +164 -0
- crossword-app/backend-py/CROSSWORD_GENERATION_WALKTHROUGH.md +434 -0
- crossword-app/backend-py/README.md +224 -49
- crossword-app/backend-py/all-packages.txt +69 -0
- crossword-app/backend-py/app.py +80 -24
- crossword-app/backend-py/data/data +0 -1
- crossword-app/backend-py/data/word-lists/animals.json +0 -165
- crossword-app/backend-py/data/word-lists/geography.json +0 -161
- crossword-app/backend-py/data/word-lists/science.json +0 -170
- crossword-app/backend-py/data/word-lists/technology.json +0 -221
- crossword-app/backend-py/debug_full_generation.py +0 -316
- crossword-app/backend-py/debug_grid_direct.py +0 -293
- crossword-app/backend-py/debug_index_error.py +0 -307
- crossword-app/backend-py/debug_simple.py +0 -142
- crossword-app/backend-py/public/assets/index-2XJqMaqu.js +10 -0
- crossword-app/backend-py/public/assets/index-2XJqMaqu.js.map +1 -0
- crossword-app/backend-py/public/assets/index-7dkEH9uQ.js +10 -0
- crossword-app/backend-py/public/assets/index-7dkEH9uQ.js.map +1 -0
- crossword-app/backend-py/public/assets/index-CWqdoNhy.css +1 -0
- crossword-app/backend-py/public/assets/index-DyT-gQda.css +1 -0
- crossword-app/backend-py/public/assets/index-V4v18wFW.css +1 -0
- crossword-app/backend-py/public/assets/index-uK3VdD5a.js +10 -0
- crossword-app/backend-py/public/assets/index-uK3VdD5a.js.map +1 -0
- crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js +0 -0
- crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js.map +0 -0
- crossword-app/backend-py/public/index.html +16 -0
- crossword-app/backend-py/requirements.txt +13 -10
- crossword-app/backend-py/src/routes/api.py +144 -27
- crossword-app/backend-py/src/services/// +12 -0
- crossword-app/backend-py/src/services/__pycache__/__init__.cpython-310.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/__init__.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-310.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator_fixed.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator_wrapper.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/vector_search.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/word_cache.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/clue_generator.py +35 -0
- crossword-app/backend-py/src/services/crossword_generator.py +150 -89
- crossword-app/backend-py/src/services/crossword_generator_wrapper.py +16 -12
- crossword-app/backend-py/src/services/thematic_word_service.py +1057 -0
- crossword-app/backend-py/src/services/unified_word_service.py +250 -0
- crossword-app/backend-py/src/services/vector_search.py +109 -106
- crossword-app/backend-py/src/services/wordnet_clue_generator.py +640 -0
- crossword-app/backend-py/test-integration/test_boundary_fix.py +0 -147
- crossword-app/backend-py/test-integration/test_bounds_comprehensive.py +0 -266
- crossword-app/backend-py/test-integration/test_bounds_fix.py +0 -90
    	
        .gitignore
    CHANGED
    
    | @@ -49,7 +49,24 @@ pids | |
| 49 | 
             
            ehthumbs.db
         | 
| 50 | 
             
            Thumbs.db
         | 
| 51 |  | 
| 52 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 53 | 
             
            issues/
         | 
| 54 | 
             
            samples/
         | 
| 55 | 
             
            venv/
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 49 | 
             
            ehthumbs.db
         | 
| 50 | 
             
            Thumbs.db
         | 
| 51 |  | 
| 52 | 
            +
            # Python
         | 
| 53 | 
            +
            __pycache__/
         | 
| 54 | 
            +
            *.py[cod]
         | 
| 55 | 
            +
            *$py.class
         | 
| 56 | 
            +
            *.so
         | 
| 57 | 
            +
            .Python
         | 
| 58 | 
            +
            *.egg-info/
         | 
| 59 | 
            +
            .pytest_cache/
         | 
| 60 | 
            +
            .coverage
         | 
| 61 | 
            +
            htmlcov/
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            # hack
         | 
| 64 | 
             
            issues/
         | 
| 65 | 
             
            samples/
         | 
| 66 | 
             
            venv/
         | 
| 67 | 
            +
            crossword-app/backend-py/src/services/model_cache/
         | 
| 68 | 
            +
            hack/model_cache/
         | 
| 69 | 
            +
            cache-dir/
         | 
| 70 | 
            +
            .KARO.md
         | 
| 71 | 
            +
            CLAUDE.md
         | 
| 72 | 
            +
            crossword-app/backend-py/faiss_cache/
         | 
    	
        CLAUDE.md
    CHANGED
    
    | @@ -214,4 +214,6 @@ DATABASE_URL=postgresql://user:pass@host:port/db  # Optional | |
| 214 | 
             
            - Docker build time: ~5-10 minutes (includes frontend build + Python deps)
         | 
| 215 | 
             
            - Container size: ~1.5GB (includes ML models and dependencies)
         | 
| 216 | 
             
            - Hugging Face Spaces deployment: Automatic on git push
         | 
| 217 | 
            -
            - run unit tests after fixing a bug
         | 
|  | |
|  | 
|  | |
| 214 | 
             
            - Docker build time: ~5-10 minutes (includes frontend build + Python deps)
         | 
| 215 | 
             
            - Container size: ~1.5GB (includes ML models and dependencies)
         | 
| 216 | 
             
            - Hugging Face Spaces deployment: Automatic on git push
         | 
| 217 | 
            +
            - run unit tests after fixing a bug
         | 
| 218 | 
            +
            - do not use any static files for any word generation or clue gebneration.
         | 
| 219 | 
            +
            - we do not prefer inference api based solution
         | 
    	
        Dockerfile
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 | 
             
            # Multi-stage build to optimize performance and security
         | 
| 2 | 
             
            # Stage 1: Builder - Install dependencies and build as root
         | 
| 3 | 
            -
            FROM python:3.11-slim  | 
| 4 |  | 
| 5 | 
             
            # Set working directory
         | 
| 6 | 
             
            WORKDIR /app
         | 
| @@ -43,7 +43,7 @@ RUN mkdir -p backend-py/public && cp -r frontend/dist/* backend-py/public/ | |
| 43 | 
             
            RUN cd backend-py && ln -sf ../backend/data data
         | 
| 44 |  | 
| 45 | 
             
            # Stage 2: Runtime - Copy only necessary files as non-root user
         | 
| 46 | 
            -
            FROM python:3.11-slim  | 
| 47 |  | 
| 48 | 
             
            # Copy Python packages from builder stage
         | 
| 49 | 
             
            COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
         | 
| @@ -78,8 +78,8 @@ ENV PYTHONUNBUFFERED=1 | |
| 78 | 
             
            ENV PIP_NO_CACHE_DIR=1
         | 
| 79 |  | 
| 80 | 
             
            # Health check
         | 
| 81 | 
            -
            HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
         | 
| 82 | 
            -
             | 
| 83 |  | 
| 84 | 
             
            # Start the Python backend server with uvicorn for better production performance
         | 
| 85 | 
            -
            CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
         | 
|  | |
| 1 | 
             
            # Multi-stage build to optimize performance and security
         | 
| 2 | 
             
            # Stage 1: Builder - Install dependencies and build as root
         | 
| 3 | 
            +
            FROM python:3.11-slim AS builder
         | 
| 4 |  | 
| 5 | 
             
            # Set working directory
         | 
| 6 | 
             
            WORKDIR /app
         | 
|  | |
| 43 | 
             
            RUN cd backend-py && ln -sf ../backend/data data
         | 
| 44 |  | 
| 45 | 
             
            # Stage 2: Runtime - Copy only necessary files as non-root user
         | 
| 46 | 
            +
            FROM python:3.11-slim AS runtime
         | 
| 47 |  | 
| 48 | 
             
            # Copy Python packages from builder stage
         | 
| 49 | 
             
            COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
         | 
|  | |
| 78 | 
             
            ENV PIP_NO_CACHE_DIR=1
         | 
| 79 |  | 
| 80 | 
             
            # Health check
         | 
| 81 | 
            +
            # HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
         | 
| 82 | 
            +
            #     CMD curl -f http://localhost:7860/health || exit 1
         | 
| 83 |  | 
| 84 | 
             
            # Start the Python backend server with uvicorn for better production performance
         | 
| 85 | 
            +
            CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
         | 
    	
        VOCABULARY_OPTIMIZATION.md
    ADDED
    
    | @@ -0,0 +1,164 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            # Vocabulary Optimization & Unification
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            ## Problem Solved
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Previously, the crossword system had **vocabulary redundancy** with 3 separate sources:
         | 
| 6 | 
            +
            - **SentenceTransformer Model Vocabulary**: ~30K tokens → ~8-12K actual words after filtering
         | 
| 7 | 
            +
            - **NLTK Words Corpus**: 41,998 words for embeddings in thematic generator  
         | 
| 8 | 
            +
            - **WordFreq Database**: 319,938 words for frequency data
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            This created inconsistencies, memory waste, and limited vocabulary coverage.
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            ## Solution: Unified Architecture
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            ### New Design
         | 
| 15 | 
            +
            - **Single Vocabulary Source**: WordFreq database (319,938 words)
         | 
| 16 | 
            +
            - **Single Embedding Model**: all-mpnet-base-v2 (generates embeddings for any text)
         | 
| 17 | 
            +
            - **Unified Filtering**: Consistent crossword-suitable word filtering
         | 
| 18 | 
            +
            - **Shared Caching**: Single vocabulary + embeddings + frequency cache
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            ### Key Components
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            #### 1. VocabularyManager (`hack/thematic_word_generator.py`)
         | 
| 23 | 
            +
            - Loads and filters WordFreq vocabulary
         | 
| 24 | 
            +
            - Applies crossword-suitable filtering (3-12 chars, alphabetic, excludes boring words)
         | 
| 25 | 
            +
            - Generates frequency data with 10-tier classification
         | 
| 26 | 
            +
            - Handles caching for performance
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            #### 2. UnifiedThematicWordGenerator (`hack/thematic_word_generator.py`) 
         | 
| 29 | 
            +
            - Uses WordFreq vocabulary instead of NLTK words
         | 
| 30 | 
            +
            - Generates all-mpnet-base-v2 embeddings for WordFreq words
         | 
| 31 | 
            +
            - Maintains 10-tier frequency classification system
         | 
| 32 | 
            +
            - Provides both hack tool API and backend-compatible API
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            #### 3. UnifiedWordService (`crossword-app/backend-py/src/services/unified_word_service.py`)
         | 
| 35 | 
            +
            - Bridge adapter for backend integration
         | 
| 36 | 
            +
            - Compatible with existing VectorSearchService interface
         | 
| 37 | 
            +
            - Uses comprehensive WordFreq vocabulary instead of limited model vocabulary
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ## Usage
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            ### For Hack Tools
         | 
| 42 | 
            +
            ```python
         | 
| 43 | 
            +
            from thematic_word_generator import UnifiedThematicWordGenerator
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            # Initialize with desired vocabulary size
         | 
| 46 | 
            +
            generator = UnifiedThematicWordGenerator(vocab_size_limit=100000)
         | 
| 47 | 
            +
            generator.initialize()
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            # Generate thematic words with tier info
         | 
| 50 | 
            +
            results = generator.generate_thematic_words(
         | 
| 51 | 
            +
                topic="science", 
         | 
| 52 | 
            +
                num_words=10,
         | 
| 53 | 
            +
                difficulty_tier="tier_5_common"  # Optional tier filtering
         | 
| 54 | 
            +
            )
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            for word, similarity, tier in results:
         | 
| 57 | 
            +
                print(f"{word}: {similarity:.3f} ({tier})")
         | 
| 58 | 
            +
            ```
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            ### For Backend Integration
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            #### Option 1: Replace VectorSearchService
         | 
| 63 | 
            +
            ```python
         | 
| 64 | 
            +
            # In crossword_generator.py
         | 
| 65 | 
            +
            from .unified_word_service import create_unified_word_service
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            # Initialize
         | 
| 68 | 
            +
            vector_service = await create_unified_word_service(vocab_size_limit=100000)
         | 
| 69 | 
            +
            crossword_gen = CrosswordGenerator(vector_service=vector_service)
         | 
| 70 | 
            +
            ```
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            #### Option 2: Direct Usage
         | 
| 73 | 
            +
            ```python
         | 
| 74 | 
            +
            from .unified_word_service import UnifiedWordService
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            service = UnifiedWordService(vocab_size_limit=100000)
         | 
| 77 | 
            +
            await service.initialize()
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            # Compatible with existing interface
         | 
| 80 | 
            +
            words = await service.find_similar_words("animal", "medium", max_words=15)
         | 
| 81 | 
            +
            ```
         | 
| 82 | 
            +
             | 
| 83 | 
            +
            ## Performance Improvements
         | 
| 84 | 
            +
             | 
| 85 | 
            +
            ### Memory Usage
         | 
| 86 | 
            +
            - **Before**: 3 separate vocabularies + embeddings (~500MB+)
         | 
| 87 | 
            +
            - **After**: Single vocabulary + embeddings (~200MB)
         | 
| 88 | 
            +
            - **Reduction**: ~60% memory usage reduction
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            ### Vocabulary Coverage  
         | 
| 91 | 
            +
            - **Before**: Limited to ~8-12K words from model tokenizer
         | 
| 92 | 
            +
            - **After**: Up to 100K+ filtered words from WordFreq database
         | 
| 93 | 
            +
            - **Improvement**: 10x+ vocabulary coverage
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            ### Consistency
         | 
| 96 | 
            +
            - **Before**: Different words available in hack tools vs backend
         | 
| 97 | 
            +
            - **After**: Same comprehensive vocabulary across all components
         | 
| 98 | 
            +
            - **Benefit**: Consistent word quality and availability
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            ## Configuration
         | 
| 101 | 
            +
             | 
| 102 | 
            +
            ### Environment Variables
         | 
| 103 | 
            +
            - `MAX_VOCABULARY_SIZE`: Maximum vocabulary size (default: 100000)
         | 
| 104 | 
            +
            - `EMBEDDING_MODEL`: Model name (default: all-mpnet-base-v2)
         | 
| 105 | 
            +
            - `WORD_SIMILARITY_THRESHOLD`: Minimum similarity (default: 0.3)
         | 
| 106 | 
            +
             | 
| 107 | 
            +
            ### Vocabulary Size Options
         | 
| 108 | 
            +
            - **Small (10K)**: Fast initialization, basic vocabulary
         | 
| 109 | 
            +
            - **Medium (50K)**: Balanced performance and coverage  
         | 
| 110 | 
            +
            - **Large (100K)**: Comprehensive coverage, slower initialization
         | 
| 111 | 
            +
            - **Full (319K)**: Complete WordFreq database, longest initialization
         | 
| 112 | 
            +
             | 
| 113 | 
            +
            ## Migration Guide
         | 
| 114 | 
            +
             | 
| 115 | 
            +
            ### For Existing Hack Tools
         | 
| 116 | 
            +
            1. Update imports: `from thematic_word_generator import UnifiedThematicWordGenerator`
         | 
| 117 | 
            +
            2. Replace `ThematicWordGenerator` with `UnifiedThematicWordGenerator`
         | 
| 118 | 
            +
            3. API remains compatible, but now uses comprehensive WordFreq vocabulary
         | 
| 119 | 
            +
             | 
| 120 | 
            +
            ### For Backend Services
         | 
| 121 | 
            +
            1. Import: `from .unified_word_service import UnifiedWordService`
         | 
| 122 | 
            +
            2. Replace `VectorSearchService` initialization with `UnifiedWordService`
         | 
| 123 | 
            +
            3. All existing methods remain compatible
         | 
| 124 | 
            +
            4. Benefits: Better vocabulary coverage, consistent frequency data
         | 
| 125 | 
            +
             | 
| 126 | 
            +
            ### Backwards Compatibility
         | 
| 127 | 
            +
            - All existing APIs maintained
         | 
| 128 | 
            +
            - Same method signatures and return formats
         | 
| 129 | 
            +
            - Gradual migration possible - can run both systems in parallel
         | 
| 130 | 
            +
             | 
| 131 | 
            +
            ## Benefits Summary
         | 
| 132 | 
            +
             | 
| 133 | 
            +
            ✅ **Eliminates Redundancy**: Single vocabulary source instead of 3 separate ones  
         | 
| 134 | 
            +
            ✅ **Improves Coverage**: 100K+ words vs previous 8-12K words  
         | 
| 135 | 
            +
            ✅ **Reduces Memory**: ~60% reduction in memory usage  
         | 
| 136 | 
            +
            ✅ **Ensures Consistency**: Same vocabulary across hack tools and backend  
         | 
| 137 | 
            +
            ✅ **Maintains Performance**: Smart caching and batch processing  
         | 
| 138 | 
            +
            ✅ **Preserves Features**: 10-tier frequency classification, difficulty filtering  
         | 
| 139 | 
            +
            ✅ **Enables Growth**: Easy to add new features with unified architecture  
         | 
| 140 | 
            +
             | 
| 141 | 
            +
            ## Cache Management
         | 
| 142 | 
            +
             | 
| 143 | 
            +
            ### Cache Locations
         | 
| 144 | 
            +
            - **Hack tools**: `hack/model_cache/`
         | 
| 145 | 
            +
            - **Backend**: `crossword-app/backend-py/cache/unified_generator/`
         | 
| 146 | 
            +
             | 
| 147 | 
            +
            ### Cache Files
         | 
| 148 | 
            +
            - `unified_vocabulary_<size>.pkl`: Filtered vocabulary
         | 
| 149 | 
            +
            - `unified_frequencies_<size>.pkl`: Frequency data  
         | 
| 150 | 
            +
            - `unified_embeddings_<model>_<size>.npy`: Pre-computed embeddings
         | 
| 151 | 
            +
             | 
| 152 | 
            +
            ### Cache Invalidation
         | 
| 153 | 
            +
            Caches are automatically rebuilt if:
         | 
| 154 | 
            +
            - Vocabulary size limit changes
         | 
| 155 | 
            +
            - Embedding model changes
         | 
| 156 | 
            +
            - WordFreq database updates (rare)
         | 
| 157 | 
            +
             | 
| 158 | 
            +
            ## Future Enhancements
         | 
| 159 | 
            +
             | 
| 160 | 
            +
            1. **Semantic Clustering**: Group words by semantic similarity
         | 
| 161 | 
            +
            2. **Dynamic Difficulty**: Real-time difficulty adjustment based on user performance  
         | 
| 162 | 
            +
            3. **Topic Expansion**: Automatic topic discovery and expansion
         | 
| 163 | 
            +
            4. **Multilingual Support**: Extend to other languages using WordFreq
         | 
| 164 | 
            +
            5. **Custom Vocabularies**: Allow domain-specific vocabulary additions
         | 
    	
        crossword-app/backend-py/CROSSWORD_GENERATION_WALKTHROUGH.md
    ADDED
    
    | @@ -0,0 +1,434 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            # Crossword Generation Code Walkthrough
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This document provides a detailed, line-by-line walkthrough of how crossword puzzle generation works in the Python backend, tracing the complete flow from API request to finalized words and clues.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## Overview
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            The Python backend implements AI-powered crossword generation using:
         | 
| 8 | 
            +
            - **FastAPI** for web framework and API endpoints
         | 
| 9 | 
            +
            - **Vector similarity search** with sentence-transformers and FAISS for intelligent word discovery
         | 
| 10 | 
            +
            - **Backtracking algorithm** for word placement in the crossword grid
         | 
| 11 | 
            +
            - **Multi-layer caching system** for performance and fallback mechanisms
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ## 1. Application Startup (`app.py`)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            ### Entry Point and Service Initialization
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            ```python
         | 
| 18 | 
            +
            # Lines 66-95: Async lifespan context manager
         | 
| 19 | 
            +
            @asynccontextmanager
         | 
| 20 | 
            +
            async def lifespan(app: FastAPI):
         | 
| 21 | 
            +
                global vector_service
         | 
| 22 | 
            +
                
         | 
| 23 | 
            +
                # Startup initialization
         | 
| 24 | 
            +
                vector_service = VectorSearchService()
         | 
| 25 | 
            +
                await vector_service.initialize()
         | 
| 26 | 
            +
                app.state.vector_service = vector_service
         | 
| 27 | 
            +
            ```
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            **Flow:**
         | 
| 30 | 
            +
            1. FastAPI application starts with lifespan context manager
         | 
| 31 | 
            +
            2. Creates global `VectorSearchService` instance (line 79)
         | 
| 32 | 
            +
            3. Calls `vector_service.initialize()` (line 82) which:
         | 
| 33 | 
            +
               - Loads sentence-transformer model (~30-60 seconds)
         | 
| 34 | 
            +
               - Builds or loads cached FAISS index for word embeddings
         | 
| 35 | 
            +
               - Initializes word cache manager for fallbacks
         | 
| 36 | 
            +
            4. Makes vector service available to all API routes via `app.state`
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            ## 2. API Request Handling (`src/routes/api.py`)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            ### Crossword Generation Endpoint
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            ```python
         | 
| 43 | 
            +
            # Lines 77-118: Main crossword generation endpoint
         | 
| 44 | 
            +
            @router.post("/generate", response_model=PuzzleResponse)
         | 
| 45 | 
            +
            async def generate_puzzle(
         | 
| 46 | 
            +
                request: GeneratePuzzleRequest,
         | 
| 47 | 
            +
                crossword_gen: CrosswordGenerator = Depends(get_crossword_generator)
         | 
| 48 | 
            +
            ):
         | 
| 49 | 
            +
            ```
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            **Flow:**
         | 
| 52 | 
            +
            1. Client POST request to `/api/generate` with JSON body:
         | 
| 53 | 
            +
               ```json
         | 
| 54 | 
            +
               {
         | 
| 55 | 
            +
                 "topics": ["animals", "science"], 
         | 
| 56 | 
            +
                 "difficulty": "medium",
         | 
| 57 | 
            +
                    }
         | 
| 58 | 
            +
               ```
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            2. FastAPI validates request against `GeneratePuzzleRequest` model (lines 20-23)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            3. Dependency injection calls `get_crossword_generator()` (lines 57-63):
         | 
| 63 | 
            +
               ```python
         | 
| 64 | 
            +
               def get_crossword_generator(request: Request) -> CrosswordGenerator:
         | 
| 65 | 
            +
                   global generator
         | 
| 66 | 
            +
                   if generator is None:
         | 
| 67 | 
            +
                       vector_service = getattr(request.app.state, 'vector_service', None)
         | 
| 68 | 
            +
                       generator = CrosswordGenerator(vector_service)
         | 
| 69 | 
            +
                   return generator
         | 
| 70 | 
            +
               ```
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            4. Creates or reuses `CrosswordGenerator` wrapper with vector service reference
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            ## 3. Crossword Generation Wrapper (`src/services/crossword_generator_wrapper.py`)
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            ### Simple Delegation Layer
         | 
| 77 | 
            +
             | 
| 78 | 
            +
            ```python
         | 
| 79 | 
            +
            # Lines 20-51: Generate puzzle method
         | 
| 80 | 
            +
            async def generate_puzzle(
         | 
| 81 | 
            +
                self, 
         | 
| 82 | 
            +
                topics: List[str], 
         | 
| 83 | 
            +
                difficulty: str = "medium", 
         | 
| 84 | 
            +
                ) -> Dict[str, Any]:
         | 
| 85 | 
            +
            ```
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            **Flow:**
         | 
| 88 | 
            +
            1. Wrapper receives request from API route
         | 
| 89 | 
            +
            2. **Line 41**: Imports actual generator to avoid circular imports:
         | 
| 90 | 
            +
               ```python
         | 
| 91 | 
            +
               from .crossword_generator import CrosswordGenerator as ActualGenerator
         | 
| 92 | 
            +
               ```
         | 
| 93 | 
            +
            3. **Line 42**: Creates actual generator instance with vector service
         | 
| 94 | 
            +
            4. **Line 44**: Delegates to actual generator's `generate_puzzle()` method
         | 
| 95 | 
            +
             | 
| 96 | 
            +
            ## 4. Core Crossword Generation (`src/services/crossword_generator.py`)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            ### 4.1 Main Generation Method
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            ```python
         | 
| 101 | 
            +
            # Lines 22-66: Core puzzle generation
         | 
| 102 | 
            +
            async def generate_puzzle(self, topics: List[str], difficulty: str = "medium",  = False):
         | 
| 103 | 
            +
            ```
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            **Key steps:**
         | 
| 106 | 
            +
             | 
| 107 | 
            +
            1. **Line 37**: Select words using AI or static sources:
         | 
| 108 | 
            +
               ```python
         | 
| 109 | 
            +
               words = await self._select_words(topics, difficulty, use_ai)
         | 
| 110 | 
            +
               ```
         | 
| 111 | 
            +
             | 
| 112 | 
            +
            2. **Line 44**: Create crossword grid from selected words:
         | 
| 113 | 
            +
               ```python
         | 
| 114 | 
            +
               grid_result = self._create_grid(words)
         | 
| 115 | 
            +
               ```
         | 
| 116 | 
            +
             | 
| 117 | 
            +
            3. **Lines 52-62**: Return structured response with grid, clues, and metadata
         | 
| 118 | 
            +
             | 
| 119 | 
            +
            ### 4.2 Word Selection Process
         | 
| 120 | 
            +
             | 
| 121 | 
            +
            ```python
         | 
| 122 | 
            +
            # Lines 68-97: Word selection logic
         | 
| 123 | 
            +
            async def _select_words(self, topics: List[str], difficulty: str, ):
         | 
| 124 | 
            +
            ```
         | 
| 125 | 
            +
             | 
| 126 | 
            +
            **Flow branches based on `use_ai` parameter:**
         | 
| 127 | 
            +
             | 
| 128 | 
            +
            #### AI-Powered Word Selection (Lines 72-83):
         | 
| 129 | 
            +
            ```python
         | 
| 130 | 
            +
            if use_ai and self.vector_service:
         | 
| 131 | 
            +
                for topic in topics:
         | 
| 132 | 
            +
                    ai_words = await self.vector_service.find_similar_words(
         | 
| 133 | 
            +
                        topic, difficulty, self.max_words // len(topics)
         | 
| 134 | 
            +
                    )
         | 
| 135 | 
            +
                    all_words.extend(ai_words)
         | 
| 136 | 
            +
            ```
         | 
| 137 | 
            +
             | 
| 138 | 
            +
            #### Fallback to Cached Words (Lines 86-91):
         | 
| 139 | 
            +
            ```python
         | 
| 140 | 
            +
            if self.vector_service:
         | 
| 141 | 
            +
                for topic in topics:
         | 
| 142 | 
            +
                    cached_words = await self.vector_service._get_cached_fallback(
         | 
| 143 | 
            +
                        topic, difficulty, self.max_words // len(topics)
         | 
| 144 | 
            +
                    )
         | 
| 145 | 
            +
            ```
         | 
| 146 | 
            +
             | 
| 147 | 
            +
            #### Final Fallback to Static JSON Files (Lines 93-95):
         | 
| 148 | 
            +
            ```python
         | 
| 149 | 
            +
            else:
         | 
| 150 | 
            +
                all_words = await self._get_static_words(topics, difficulty)
         | 
| 151 | 
            +
            ```
         | 
| 152 | 
            +
             | 
| 153 | 
            +
            ### 4.3 Word Sorting for Crossword Viability
         | 
| 154 | 
            +
             | 
| 155 | 
            +
            ```python
         | 
| 156 | 
            +
            # Lines 129-168: Sort words by crossword suitability
         | 
| 157 | 
            +
            def _sort_words_for_crossword(self, words: List[Dict[str, Any]]):
         | 
| 158 | 
            +
            ```
         | 
| 159 | 
            +
             | 
| 160 | 
            +
            **Scoring algorithm:**
         | 
| 161 | 
            +
            - **Lines 138-147**: Length-based scoring (shorter words preferred)
         | 
| 162 | 
            +
            - **Lines 150-153**: Common letter bonus (E, A, R, I, O, T, N, S)
         | 
| 163 | 
            +
            - **Lines 156-158**: Vowel distribution bonus
         | 
| 164 | 
            +
            - **Lines 161-162**: Penalty for very long words
         | 
| 165 | 
            +
             | 
| 166 | 
            +
            ## 5. AI Word Discovery (`src/services/vector_search.py`)
         | 
| 167 | 
            +
             | 
| 168 | 
            +
            ### 5.1 Vector Search Initialization
         | 
| 169 | 
            +
             | 
| 170 | 
            +
            ```python
         | 
| 171 | 
            +
            # Lines 71-143: Service initialization
         | 
| 172 | 
            +
            async def initialize(self):
         | 
| 173 | 
            +
            ```
         | 
| 174 | 
            +
             | 
| 175 | 
            +
            **Initialization flow:**
         | 
| 176 | 
            +
            1. **Lines 90-92**: Load sentence-transformer model
         | 
| 177 | 
            +
            2. **Lines 95-119**: Build or load cached FAISS index
         | 
| 178 | 
            +
            3. **Lines 124-134**: Initialize word cache manager
         | 
| 179 | 
            +
             | 
| 180 | 
            +
            ### 5.2 Core Word Finding Algorithm
         | 
| 181 | 
            +
             | 
| 182 | 
            +
            ```python
         | 
| 183 | 
            +
            # Lines 279-374: Main word discovery method
         | 
| 184 | 
            +
            async def find_similar_words(
         | 
| 185 | 
            +
                self, 
         | 
| 186 | 
            +
                topic: str, 
         | 
| 187 | 
            +
                difficulty: str = "medium", 
         | 
| 188 | 
            +
                max_words: int = 15
         | 
| 189 | 
            +
            ):
         | 
| 190 | 
            +
            ```
         | 
| 191 | 
            +
             | 
| 192 | 
            +
            **Search strategy branches:**
         | 
| 193 | 
            +
             | 
| 194 | 
            +
            #### Hierarchical Search (Lines 296-325):
         | 
| 195 | 
            +
            ```python
         | 
| 196 | 
            +
            if self.use_hierarchical_search:
         | 
| 197 | 
            +
                all_candidates = await self._hierarchical_search(topic, difficulty, max_words)
         | 
| 198 | 
            +
                combined_results = self._combine_hierarchical_results(all_candidates, max_words * 2)
         | 
| 199 | 
            +
            ```
         | 
| 200 | 
            +
             | 
| 201 | 
            +
            #### Traditional Single Search (Lines 328-337):
         | 
| 202 | 
            +
            ```python
         | 
| 203 | 
            +
            else:
         | 
| 204 | 
            +
                traditional_results = await self._traditional_single_search(topic, difficulty, max_words * 2)
         | 
| 205 | 
            +
            ```
         | 
| 206 | 
            +
             | 
| 207 | 
            +
            ### 5.3 Hierarchical Search Process
         | 
| 208 | 
            +
             | 
| 209 | 
            +
            ```python
         | 
| 210 | 
            +
            # Lines 639-748: Multi-phase hierarchical search
         | 
| 211 | 
            +
            async def _hierarchical_search(self, topic: str, difficulty: str, max_words: int):
         | 
| 212 | 
            +
            ```
         | 
| 213 | 
            +
             | 
| 214 | 
            +
            **Three-phase approach:**
         | 
| 215 | 
            +
             | 
| 216 | 
            +
            #### Phase 1: Topic Variations (Lines 652-694)
         | 
| 217 | 
            +
            ```python
         | 
| 218 | 
            +
            topic_variations = self._expand_topic_variations(topic)  # "Animal" → ["Animal", "Animals"]
         | 
| 219 | 
            +
             | 
| 220 | 
            +
            for variation in topic_variations:
         | 
| 221 | 
            +
                topic_embedding = self.model.encode([variation], convert_to_numpy=True)
         | 
| 222 | 
            +
                scores, indices = self.faiss_index.search(topic_embedding, search_size)
         | 
| 223 | 
            +
                variation_candidates = self._collect_candidates_with_threshold(scores, indices, threshold, variation, difficulty)
         | 
| 224 | 
            +
            ```
         | 
| 225 | 
            +
             | 
| 226 | 
            +
            #### Phase 2: Subcategory Identification (Lines 697-700)
         | 
| 227 | 
            +
            ```python
         | 
| 228 | 
            +
            subcategories = self._identify_subcategories(main_topic_candidates, topic)
         | 
| 229 | 
            +
            ```
         | 
| 230 | 
            +
             | 
| 231 | 
            +
            #### Phase 3: Subcategory Search (Lines 703-733)
         | 
| 232 | 
            +
            ```python
         | 
| 233 | 
            +
            for subcategory in subcategories:
         | 
| 234 | 
            +
                subcat_embedding = self.model.encode([subcategory], convert_to_numpy=True)
         | 
| 235 | 
            +
                sub_scores, sub_indices = self.faiss_index.search(subcat_embedding, sub_search_size)
         | 
| 236 | 
            +
            ```
         | 
| 237 | 
            +
             | 
| 238 | 
            +
            ### 5.4 Word Quality Filtering
         | 
| 239 | 
            +
             | 
| 240 | 
            +
            ```python
         | 
| 241 | 
            +
            # Lines 1164-1215: Candidate collection with filtering
         | 
| 242 | 
            +
            def _collect_candidates_with_threshold(self, scores, indices, threshold, topic, difficulty):
         | 
| 243 | 
            +
            ```
         | 
| 244 | 
            +
             | 
| 245 | 
            +
            **Multi-stage filtering:**
         | 
| 246 | 
            +
            1. **Line 1176**: Similarity threshold check
         | 
| 247 | 
            +
            2. **Line 1183**: Difficulty matching (word length)
         | 
| 248 | 
            +
            3. **Line 1185**: Interest and topic relevance check:
         | 
| 249 | 
            +
               ```python
         | 
| 250 | 
            +
               if self._is_interesting_word(word, topic) and self._is_topic_relevant(word, topic):
         | 
| 251 | 
            +
               ```
         | 
| 252 | 
            +
             | 
| 253 | 
            +
            ## 6. Grid Creation and Word Placement
         | 
| 254 | 
            +
             | 
| 255 | 
            +
            ### 6.1 Grid Generation Entry Point
         | 
| 256 | 
            +
             | 
| 257 | 
            +
            ```python
         | 
| 258 | 
            +
            # Lines 170-243: Main grid creation method
         | 
| 259 | 
            +
            def _create_grid(self, words: List[Dict[str, Any]]):
         | 
| 260 | 
            +
            ```
         | 
| 261 | 
            +
             | 
| 262 | 
            +
            **Flow:**
         | 
| 263 | 
            +
            1. **Lines 184-203**: Process and sort words by length (longest first)
         | 
| 264 | 
            +
            2. **Lines 209-213**: Calculate appropriate grid size
         | 
| 265 | 
            +
            3. **Lines 212-237**: Multiple placement attempts with increasing grid size
         | 
| 266 | 
            +
            4. **Lines 240-241**: Fallback to simple two-word cross
         | 
| 267 | 
            +
             | 
| 268 | 
            +
            ### 6.2 Word Placement Algorithm
         | 
| 269 | 
            +
             | 
| 270 | 
            +
            ```python
         | 
| 271 | 
            +
            # Lines 259-295: Backtracking word placement
         | 
| 272 | 
            +
            def _place_words_in_grid(self, word_list: List[str], word_objs: List[Dict[str, Any]], size: int):
         | 
| 273 | 
            +
            ```
         | 
| 274 | 
            +
             | 
| 275 | 
            +
            **Setup:**
         | 
| 276 | 
            +
            - **Line 263**: Initialize empty grid with dots
         | 
| 277 | 
            +
            - **Line 270**: Call recursive backtracking algorithm
         | 
| 278 | 
            +
            - **Lines 272-287**: Generate clues and assign crossword numbers
         | 
| 279 | 
            +
             | 
| 280 | 
            +
            ### 6.3 Backtracking Algorithm
         | 
| 281 | 
            +
             | 
| 282 | 
            +
            ```python
         | 
| 283 | 
            +
            # Lines 297-357: Recursive backtracking placement
         | 
| 284 | 
            +
            def _backtrack_placement(self, grid, word_list, word_objs, word_index, placed_words, start_time, timeout):
         | 
| 285 | 
            +
            ```
         | 
| 286 | 
            +
             | 
| 287 | 
            +
            **Algorithm flow:**
         | 
| 288 | 
            +
             | 
| 289 | 
            +
            #### Base Cases:
         | 
| 290 | 
            +
            - **Lines 302-303**: Timeout check every 50 calls
         | 
| 291 | 
            +
            - **Lines 305-306**: Success when all words placed
         | 
| 292 | 
            +
             | 
| 293 | 
            +
            #### First Word Placement (Lines 312-332):
         | 
| 294 | 
            +
            ```python
         | 
| 295 | 
            +
            if word_index == 0:
         | 
| 296 | 
            +
                center_row = size // 2
         | 
| 297 | 
            +
                center_col = (size - len(word)) // 2
         | 
| 298 | 
            +
                
         | 
| 299 | 
            +
                if self._can_place_word(grid, word, center_row, center_col, "horizontal"):
         | 
| 300 | 
            +
                    original_state = self._place_word(grid, word, center_row, center_col, "horizontal")
         | 
| 301 | 
            +
            ```
         | 
| 302 | 
            +
             | 
| 303 | 
            +
            #### Subsequent Word Placement (Lines 334-356):
         | 
| 304 | 
            +
            ```python
         | 
| 305 | 
            +
            all_placements = self._find_all_intersection_placements(grid, word, placed_words)
         | 
| 306 | 
            +
            all_placements.sort(key=lambda p: p["score"], reverse=True)
         | 
| 307 | 
            +
             | 
| 308 | 
            +
            for placement in all_placements:
         | 
| 309 | 
            +
                if self._can_place_word(grid, word, row, col, direction):
         | 
| 310 | 
            +
                    # Try placement and recurse
         | 
| 311 | 
            +
                    if self._backtrack_placement(...):
         | 
| 312 | 
            +
                        return True
         | 
| 313 | 
            +
                    # Backtrack if failed
         | 
| 314 | 
            +
                    self._remove_word(grid, original_state)
         | 
| 315 | 
            +
            ```
         | 
| 316 | 
            +
             | 
| 317 | 
            +
            ### 6.4 Word Placement Validation
         | 
| 318 | 
            +
             | 
| 319 | 
            +
            ```python
         | 
| 320 | 
            +
            # Lines 359-417: Comprehensive placement validation
         | 
| 321 | 
            +
            def _can_place_word(self, grid: List[List[str]], word: str, row: int, col: int, direction: str):
         | 
| 322 | 
            +
            ```
         | 
| 323 | 
            +
             | 
| 324 | 
            +
            **Critical validation checks:**
         | 
| 325 | 
            +
            1. **Lines 364-365**: Boundary checks
         | 
| 326 | 
            +
            2. **Lines 372-375**: Word boundary enforcement (no adjacent letters)
         | 
| 327 | 
            +
            3. **Lines 378-390**: Letter-by-letter placement validation
         | 
| 328 | 
            +
            4. **Lines 388-390**: Perpendicular intersection validation
         | 
| 329 | 
            +
             | 
| 330 | 
            +
            ### 6.5 Intersection Finding
         | 
| 331 | 
            +
             | 
| 332 | 
            +
            ```python
         | 
| 333 | 
            +
            # Lines 486-505: Find all possible intersections
         | 
| 334 | 
            +
            def _find_all_intersection_placements(self, grid, word, placed_words):
         | 
| 335 | 
            +
            ```
         | 
| 336 | 
            +
             | 
| 337 | 
            +
            **Process:**
         | 
| 338 | 
            +
            1. **Lines 491-502**: For each placed word, find letter intersections
         | 
| 339 | 
            +
            2. **Lines 496-502**: Calculate placement position for each intersection
         | 
| 340 | 
            +
            3. **Lines 499-502**: Score placement quality
         | 
| 341 | 
            +
             | 
| 342 | 
            +
            ## 7. Clue Generation and Final Assembly
         | 
| 343 | 
            +
             | 
| 344 | 
            +
            ### 7.1 Grid Trimming and Optimization
         | 
| 345 | 
            +
             | 
| 346 | 
            +
            ```python
         | 
| 347 | 
            +
            # Lines 589-642: Remove excess empty space
         | 
| 348 | 
            +
            def _trim_grid(self, grid, placed_words):
         | 
| 349 | 
            +
            ```
         | 
| 350 | 
            +
             | 
| 351 | 
            +
            **Process:**
         | 
| 352 | 
            +
            1. **Lines 595-610**: Find bounding box of all placed words
         | 
| 353 | 
            +
            2. **Lines 612-631**: Create trimmed grid with padding
         | 
| 354 | 
            +
            3. **Lines 634-641**: Update word positions relative to new grid
         | 
| 355 | 
            +
             | 
| 356 | 
            +
            ### 7.2 Crossword Numbering
         | 
| 357 | 
            +
             | 
| 358 | 
            +
            ```python
         | 
| 359 | 
            +
            # Lines 698-750: Assign proper crossword numbers and create clues
         | 
| 360 | 
            +
            def _assign_numbers_and_clues(self, placed_words, clues_data):
         | 
| 361 | 
            +
            ```
         | 
| 362 | 
            +
             | 
| 363 | 
            +
            **Crossword numbering rules:**
         | 
| 364 | 
            +
            1. **Lines 710-714**: Group words by starting position
         | 
| 365 | 
            +
            2. **Lines 716**: Sort by reading order (top-to-bottom, left-to-right)
         | 
| 366 | 
            +
            3. **Lines 725-749**: Assign shared numbers for words starting at same position
         | 
| 367 | 
            +
            4. **Lines 738-745**: Create clue objects with proper formatting
         | 
| 368 | 
            +
             | 
| 369 | 
            +
            ### 7.3 Final Response Assembly
         | 
| 370 | 
            +
             | 
| 371 | 
            +
            **Back in `crossword_generator.py` lines 52-62:**
         | 
| 372 | 
            +
            ```python
         | 
| 373 | 
            +
            return {
         | 
| 374 | 
            +
                "grid": grid_result["grid"],
         | 
| 375 | 
            +
                "clues": grid_result["clues"],
         | 
| 376 | 
            +
                "metadata": {
         | 
| 377 | 
            +
                    "topics": topics,
         | 
| 378 | 
            +
                    "difficulty": difficulty,
         | 
| 379 | 
            +
                    "wordCount": len(grid_result["placed_words"]),
         | 
| 380 | 
            +
                    "size": len(grid_result["grid"]),
         | 
| 381 | 
            +
                    "aiGenerated":     }
         | 
| 382 | 
            +
            }
         | 
| 383 | 
            +
            ```
         | 
| 384 | 
            +
             | 
| 385 | 
            +
            ## 8. Caching System (`src/services/word_cache.py`)
         | 
| 386 | 
            +
             | 
| 387 | 
            +
            ### 8.1 Cache Initialization
         | 
| 388 | 
            +
             | 
| 389 | 
            +
            ```python
         | 
| 390 | 
            +
            # Lines 75-113: Load existing cache files
         | 
| 391 | 
            +
            async def initialize(self):
         | 
| 392 | 
            +
            ```
         | 
| 393 | 
            +
             | 
| 394 | 
            +
            **Process:**
         | 
| 395 | 
            +
            1. **Lines 86-108**: Load all `.json` cache files from disk
         | 
| 396 | 
            +
            2. **Lines 99-102**: Validate cache structure and load into memory
         | 
| 397 | 
            +
            3. **Lines 110**: Report loaded cache statistics
         | 
| 398 | 
            +
             | 
| 399 | 
            +
            ### 8.2 Word Caching
         | 
| 400 | 
            +
             | 
| 401 | 
            +
            ```python
         | 
| 402 | 
            +
            # Lines 166-224: Cache successful word discoveries
         | 
| 403 | 
            +
            async def cache_words(self, topic, difficulty, words, source="vector_search"):
         | 
| 404 | 
            +
            ```
         | 
| 405 | 
            +
             | 
| 406 | 
            +
            **Storage process:**
         | 
| 407 | 
            +
            1. **Lines 186-193**: Enhance words with caching metadata
         | 
| 408 | 
            +
            2. **Lines 196-207**: Create structured cache data with expiration
         | 
| 409 | 
            +
            3. **Lines 210-213**: Save to disk (if permissions allow)
         | 
| 410 | 
            +
            4. **Lines 216-217**: Update in-memory cache
         | 
| 411 | 
            +
             | 
| 412 | 
            +
            ## Complete Data Flow Summary
         | 
| 413 | 
            +
             | 
| 414 | 
            +
            1. **API Request** → `/api/generate` with topics, difficulty, 2. **Route Handler** → Validates request, injects dependencies
         | 
| 415 | 
            +
            3. **Wrapper** → Delegates to actual generator with vector service
         | 
| 416 | 
            +
            4. **Word Selection** → AI vector search OR cached words OR static JSON fallback
         | 
| 417 | 
            +
            5. **Vector Search** (if AI enabled):
         | 
| 418 | 
            +
               - Load sentence-transformer model
         | 
| 419 | 
            +
               - Perform hierarchical semantic search
         | 
| 420 | 
            +
               - Filter by similarity threshold, difficulty, relevance
         | 
| 421 | 
            +
               - Apply word exclusions and variety filtering
         | 
| 422 | 
            +
            6. **Grid Creation**:
         | 
| 423 | 
            +
               - Sort words by crossword viability
         | 
| 424 | 
            +
               - Calculate appropriate grid size
         | 
| 425 | 
            +
               - Use backtracking algorithm to place words
         | 
| 426 | 
            +
               - Validate word boundaries and intersections
         | 
| 427 | 
            +
            7. **Grid Optimization**:
         | 
| 428 | 
            +
               - Trim excess empty space
         | 
| 429 | 
            +
               - Assign proper crossword numbers
         | 
| 430 | 
            +
               - Generate clue objects
         | 
| 431 | 
            +
            8. **Response Assembly** → Return grid, clues, and metadata
         | 
| 432 | 
            +
            9. **Caching** → Store successful AI discoveries for future use
         | 
| 433 | 
            +
             | 
| 434 | 
            +
            The system gracefully degrades from AI → cached words → static words, ensuring crossword generation always succeeds even when AI components fail.
         | 
    	
        crossword-app/backend-py/README.md
    CHANGED
    
    | @@ -1,24 +1,25 @@ | |
| 1 | 
            -
            # Python Backend with  | 
| 2 |  | 
| 3 | 
            -
            This is the Python implementation of the crossword generator backend, featuring  | 
| 4 |  | 
| 5 | 
             
            ## 🚀 Features
         | 
| 6 |  | 
| 7 | 
            -
            - ** | 
| 8 | 
            -
            - ** | 
|  | |
|  | |
| 9 | 
             
            - **FastAPI**: Modern, fast Python web framework
         | 
| 10 | 
             
            - **Same API**: Compatible with existing React frontend
         | 
| 11 | 
            -
            - **Hybrid Approach**: AI vector search with static word fallback
         | 
| 12 |  | 
| 13 | 
             
            ## 🔄 Differences from JavaScript Backend
         | 
| 14 |  | 
| 15 | 
             
            | Feature | JavaScript Backend | Python Backend |
         | 
| 16 | 
             
            |---------|-------------------|----------------|
         | 
| 17 | 
            -
            | **Word Generation** |  | 
| 18 | 
            -
            | **Vocabulary Size** | ~100 words per topic |  | 
| 19 | 
            -
            | **AI Approach** |  | 
| 20 | 
            -
            | **Performance** | Fast but limited | Slower startup,  | 
| 21 | 
            -
            | **Dependencies** | Node.js +  | 
| 22 |  | 
| 23 | 
             
            ## 🛠️ Setup & Installation
         | 
| 24 |  | 
| @@ -70,10 +71,11 @@ backend-py/ | |
| 70 | 
             
            ├── requirements-dev.txt            # Full development dependencies
         | 
| 71 | 
             
            ├── src/
         | 
| 72 | 
             
            │   ├── services/
         | 
| 73 | 
            -
            │   │   ├──  | 
| 74 | 
            -
            │   │    | 
|  | |
| 75 | 
             
            │   └── routes/
         | 
| 76 | 
            -
            │       └── api.py | 
| 77 | 
             
            ├── test-unit/                      # Unit tests (pytest framework) - 5 files
         | 
| 78 | 
             
            │   ├── test_crossword_generator.py
         | 
| 79 | 
             
            │   ├── test_api_routes.py
         | 
| @@ -90,8 +92,9 @@ backend-py/ | |
| 90 |  | 
| 91 | 
             
            ### Core ML Stack
         | 
| 92 | 
             
            - `sentence-transformers`: Local model loading and embeddings
         | 
| 93 | 
            -
            - ` | 
| 94 | 
             
            - `torch`: PyTorch for model inference
         | 
|  | |
| 95 | 
             
            - `numpy`: Vector operations
         | 
| 96 |  | 
| 97 | 
             
            ### Web Framework
         | 
| @@ -203,39 +206,56 @@ pytest test-unit/ --cov=src --cov-report=html --ignore=test-unit/test_vector_sea | |
| 203 |  | 
| 204 | 
             
            ## 🔧 Configuration
         | 
| 205 |  | 
| 206 | 
            -
            Environment  | 
|  | |
|  | |
| 207 |  | 
| 208 | 
             
            ```bash
         | 
| 209 | 
            -
            #  | 
| 210 | 
            -
             | 
| 211 | 
            -
             | 
|  | |
| 212 |  | 
| 213 | 
            -
            #  | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 216 |  | 
| 217 | 
             
            # Optional
         | 
| 218 | 
            -
            LOG_LEVEL=INFO
         | 
| 219 | 
             
            ```
         | 
| 220 |  | 
| 221 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 222 |  | 
| 223 | 
             
            1. **Initialization**:
         | 
|  | |
|  | |
| 224 | 
             
               - Load sentence-transformers model locally
         | 
| 225 | 
            -
               -  | 
| 226 | 
            -
               -  | 
| 227 | 
            -
               - Build FAISS index for fast similarity search
         | 
| 228 |  | 
| 229 | 
             
            2. **Word Generation**:
         | 
| 230 | 
             
               - Get topic embedding: `"Animals" → [768-dim vector]`
         | 
| 231 | 
            -
               -  | 
| 232 | 
            -
               - Filter by similarity threshold  | 
| 233 | 
            -
               - Filter by  | 
| 234 | 
             
               - Return top matches with generated clues
         | 
| 235 |  | 
| 236 | 
            -
            3. ** | 
| 237 | 
            -
               -  | 
| 238 | 
            -
               -  | 
|  | |
| 239 |  | 
| 240 | 
             
            ## 🧪 Testing
         | 
| 241 |  | 
| @@ -248,16 +268,163 @@ python test_local.py | |
| 248 | 
             
            python app.py
         | 
| 249 | 
             
            ```
         | 
| 250 |  | 
| 251 | 
            -
            ## 🐳  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 252 |  | 
| 253 | 
            -
             | 
| 254 |  | 
| 255 | 
            -
             | 
| 256 | 
            -
             | 
| 257 | 
            -
            #  | 
| 258 | 
            -
             | 
| 259 | 
            -
             | 
| 260 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 261 | 
             
            ```
         | 
| 262 |  | 
| 263 | 
             
            ## 🧪 Testing
         | 
| @@ -301,19 +468,25 @@ pytest tests/ --cov=src --cov-report=html | |
| 301 |  | 
| 302 | 
             
            **Startup Time**:
         | 
| 303 | 
             
            - JavaScript: ~2 seconds
         | 
| 304 | 
            -
            - Python: ~30-60 seconds (model download +  | 
|  | |
| 305 |  | 
| 306 | 
             
            **Word Quality**:
         | 
| 307 | 
            -
            - JavaScript: Limited by static word lists
         | 
| 308 | 
            -
            - Python:  | 
| 309 |  | 
| 310 | 
             
            **Memory Usage**:
         | 
| 311 | 
             
            - JavaScript: ~100MB
         | 
| 312 | 
            -
            - Python: ~500MB-1GB (model + embeddings | 
|  | |
| 313 |  | 
| 314 | 
             
            **API Response Time**:
         | 
| 315 | 
            -
            - JavaScript: ~100ms ( | 
| 316 | 
            -
            - Python: ~200-500ms ( | 
|  | |
|  | |
|  | |
|  | |
| 317 |  | 
| 318 | 
             
            ## 🔄 Migration Strategy
         | 
| 319 |  | 
| @@ -325,8 +498,10 @@ pytest tests/ --cov=src --cov-report=html | |
| 325 |  | 
| 326 | 
             
            ## 🎯 Next Steps
         | 
| 327 |  | 
| 328 | 
            -
            - [ | 
| 329 | 
            -
            - [ | 
|  | |
|  | |
| 330 | 
             
            - [ ] Add more sophisticated crossword grid generation
         | 
| 331 | 
             
            - [ ] Implement LLM-based clue generation
         | 
| 332 | 
            -
            - [ ] Add  | 
|  | |
| 1 | 
            +
            # Python Backend with Thematic AI Word Generation
         | 
| 2 |  | 
| 3 | 
            +
            This is the Python implementation of the crossword generator backend, featuring AI-powered thematic word generation using WordFreq vocabulary and semantic embeddings.
         | 
| 4 |  | 
| 5 | 
             
            ## 🚀 Features
         | 
| 6 |  | 
| 7 | 
            +
            - **Thematic Word Generation**: Uses sentence-transformers for semantic word discovery from WordFreq vocabulary
         | 
| 8 | 
            +
            - **319K+ Word Database**: Comprehensive vocabulary from WordFreq with frequency data
         | 
| 9 | 
            +
            - **10-Tier Difficulty System**: Smart word selection based on frequency tiers
         | 
| 10 | 
            +
            - **Environment Variable Configuration**: Flexible cache and model configuration
         | 
| 11 | 
             
            - **FastAPI**: Modern, fast Python web framework
         | 
| 12 | 
             
            - **Same API**: Compatible with existing React frontend
         | 
|  | |
| 13 |  | 
| 14 | 
             
            ## 🔄 Differences from JavaScript Backend
         | 
| 15 |  | 
| 16 | 
             
            | Feature | JavaScript Backend | Python Backend |
         | 
| 17 | 
             
            |---------|-------------------|----------------|
         | 
| 18 | 
            +
            | **Word Generation** | Static word lists | Thematic AI word generation from 319K vocabulary |
         | 
| 19 | 
            +
            | **Vocabulary Size** | ~100 words per topic | Filtered from 319K WordFreq database |
         | 
| 20 | 
            +
            | **AI Approach** | Basic filtering | Semantic similarity with frequency tiers |
         | 
| 21 | 
            +
            | **Performance** | Fast but limited | Slower startup, richer word selection |
         | 
| 22 | 
            +
            | **Dependencies** | Node.js + static files | Python + ML libraries |
         | 
| 23 |  | 
| 24 | 
             
            ## 🛠️ Setup & Installation
         | 
| 25 |  | 
|  | |
| 71 | 
             
            ├── requirements-dev.txt            # Full development dependencies
         | 
| 72 | 
             
            ├── src/
         | 
| 73 | 
             
            │   ├── services/
         | 
| 74 | 
            +
            │   │   ├── thematic_word_service.py    # Thematic AI word generation
         | 
| 75 | 
            +
            │   │   ├── crossword_generator.py      # Puzzle generation logic
         | 
| 76 | 
            +
            │   │   └── crossword_generator_wrapper.py  # Service wrapper
         | 
| 77 | 
             
            │   └── routes/
         | 
| 78 | 
            +
            │       └── api.py                      # API endpoints (matches JS backend)
         | 
| 79 | 
             
            ├── test-unit/                      # Unit tests (pytest framework) - 5 files
         | 
| 80 | 
             
            │   ├── test_crossword_generator.py
         | 
| 81 | 
             
            │   ├── test_api_routes.py
         | 
|  | |
| 92 |  | 
| 93 | 
             
            ### Core ML Stack
         | 
| 94 | 
             
            - `sentence-transformers`: Local model loading and embeddings
         | 
| 95 | 
            +
            - `wordfreq`: 319K word vocabulary with frequency data
         | 
| 96 | 
             
            - `torch`: PyTorch for model inference
         | 
| 97 | 
            +
            - `scikit-learn`: Cosine similarity and clustering
         | 
| 98 | 
             
            - `numpy`: Vector operations
         | 
| 99 |  | 
| 100 | 
             
            ### Web Framework
         | 
|  | |
| 206 |  | 
| 207 | 
             
            ## 🔧 Configuration
         | 
| 208 |  | 
| 209 | 
            +
            ### Environment Variables
         | 
| 210 | 
            +
             | 
| 211 | 
            +
            The backend supports flexible configuration via environment variables:
         | 
| 212 |  | 
| 213 | 
             
            ```bash
         | 
| 214 | 
            +
            # Cache Configuration
         | 
| 215 | 
            +
            CACHE_DIR=/app/cache                        # Cache directory for all service files
         | 
| 216 | 
            +
            THEMATIC_VOCAB_SIZE_LIMIT=50000            # Maximum vocabulary size (default: 100000)
         | 
| 217 | 
            +
            THEMATIC_MODEL_NAME=all-mpnet-base-v2      # Sentence transformer model
         | 
| 218 |  | 
| 219 | 
            +
            # Core Application Settings  
         | 
| 220 | 
            +
            PORT=7860                                  # Server port
         | 
| 221 | 
            +
            NODE_ENV=production                        # Environment mode
         | 
| 222 |  | 
| 223 | 
             
            # Optional
         | 
| 224 | 
            +
            LOG_LEVEL=INFO                            # Logging level
         | 
| 225 | 
             
            ```
         | 
| 226 |  | 
| 227 | 
            +
            ### Cache Structure
         | 
| 228 | 
            +
             | 
| 229 | 
            +
            The service creates the following cache files:
         | 
| 230 | 
            +
             | 
| 231 | 
            +
            ```
         | 
| 232 | 
            +
            {CACHE_DIR}/
         | 
| 233 | 
            +
            ├── vocabulary_{size}.pkl              # Processed vocabulary words
         | 
| 234 | 
            +
            ├── frequencies_{size}.pkl             # Word frequency data
         | 
| 235 | 
            +
            ├── embeddings_{model}_{size}.npy      # Word embeddings
         | 
| 236 | 
            +
            └── sentence-transformers/             # Hugging Face model cache
         | 
| 237 | 
            +
            ```
         | 
| 238 | 
            +
             | 
| 239 | 
            +
            ## 🎯 Thematic Word Generation Process
         | 
| 240 |  | 
| 241 | 
             
            1. **Initialization**:
         | 
| 242 | 
            +
               - Load WordFreq vocabulary database (319K words)
         | 
| 243 | 
            +
               - Filter words for crossword suitability (length, content)  
         | 
| 244 | 
             
               - Load sentence-transformers model locally
         | 
| 245 | 
            +
               - Pre-compute embeddings for filtered vocabulary
         | 
| 246 | 
            +
               - Create 10-tier frequency classification system
         | 
|  | |
| 247 |  | 
| 248 | 
             
            2. **Word Generation**:
         | 
| 249 | 
             
               - Get topic embedding: `"Animals" → [768-dim vector]`
         | 
| 250 | 
            +
               - Compute cosine similarity with all vocabulary embeddings
         | 
| 251 | 
            +
               - Filter by similarity threshold and difficulty tier
         | 
| 252 | 
            +
               - Filter by crossword-specific criteria (length, etc.)
         | 
| 253 | 
             
               - Return top matches with generated clues
         | 
| 254 |  | 
| 255 | 
            +
            3. **Multi-Theme Support**:
         | 
| 256 | 
            +
               - Detect multiple themes using clustering
         | 
| 257 | 
            +
               - Generate words that relate to combined themes
         | 
| 258 | 
            +
               - Balance word selection across different topics
         | 
| 259 |  | 
| 260 | 
             
            ## 🧪 Testing
         | 
| 261 |  | 
|  | |
| 268 | 
             
            python app.py
         | 
| 269 | 
             
            ```
         | 
| 270 |  | 
| 271 | 
            +
            ## 🐳 Container Deployment
         | 
| 272 | 
            +
             | 
| 273 | 
            +
            ### Docker Run with Cache Configuration
         | 
| 274 | 
            +
             | 
| 275 | 
            +
            ```bash
         | 
| 276 | 
            +
            # Basic deployment
         | 
| 277 | 
            +
            docker run -e CACHE_DIR=/app/cache \
         | 
| 278 | 
            +
                       -e THEMATIC_VOCAB_SIZE_LIMIT=50000 \
         | 
| 279 | 
            +
                       -v /host/cache:/app/cache \
         | 
| 280 | 
            +
                       -p 7860:7860 \
         | 
| 281 | 
            +
                       your-crossword-app
         | 
| 282 | 
            +
             | 
| 283 | 
            +
            # With all configuration options
         | 
| 284 | 
            +
            docker run -e CACHE_DIR=/app/cache \
         | 
| 285 | 
            +
                       -e THEMATIC_VOCAB_SIZE_LIMIT=25000 \
         | 
| 286 | 
            +
                       -e THEMATIC_MODEL_NAME=all-mpnet-base-v2 \
         | 
| 287 | 
            +
                       -e NODE_ENV=production \
         | 
| 288 | 
            +
                       -v /host/cache:/app/cache \
         | 
| 289 | 
            +
                       -p 7860:7860 \
         | 
| 290 | 
            +
                       your-crossword-app
         | 
| 291 | 
            +
            ```
         | 
| 292 | 
            +
             | 
| 293 | 
            +
            ### Docker Compose
         | 
| 294 | 
            +
             | 
| 295 | 
            +
            ```yaml
         | 
| 296 | 
            +
            version: '3.8'
         | 
| 297 | 
            +
            services:
         | 
| 298 | 
            +
              crossword-backend:
         | 
| 299 | 
            +
                image: your-crossword-app
         | 
| 300 | 
            +
                environment:
         | 
| 301 | 
            +
                  - CACHE_DIR=/app/cache
         | 
| 302 | 
            +
                  - THEMATIC_VOCAB_SIZE_LIMIT=50000
         | 
| 303 | 
            +
                  - THEMATIC_MODEL_NAME=all-mpnet-base-v2
         | 
| 304 | 
            +
                  - NODE_ENV=production
         | 
| 305 | 
            +
                volumes:
         | 
| 306 | 
            +
                  - ./cache:/app/cache
         | 
| 307 | 
            +
                ports:
         | 
| 308 | 
            +
                  - "7860:7860"
         | 
| 309 | 
            +
                restart: unless-stopped
         | 
| 310 | 
            +
            ```
         | 
| 311 | 
            +
             | 
| 312 | 
            +
            ### Pre-built Cache Strategy (Recommended)
         | 
| 313 | 
            +
             | 
| 314 | 
            +
            For production deployments, pre-build the cache to avoid long startup times:
         | 
| 315 | 
            +
             | 
| 316 | 
            +
            ```bash
         | 
| 317 | 
            +
            # 1. Build cache locally or in a build container
         | 
| 318 | 
            +
            export CACHE_DIR=/local/cache
         | 
| 319 | 
            +
            export THEMATIC_VOCAB_SIZE_LIMIT=50000
         | 
| 320 | 
            +
            python -c "from src.services.thematic_word_service import ThematicWordService; s=ThematicWordService(); s.initialize()"
         | 
| 321 | 
            +
             | 
| 322 | 
            +
            # 2. Deploy with pre-built cache (read-only mount)
         | 
| 323 | 
            +
            docker run -e CACHE_DIR=/app/cache \
         | 
| 324 | 
            +
                       -v /local/cache:/app/cache:ro \
         | 
| 325 | 
            +
                       -p 7860:7860 \
         | 
| 326 | 
            +
                       your-crossword-app
         | 
| 327 | 
            +
            ```
         | 
| 328 | 
            +
             | 
| 329 | 
            +
            ### Debugging Cache Issues
         | 
| 330 |  | 
| 331 | 
            +
            If cache files are not being created in your container:
         | 
| 332 |  | 
| 333 | 
            +
            1. **Check Health Endpoints:**
         | 
| 334 | 
            +
            ```bash
         | 
| 335 | 
            +
            # Basic health check
         | 
| 336 | 
            +
            curl http://localhost:7860/api/health
         | 
| 337 | 
            +
             | 
| 338 | 
            +
            # Detailed cache status
         | 
| 339 | 
            +
            curl http://localhost:7860/api/health/cache
         | 
| 340 | 
            +
             | 
| 341 | 
            +
            # Force cache re-initialization
         | 
| 342 | 
            +
            curl -X POST http://localhost:7860/api/health/cache/reinitialize
         | 
| 343 | 
            +
            ```
         | 
| 344 | 
            +
             | 
| 345 | 
            +
            2. **Check Container Logs:**
         | 
| 346 | 
            +
            ```bash
         | 
| 347 | 
            +
            docker logs your-container-name
         | 
| 348 | 
            +
            ```
         | 
| 349 | 
            +
            Look for cache directory permissions and initialization messages.
         | 
| 350 | 
            +
             | 
| 351 | 
            +
            3. **Test Cache Directory:**
         | 
| 352 | 
            +
            ```bash
         | 
| 353 | 
            +
            # Run test script to verify cache setup
         | 
| 354 | 
            +
            docker exec your-container python test_cache_startup.py
         | 
| 355 | 
            +
            ```
         | 
| 356 | 
            +
             | 
| 357 | 
            +
            4. **Common Issues:**
         | 
| 358 | 
            +
               - **Permission denied**: Container user can't write to mounted volume
         | 
| 359 | 
            +
               - **Missing dependencies**: ML libraries not installed in container
         | 
| 360 | 
            +
               - **Volume not mounted**: Cache directory not properly mounted
         | 
| 361 | 
            +
               - **Environment variables**: `CACHE_DIR` not set correctly
         | 
| 362 | 
            +
             | 
| 363 | 
            +
            5. **Fix Permission Issues:**
         | 
| 364 | 
            +
            ```bash
         | 
| 365 | 
            +
            # Option 1: Change ownership of host directory
         | 
| 366 | 
            +
            sudo chown -R 1000:1000 /host/cache
         | 
| 367 | 
            +
             | 
| 368 | 
            +
            # Option 2: Run container with specific user
         | 
| 369 | 
            +
            docker run --user 1000:1000 ...
         | 
| 370 | 
            +
             | 
| 371 | 
            +
            # Option 3: Set permissions in Dockerfile
         | 
| 372 | 
            +
            RUN mkdir -p /app/cache && chmod 777 /app/cache
         | 
| 373 | 
            +
            ```
         | 
| 374 | 
            +
             | 
| 375 | 
            +
            ### Kubernetes Deployment
         | 
| 376 | 
            +
             | 
| 377 | 
            +
            ```yaml
         | 
| 378 | 
            +
            apiVersion: v1
         | 
| 379 | 
            +
            kind: ConfigMap
         | 
| 380 | 
            +
            metadata:
         | 
| 381 | 
            +
              name: crossword-config
         | 
| 382 | 
            +
            data:
         | 
| 383 | 
            +
              CACHE_DIR: "/app/cache"
         | 
| 384 | 
            +
              THEMATIC_VOCAB_SIZE_LIMIT: "50000"
         | 
| 385 | 
            +
              THEMATIC_MODEL_NAME: "all-mpnet-base-v2"
         | 
| 386 | 
            +
              NODE_ENV: "production"
         | 
| 387 | 
            +
            ---
         | 
| 388 | 
            +
            apiVersion: v1
         | 
| 389 | 
            +
            kind: PersistentVolumeClaim
         | 
| 390 | 
            +
            metadata:
         | 
| 391 | 
            +
              name: crossword-cache
         | 
| 392 | 
            +
            spec:
         | 
| 393 | 
            +
              accessModes:
         | 
| 394 | 
            +
                - ReadWriteOnce
         | 
| 395 | 
            +
              resources:
         | 
| 396 | 
            +
                requests:
         | 
| 397 | 
            +
                  storage: 5Gi
         | 
| 398 | 
            +
            ---
         | 
| 399 | 
            +
            apiVersion: apps/v1
         | 
| 400 | 
            +
            kind: Deployment
         | 
| 401 | 
            +
            metadata:
         | 
| 402 | 
            +
              name: crossword-backend
         | 
| 403 | 
            +
            spec:
         | 
| 404 | 
            +
              replicas: 1
         | 
| 405 | 
            +
              selector:
         | 
| 406 | 
            +
                matchLabels:
         | 
| 407 | 
            +
                  app: crossword-backend
         | 
| 408 | 
            +
              template:
         | 
| 409 | 
            +
                metadata:
         | 
| 410 | 
            +
                  labels:
         | 
| 411 | 
            +
                    app: crossword-backend
         | 
| 412 | 
            +
                spec:
         | 
| 413 | 
            +
                  containers:
         | 
| 414 | 
            +
                  - name: backend
         | 
| 415 | 
            +
                    image: your-crossword-app
         | 
| 416 | 
            +
                    envFrom:
         | 
| 417 | 
            +
                    - configMapRef:
         | 
| 418 | 
            +
                        name: crossword-config
         | 
| 419 | 
            +
                    volumeMounts:
         | 
| 420 | 
            +
                    - name: cache-volume
         | 
| 421 | 
            +
                      mountPath: /app/cache
         | 
| 422 | 
            +
                    ports:
         | 
| 423 | 
            +
                    - containerPort: 7860
         | 
| 424 | 
            +
                  volumes:
         | 
| 425 | 
            +
                  - name: cache-volume
         | 
| 426 | 
            +
                    persistentVolumeClaim:
         | 
| 427 | 
            +
                      claimName: crossword-cache
         | 
| 428 | 
             
            ```
         | 
| 429 |  | 
| 430 | 
             
            ## 🧪 Testing
         | 
|  | |
| 468 |  | 
| 469 | 
             
            **Startup Time**:
         | 
| 470 | 
             
            - JavaScript: ~2 seconds
         | 
| 471 | 
            +
            - Python: ~30-60 seconds (model download + embedding generation)
         | 
| 472 | 
            +
            - Python (with cache): ~5-10 seconds
         | 
| 473 |  | 
| 474 | 
             
            **Word Quality**:
         | 
| 475 | 
            +
            - JavaScript: Limited by static word lists (~100 words/topic)
         | 
| 476 | 
            +
            - Python: Rich thematic generation from 319K word database
         | 
| 477 |  | 
| 478 | 
             
            **Memory Usage**:
         | 
| 479 | 
             
            - JavaScript: ~100MB
         | 
| 480 | 
            +
            - Python: ~500MB-1GB (model + embeddings)
         | 
| 481 | 
            +
            - Cache Size: ~50-200MB per 50K vocabulary
         | 
| 482 |  | 
| 483 | 
             
            **API Response Time**:
         | 
| 484 | 
            +
            - JavaScript: ~100ms (static word lookup)
         | 
| 485 | 
            +
            - Python: ~200-500ms (semantic similarity computation)
         | 
| 486 | 
            +
             | 
| 487 | 
            +
            **Cache Performance**:
         | 
| 488 | 
            +
            - Vocabulary loading: ~1-2 seconds from cache vs 30+ seconds generation
         | 
| 489 | 
            +
            - Embeddings loading: ~2-5 seconds from cache vs 60+ seconds generation
         | 
| 490 |  | 
| 491 | 
             
            ## 🔄 Migration Strategy
         | 
| 492 |  | 
|  | |
| 498 |  | 
| 499 | 
             
            ## 🎯 Next Steps
         | 
| 500 |  | 
| 501 | 
            +
            - [x] Replace vector search with thematic word generation
         | 
| 502 | 
            +
            - [x] Implement environment variable cache configuration  
         | 
| 503 | 
            +
            - [x] Add 10-tier difficulty system based on word frequency
         | 
| 504 | 
            +
            - [ ] Optimize embedding computation performance
         | 
| 505 | 
             
            - [ ] Add more sophisticated crossword grid generation
         | 
| 506 | 
             
            - [ ] Implement LLM-based clue generation
         | 
| 507 | 
            +
            - [ ] Add cache warming strategies for production deployment
         | 
    	
        crossword-app/backend-py/all-packages.txt
    ADDED
    
    | @@ -0,0 +1,69 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            annotated-types==0.7.0
         | 
| 2 | 
            +
            anyio==4.10.0
         | 
| 3 | 
            +
            certifi==2025.8.3
         | 
| 4 | 
            +
            charset-normalizer==3.4.3
         | 
| 5 | 
            +
            click==8.2.1
         | 
| 6 | 
            +
            exceptiongroup==1.3.0
         | 
| 7 | 
            +
            faiss-cpu==1.9.0
         | 
| 8 | 
            +
            fastapi==0.115.0
         | 
| 9 | 
            +
            filelock==3.19.1
         | 
| 10 | 
            +
            fsspec==2025.7.0
         | 
| 11 | 
            +
            h11==0.16.0
         | 
| 12 | 
            +
            httpcore==1.0.9
         | 
| 13 | 
            +
            httptools==0.6.4
         | 
| 14 | 
            +
            httpx==0.28.1
         | 
| 15 | 
            +
            huggingface-hub==0.26.2
         | 
| 16 | 
            +
            idna==3.10
         | 
| 17 | 
            +
            iniconfig==2.1.0
         | 
| 18 | 
            +
            Jinja2==3.1.6
         | 
| 19 | 
            +
            joblib==1.5.1
         | 
| 20 | 
            +
            MarkupSafe==3.0.2
         | 
| 21 | 
            +
            mpmath==1.3.0
         | 
| 22 | 
            +
            networkx==3.4.2
         | 
| 23 | 
            +
            numpy==1.26.4
         | 
| 24 | 
            +
            nvidia-cublas-cu12==12.4.5.8
         | 
| 25 | 
            +
            nvidia-cuda-cupti-cu12==12.4.127
         | 
| 26 | 
            +
            nvidia-cuda-nvrtc-cu12==12.4.127
         | 
| 27 | 
            +
            nvidia-cuda-runtime-cu12==12.4.127
         | 
| 28 | 
            +
            nvidia-cudnn-cu12==9.1.0.70
         | 
| 29 | 
            +
            nvidia-cufft-cu12==11.2.1.3
         | 
| 30 | 
            +
            nvidia-curand-cu12==10.3.5.147
         | 
| 31 | 
            +
            nvidia-cusolver-cu12==11.6.1.9
         | 
| 32 | 
            +
            nvidia-cusparse-cu12==12.3.1.170
         | 
| 33 | 
            +
            nvidia-nccl-cu12==2.21.5
         | 
| 34 | 
            +
            nvidia-nvjitlink-cu12==12.4.127
         | 
| 35 | 
            +
            nvidia-nvtx-cu12==12.4.127
         | 
| 36 | 
            +
            packaging==25.0
         | 
| 37 | 
            +
            pillow==11.3.0
         | 
| 38 | 
            +
            pluggy==1.6.0
         | 
| 39 | 
            +
            pydantic==2.9.2
         | 
| 40 | 
            +
            pydantic-settings==2.5.2
         | 
| 41 | 
            +
            pydantic_core==2.23.4
         | 
| 42 | 
            +
            pytest==8.3.4
         | 
| 43 | 
            +
            pytest-asyncio==0.25.0
         | 
| 44 | 
            +
            python-dotenv==1.0.1
         | 
| 45 | 
            +
            python-multipart==0.0.12
         | 
| 46 | 
            +
            PyYAML==6.0.2
         | 
| 47 | 
            +
            regex==2025.7.34
         | 
| 48 | 
            +
            requests==2.32.4
         | 
| 49 | 
            +
            safetensors==0.6.2
         | 
| 50 | 
            +
            scikit-learn==1.5.2
         | 
| 51 | 
            +
            scipy==1.15.3
         | 
| 52 | 
            +
            sentence-transformers==3.3.0
         | 
| 53 | 
            +
            sniffio==1.3.1
         | 
| 54 | 
            +
            starlette==0.38.6
         | 
| 55 | 
            +
            structlog==24.4.0
         | 
| 56 | 
            +
            sympy==1.13.1
         | 
| 57 | 
            +
            threadpoolctl==3.6.0
         | 
| 58 | 
            +
            tokenizers==0.21.4
         | 
| 59 | 
            +
            tomli==2.2.1
         | 
| 60 | 
            +
            torch==2.5.1
         | 
| 61 | 
            +
            tqdm==4.67.1
         | 
| 62 | 
            +
            transformers==4.47.1
         | 
| 63 | 
            +
            triton==3.1.0
         | 
| 64 | 
            +
            typing_extensions==4.14.1
         | 
| 65 | 
            +
            urllib3==2.5.0
         | 
| 66 | 
            +
            uvicorn==0.32.1
         | 
| 67 | 
            +
            uvloop==0.21.0
         | 
| 68 | 
            +
            watchfiles==1.1.0
         | 
| 69 | 
            +
            websockets==15.0.1
         | 
    	
        crossword-app/backend-py/app.py
    CHANGED
    
    | @@ -17,61 +17,117 @@ import uvicorn | |
| 17 | 
             
            from dotenv import load_dotenv
         | 
| 18 |  | 
| 19 | 
             
            from src.routes.api import router as api_router
         | 
| 20 | 
            -
            from src.services. | 
| 21 |  | 
| 22 | 
             
            # Load environment variables
         | 
| 23 | 
             
            load_dotenv()
         | 
| 24 |  | 
| 25 | 
            -
            # Set up logging
         | 
| 26 | 
            -
            logging.basicConfig( | 
|  | |
|  | |
|  | |
|  | |
| 27 | 
             
            logger = logging.getLogger(__name__)
         | 
| 28 |  | 
| 29 | 
            -
             | 
| 30 | 
            -
                """Helper to log with precise timestamp."""
         | 
| 31 | 
            -
                timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
         | 
| 32 | 
            -
                logger.info(f"[{timestamp}] {message}")
         | 
| 33 |  | 
| 34 | 
            -
            # Global  | 
| 35 | 
            -
             | 
| 36 |  | 
| 37 | 
             
            @asynccontextmanager
         | 
| 38 | 
             
            async def lifespan(app: FastAPI):
         | 
| 39 | 
             
                """Initialize and cleanup application resources."""
         | 
| 40 | 
            -
                global  | 
| 41 |  | 
| 42 | 
             
                # Startup
         | 
| 43 | 
             
                startup_time = time.time()
         | 
| 44 | 
            -
                 | 
| 45 |  | 
| 46 | 
            -
                # Initialize  | 
| 47 | 
             
                try:
         | 
| 48 | 
             
                    service_start = time.time()
         | 
| 49 | 
            -
                     | 
| 50 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 51 |  | 
| 52 | 
            -
                     | 
| 53 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 54 |  | 
| 55 | 
             
                    init_time = time.time() - service_start
         | 
| 56 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 57 | 
             
                except Exception as e:
         | 
| 58 | 
            -
                    logger.error(f"❌ Failed to initialize  | 
| 59 | 
            -
                     | 
|  | |
|  | |
|  | |
| 60 |  | 
| 61 | 
            -
                # Make  | 
| 62 | 
            -
                app.state. | 
| 63 |  | 
| 64 | 
             
                yield
         | 
| 65 |  | 
| 66 | 
             
                # Shutdown
         | 
| 67 | 
             
                logger.info("🛑 Shutting down Python backend...")
         | 
| 68 | 
            -
                if  | 
| 69 | 
            -
                    await vector_service.cleanup()
         | 
| 70 |  | 
| 71 | 
             
            # Create FastAPI app
         | 
| 72 | 
             
            app = FastAPI(
         | 
| 73 | 
             
                title="Crossword Puzzle Generator API",
         | 
| 74 | 
            -
                description="Python backend with AI-powered  | 
| 75 | 
             
                version="2.0.0",
         | 
| 76 | 
             
                lifespan=lifespan
         | 
| 77 | 
             
            )
         | 
|  | |
| 17 | 
             
            from dotenv import load_dotenv
         | 
| 18 |  | 
| 19 | 
             
            from src.routes.api import router as api_router
         | 
| 20 | 
            +
            from src.services.thematic_word_service import ThematicWordService
         | 
| 21 |  | 
| 22 | 
             
            # Load environment variables
         | 
| 23 | 
             
            load_dotenv()
         | 
| 24 |  | 
| 25 | 
            +
            # Set up logging with filename and line numbers
         | 
| 26 | 
            +
            logging.basicConfig(
         | 
| 27 | 
            +
                level=logging.INFO,
         | 
| 28 | 
            +
                format='%(asctime)s - %(name)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s',
         | 
| 29 | 
            +
                datefmt='%H:%M:%S'
         | 
| 30 | 
            +
            )
         | 
| 31 | 
             
            logger = logging.getLogger(__name__)
         | 
| 32 |  | 
| 33 | 
            +
            # All services now use standard Python logging with filename/line numbers
         | 
|  | |
|  | |
|  | |
| 34 |  | 
| 35 | 
            +
            # Global thematic service instance
         | 
| 36 | 
            +
            thematic_service = None
         | 
| 37 |  | 
| 38 | 
             
            @asynccontextmanager
         | 
| 39 | 
             
            async def lifespan(app: FastAPI):
         | 
| 40 | 
             
                """Initialize and cleanup application resources."""
         | 
| 41 | 
            +
                global thematic_service
         | 
| 42 |  | 
| 43 | 
             
                # Startup
         | 
| 44 | 
             
                startup_time = time.time()
         | 
| 45 | 
            +
                logger.info("🚀 Initializing Python backend with thematic word service...")
         | 
| 46 |  | 
| 47 | 
            +
                # Initialize thematic service
         | 
| 48 | 
             
                try:
         | 
| 49 | 
             
                    service_start = time.time()
         | 
| 50 | 
            +
                    logger.info("🔧 Creating ThematicWordService instance...")
         | 
| 51 | 
            +
                    thematic_service = ThematicWordService()
         | 
| 52 | 
            +
                    
         | 
| 53 | 
            +
                    # Log cache configuration for debugging
         | 
| 54 | 
            +
                    cache_status = thematic_service.get_cache_status()
         | 
| 55 | 
            +
                    logger.info(f"📁 Cache directory: {cache_status['cache_directory']}")
         | 
| 56 | 
            +
                    logger.info(f"🔍 Cache directory exists: {os.path.exists(cache_status['cache_directory'])}")
         | 
| 57 | 
            +
                    logger.info(f"✏️ Cache directory writable: {os.access(cache_status['cache_directory'], os.W_OK)}")
         | 
| 58 | 
            +
                    
         | 
| 59 | 
            +
                    # Check for existing cache files
         | 
| 60 | 
            +
                    cache_complete = cache_status['complete']
         | 
| 61 | 
            +
                    logger.info(f"📦 Existing cache complete: {cache_complete}")
         | 
| 62 | 
            +
                    if not cache_complete:
         | 
| 63 | 
            +
                        for cache_type in ['vocabulary_cache', 'frequency_cache', 'embeddings_cache']:
         | 
| 64 | 
            +
                            cache_info = cache_status[cache_type]
         | 
| 65 | 
            +
                            logger.info(f"  {cache_type}: exists={cache_info['exists']}, path={cache_info['path']}")
         | 
| 66 |  | 
| 67 | 
            +
                    # Force eager initialization to create cache files
         | 
| 68 | 
            +
                    logger.info("⚡ Starting thematic service initialization (creating cache files)...")
         | 
| 69 | 
            +
                    await thematic_service.initialize_async()
         | 
| 70 | 
            +
                    
         | 
| 71 | 
            +
                    # Verify cache files were created
         | 
| 72 | 
            +
                    cache_status_after = thematic_service.get_cache_status()
         | 
| 73 | 
            +
                    logger.info(f"✅ Cache status after initialization: complete={cache_status_after['complete']}")
         | 
| 74 | 
            +
                    for cache_type in ['vocabulary_cache', 'frequency_cache', 'embeddings_cache']:
         | 
| 75 | 
            +
                        cache_info = cache_status_after[cache_type]
         | 
| 76 | 
            +
                        if cache_info['exists']:
         | 
| 77 | 
            +
                            logger.info(f"  ✅ {cache_type}: {cache_info.get('size_mb', 0):.1f}MB")
         | 
| 78 | 
            +
                        else:
         | 
| 79 | 
            +
                            logger.warning(f"  ❌ {cache_type}: NOT CREATED")
         | 
| 80 |  | 
| 81 | 
             
                    init_time = time.time() - service_start
         | 
| 82 | 
            +
                    logger.info(f"🎉 Thematic service initialized in {init_time:.2f}s")
         | 
| 83 | 
            +
                    
         | 
| 84 | 
            +
                    # Initialize WordNet clue generator during startup
         | 
| 85 | 
            +
                    logger.info("🔧 Initializing WordNet clue generator...")
         | 
| 86 | 
            +
                    try:
         | 
| 87 | 
            +
                        wordnet_start = time.time()
         | 
| 88 | 
            +
                        from src.services.wordnet_clue_generator import WordNetClueGenerator
         | 
| 89 | 
            +
                        cache_dir = thematic_service.cache_dir if thematic_service else "./cache"
         | 
| 90 | 
            +
                        wordnet_generator = WordNetClueGenerator(cache_dir=str(cache_dir))
         | 
| 91 | 
            +
                        wordnet_generator.initialize()
         | 
| 92 | 
            +
                        
         | 
| 93 | 
            +
                        # Store in thematic service for later use
         | 
| 94 | 
            +
                        if thematic_service:
         | 
| 95 | 
            +
                            thematic_service._wordnet_generator = wordnet_generator
         | 
| 96 | 
            +
                            
         | 
| 97 | 
            +
                        wordnet_time = time.time() - wordnet_start
         | 
| 98 | 
            +
                        logger.info(f"✅ WordNet clue generator initialized in {wordnet_time:.2f}s")
         | 
| 99 | 
            +
                    except Exception as e:
         | 
| 100 | 
            +
                        logger.warning(f"⚠️ Failed to initialize WordNet clue generator during startup: {e}")
         | 
| 101 | 
            +
                        logger.info("📝 WordNet clue generator will be initialized on first use")
         | 
| 102 | 
            +
                        
         | 
| 103 | 
            +
                except ImportError as e:
         | 
| 104 | 
            +
                    logger.error(f"❌ Missing dependencies for thematic service: {e}")
         | 
| 105 | 
            +
                    logger.error("💡 Install missing packages: pip install wordfreq sentence-transformers torch scikit-learn")
         | 
| 106 | 
            +
                    raise  # Fail fast on missing dependencies
         | 
| 107 | 
            +
                except PermissionError as e:
         | 
| 108 | 
            +
                    logger.error(f"❌ Permission error with cache directory: {e}")
         | 
| 109 | 
            +
                    logger.error(f"💡 Check cache directory permissions: {thematic_service.cache_dir if 'thematic_service' in locals() else 'unknown'}")
         | 
| 110 | 
            +
                    raise  # Fail fast on permission issues
         | 
| 111 | 
             
                except Exception as e:
         | 
| 112 | 
            +
                    logger.error(f"❌ Failed to initialize thematic service: {e}")
         | 
| 113 | 
            +
                    logger.error(f"🔍 Error type: {type(e).__name__}")
         | 
| 114 | 
            +
                    import traceback
         | 
| 115 | 
            +
                    logger.error(f"📋 Full traceback: {traceback.format_exc()}")
         | 
| 116 | 
            +
                    raise  # Fail fast instead of continuing without service
         | 
| 117 |  | 
| 118 | 
            +
                # Make thematic service available to routes
         | 
| 119 | 
            +
                app.state.thematic_service = thematic_service
         | 
| 120 |  | 
| 121 | 
             
                yield
         | 
| 122 |  | 
| 123 | 
             
                # Shutdown
         | 
| 124 | 
             
                logger.info("🛑 Shutting down Python backend...")
         | 
| 125 | 
            +
                # Thematic service doesn't need cleanup, but we can add it if needed in the future
         | 
|  | |
| 126 |  | 
| 127 | 
             
            # Create FastAPI app
         | 
| 128 | 
             
            app = FastAPI(
         | 
| 129 | 
             
                title="Crossword Puzzle Generator API",
         | 
| 130 | 
            +
                description="Python backend with AI-powered thematic word generation",
         | 
| 131 | 
             
                version="2.0.0",
         | 
| 132 | 
             
                lifespan=lifespan
         | 
| 133 | 
             
            )
         | 
    	
        crossword-app/backend-py/data/data
    DELETED
    
    | @@ -1 +0,0 @@ | |
| 1 | 
            -
            ../backend/data
         | 
|  | |
|  | 
    	
        crossword-app/backend-py/data/word-lists/animals.json
    DELETED
    
    | @@ -1,165 +0,0 @@ | |
| 1 | 
            -
            [
         | 
| 2 | 
            -
              { "word": "DOG", "clue": "Man's best friend" },
         | 
| 3 | 
            -
              { "word": "CAT", "clue": "Feline pet that purrs" },
         | 
| 4 | 
            -
              { "word": "ELEPHANT", "clue": "Large mammal with a trunk" },
         | 
| 5 | 
            -
              { "word": "TIGER", "clue": "Striped big cat" },
         | 
| 6 | 
            -
              { "word": "WHALE", "clue": "Largest marine mammal" },
         | 
| 7 | 
            -
              { "word": "BUTTERFLY", "clue": "Colorful flying insect" },
         | 
| 8 | 
            -
              { "word": "BIRD", "clue": "Flying creature with feathers" },
         | 
| 9 | 
            -
              { "word": "FISH", "clue": "Aquatic animal with gills" },
         | 
| 10 | 
            -
              { "word": "LION", "clue": "King of the jungle" },
         | 
| 11 | 
            -
              { "word": "BEAR", "clue": "Large mammal that hibernates" },
         | 
| 12 | 
            -
              { "word": "RABBIT", "clue": "Hopping mammal with long ears" },
         | 
| 13 | 
            -
              { "word": "HORSE", "clue": "Riding animal with hooves" },
         | 
| 14 | 
            -
              { "word": "SHEEP", "clue": "Woolly farm animal" },
         | 
| 15 | 
            -
              { "word": "GOAT", "clue": "Horned farm animal" },
         | 
| 16 | 
            -
              { "word": "DUCK", "clue": "Water bird that quacks" },
         | 
| 17 | 
            -
              { "word": "CHICKEN", "clue": "Farm bird that lays eggs" },
         | 
| 18 | 
            -
              { "word": "SNAKE", "clue": "Slithering reptile" },
         | 
| 19 | 
            -
              { "word": "TURTLE", "clue": "Shelled reptile" },
         | 
| 20 | 
            -
              { "word": "FROG", "clue": "Amphibian that croaks" },
         | 
| 21 | 
            -
              { "word": "SHARK", "clue": "Predatory ocean fish" },
         | 
| 22 | 
            -
              { "word": "DOLPHIN", "clue": "Intelligent marine mammal" },
         | 
| 23 | 
            -
              { "word": "PENGUIN", "clue": "Flightless Antarctic bird" },
         | 
| 24 | 
            -
              { "word": "MONKEY", "clue": "Primate that swings in trees" },
         | 
| 25 | 
            -
              { "word": "ZEBRA", "clue": "Striped African animal" },
         | 
| 26 | 
            -
              { "word": "GIRAFFE", "clue": "Tallest land animal" },
         | 
| 27 | 
            -
              { "word": "WOLF", "clue": "Wild canine that howls" },
         | 
| 28 | 
            -
              { "word": "FOX", "clue": "Cunning red-furred animal" },
         | 
| 29 | 
            -
              { "word": "DEER", "clue": "Graceful forest animal with antlers" },
         | 
| 30 | 
            -
              { "word": "MOOSE", "clue": "Large antlered animal" },
         | 
| 31 | 
            -
              { "word": "SQUIRREL", "clue": "Tree-climbing nut gatherer" },
         | 
| 32 | 
            -
              { "word": "RACCOON", "clue": "Masked nocturnal animal" },
         | 
| 33 | 
            -
              { "word": "BEAVER", "clue": "Dam-building rodent" },
         | 
| 34 | 
            -
              { "word": "OTTER", "clue": "Playful water mammal" },
         | 
| 35 | 
            -
              { "word": "SEAL", "clue": "Marine mammal with flippers" },
         | 
| 36 | 
            -
              { "word": "WALRUS", "clue": "Tusked Arctic marine mammal" },
         | 
| 37 | 
            -
              { "word": "RHINO", "clue": "Horned thick-skinned mammal" },
         | 
| 38 | 
            -
              { "word": "HIPPO", "clue": "Large African river mammal" },
         | 
| 39 | 
            -
              { "word": "CHEETAH", "clue": "Fastest land animal" },
         | 
| 40 | 
            -
              { "word": "LEOPARD", "clue": "Spotted big cat" },
         | 
| 41 | 
            -
              { "word": "JAGUAR", "clue": "South American big cat" },
         | 
| 42 | 
            -
              { "word": "PUMA", "clue": "Mountain lion" },
         | 
| 43 | 
            -
              { "word": "LYNX", "clue": "Wild cat with tufted ears" },
         | 
| 44 | 
            -
              { "word": "KANGAROO", "clue": "Hopping Australian marsupial" },
         | 
| 45 | 
            -
              { "word": "KOALA", "clue": "Eucalyptus-eating marsupial" },
         | 
| 46 | 
            -
              { "word": "PANDA", "clue": "Black and white bamboo eater" },
         | 
| 47 | 
            -
              { "word": "SLOTH", "clue": "Slow-moving tree dweller" },
         | 
| 48 | 
            -
              { "word": "ARMADILLO", "clue": "Armored mammal" },
         | 
| 49 | 
            -
              { "word": "ANTEATER", "clue": "Long-snouted insect eater" },
         | 
| 50 | 
            -
              { "word": "PLATYPUS", "clue": "Egg-laying mammal with a bill" },
         | 
| 51 | 
            -
              { "word": "BAT", "clue": "Flying mammal" },
         | 
| 52 | 
            -
              { "word": "MOLE", "clue": "Underground tunnel digger" },
         | 
| 53 | 
            -
              { "word": "HEDGEHOG", "clue": "Spiny small mammal" },
         | 
| 54 | 
            -
              { "word": "PORCUPINE", "clue": "Quill-covered rodent" },
         | 
| 55 | 
            -
              { "word": "SKUNK", "clue": "Black and white scent-spraying mammal" },
         | 
| 56 | 
            -
              { "word": "WEASEL", "clue": "Small carnivorous mammal" },
         | 
| 57 | 
            -
              { "word": "BADGER", "clue": "Burrowing black and white mammal" },
         | 
| 58 | 
            -
              { "word": "FERRET", "clue": "Domesticated hunting animal" },
         | 
| 59 | 
            -
              { "word": "MINK", "clue": "Valuable fur-bearing animal" },
         | 
| 60 | 
            -
              { "word": "EAGLE", "clue": "Majestic bird of prey" },
         | 
| 61 | 
            -
              { "word": "HAWK", "clue": "Sharp-eyed hunting bird" },
         | 
| 62 | 
            -
              { "word": "OWL", "clue": "Nocturnal bird with large eyes" },
         | 
| 63 | 
            -
              { "word": "FALCON", "clue": "Fast diving bird of prey" },
         | 
| 64 | 
            -
              { "word": "VULTURE", "clue": "Scavenging bird" },
         | 
| 65 | 
            -
              { "word": "CROW", "clue": "Black intelligent bird" },
         | 
| 66 | 
            -
              { "word": "RAVEN", "clue": "Large black corvid" },
         | 
| 67 | 
            -
              { "word": "ROBIN", "clue": "Red-breasted songbird" },
         | 
| 68 | 
            -
              { "word": "SPARROW", "clue": "Small brown songbird" },
         | 
| 69 | 
            -
              { "word": "CARDINAL", "clue": "Bright red songbird" },
         | 
| 70 | 
            -
              { "word": "BLUEJAY", "clue": "Blue crested bird" },
         | 
| 71 | 
            -
              { "word": "WOODPECKER", "clue": "Tree-pecking bird" },
         | 
| 72 | 
            -
              { "word": "HUMMINGBIRD", "clue": "Tiny fast-flying bird" },
         | 
| 73 | 
            -
              { "word": "PELICAN", "clue": "Large-billed water bird" },
         | 
| 74 | 
            -
              { "word": "FLAMINGO", "clue": "Pink wading bird" },
         | 
| 75 | 
            -
              { "word": "STORK", "clue": "Long-legged wading bird" },
         | 
| 76 | 
            -
              { "word": "HERON", "clue": "Tall fishing bird" },
         | 
| 77 | 
            -
              { "word": "CRANE", "clue": "Large wading bird" },
         | 
| 78 | 
            -
              { "word": "SWAN", "clue": "Elegant white water bird" },
         | 
| 79 | 
            -
              { "word": "GOOSE", "clue": "Large waterfowl" },
         | 
| 80 | 
            -
              { "word": "TURKEY", "clue": "Large ground bird" },
         | 
| 81 | 
            -
              { "word": "PHEASANT", "clue": "Colorful game bird" },
         | 
| 82 | 
            -
              { "word": "QUAIL", "clue": "Small ground bird" },
         | 
| 83 | 
            -
              { "word": "PEACOCK", "clue": "Bird with spectacular tail feathers" },
         | 
| 84 | 
            -
              { "word": "OSTRICH", "clue": "Largest flightless bird" },
         | 
| 85 | 
            -
              { "word": "EMU", "clue": "Australian flightless bird" },
         | 
| 86 | 
            -
              { "word": "KIWI", "clue": "Small flightless New Zealand bird" },
         | 
| 87 | 
            -
              { "word": "PARROT", "clue": "Colorful talking bird" },
         | 
| 88 | 
            -
              { "word": "TOUCAN", "clue": "Large-billed tropical bird" },
         | 
| 89 | 
            -
              { "word": "MACAW", "clue": "Large colorful parrot" },
         | 
| 90 | 
            -
              { "word": "COCKATOO", "clue": "Crested parrot" },
         | 
| 91 | 
            -
              { "word": "CANARY", "clue": "Yellow singing bird" },
         | 
| 92 | 
            -
              { "word": "FINCH", "clue": "Small seed-eating bird" },
         | 
| 93 | 
            -
              { "word": "PIGEON", "clue": "Common city bird" },
         | 
| 94 | 
            -
              { "word": "DOVE", "clue": "Symbol of peace" },
         | 
| 95 | 
            -
              { "word": "SEAGULL", "clue": "Coastal scavenging bird" },
         | 
| 96 | 
            -
              { "word": "ALBATROSS", "clue": "Large ocean bird" },
         | 
| 97 | 
            -
              { "word": "PUFFIN", "clue": "Colorful-billed seabird" },
         | 
| 98 | 
            -
              { "word": "LIZARD", "clue": "Small scaly reptile" },
         | 
| 99 | 
            -
              { "word": "IGUANA", "clue": "Large tropical lizard" },
         | 
| 100 | 
            -
              { "word": "GECKO", "clue": "Wall-climbing lizard" },
         | 
| 101 | 
            -
              { "word": "CHAMELEON", "clue": "Color-changing reptile" },
         | 
| 102 | 
            -
              { "word": "ALLIGATOR", "clue": "Large American crocodilian" },
         | 
| 103 | 
            -
              { "word": "CROCODILE", "clue": "Large aquatic reptile" },
         | 
| 104 | 
            -
              { "word": "PYTHON", "clue": "Large constricting snake" },
         | 
| 105 | 
            -
              { "word": "COBRA", "clue": "Venomous hooded snake" },
         | 
| 106 | 
            -
              { "word": "VIPER", "clue": "Poisonous snake" },
         | 
| 107 | 
            -
              { "word": "RATTLESNAKE", "clue": "Snake with warning tail" },
         | 
| 108 | 
            -
              { "word": "SALAMANDER", "clue": "Amphibian that can regrow limbs" },
         | 
| 109 | 
            -
              { "word": "NEWT", "clue": "Small aquatic salamander" },
         | 
| 110 | 
            -
              { "word": "TOAD", "clue": "Warty amphibian" },
         | 
| 111 | 
            -
              { "word": "TADPOLE", "clue": "Frog larva" },
         | 
| 112 | 
            -
              { "word": "SALMON", "clue": "Fish that swims upstream" },
         | 
| 113 | 
            -
              { "word": "TROUT", "clue": "Freshwater game fish" },
         | 
| 114 | 
            -
              { "word": "BASS", "clue": "Popular sport fish" },
         | 
| 115 | 
            -
              { "word": "TUNA", "clue": "Large ocean fish" },
         | 
| 116 | 
            -
              { "word": "SWORDFISH", "clue": "Fish with long pointed bill" },
         | 
| 117 | 
            -
              { "word": "MARLIN", "clue": "Large billfish" },
         | 
| 118 | 
            -
              { "word": "MANTA", "clue": "Large ray fish" },
         | 
| 119 | 
            -
              { "word": "STINGRAY", "clue": "Flat fish with barbed tail" },
         | 
| 120 | 
            -
              { "word": "EEL", "clue": "Snake-like fish" },
         | 
| 121 | 
            -
              { "word": "SEAHORSE", "clue": "Horse-shaped fish" },
         | 
| 122 | 
            -
              { "word": "ANGELFISH", "clue": "Colorful tropical fish" },
         | 
| 123 | 
            -
              { "word": "GOLDFISH", "clue": "Common pet fish" },
         | 
| 124 | 
            -
              { "word": "CLOWNFISH", "clue": "Orange and white anemone fish" },
         | 
| 125 | 
            -
              { "word": "JELLYFISH", "clue": "Transparent stinging sea creature" },
         | 
| 126 | 
            -
              { "word": "OCTOPUS", "clue": "Eight-armed sea creature" },
         | 
| 127 | 
            -
              { "word": "SQUID", "clue": "Ten-armed cephalopod" },
         | 
| 128 | 
            -
              { "word": "CRAB", "clue": "Sideways-walking crustacean" },
         | 
| 129 | 
            -
              { "word": "LOBSTER", "clue": "Large marine crustacean" },
         | 
| 130 | 
            -
              { "word": "SHRIMP", "clue": "Small crustacean" },
         | 
| 131 | 
            -
              { "word": "STARFISH", "clue": "Five-armed sea creature" },
         | 
| 132 | 
            -
              { "word": "URCHIN", "clue": "Spiny sea creature" },
         | 
| 133 | 
            -
              { "word": "CORAL", "clue": "Marine organism that builds reefs" },
         | 
| 134 | 
            -
              { "word": "SPONGE", "clue": "Simple marine animal" },
         | 
| 135 | 
            -
              { "word": "OYSTER", "clue": "Pearl-producing mollusk" },
         | 
| 136 | 
            -
              { "word": "CLAM", "clue": "Burrowing shellfish" },
         | 
| 137 | 
            -
              { "word": "MUSSEL", "clue": "Dark-shelled mollusk" },
         | 
| 138 | 
            -
              { "word": "SNAIL", "clue": "Spiral-shelled gastropod" },
         | 
| 139 | 
            -
              { "word": "SLUG", "clue": "Shell-less gastropod" },
         | 
| 140 | 
            -
              { "word": "WORM", "clue": "Segmented invertebrate" },
         | 
| 141 | 
            -
              { "word": "SPIDER", "clue": "Eight-legged web spinner" },
         | 
| 142 | 
            -
              { "word": "SCORPION", "clue": "Arachnid with stinging tail" },
         | 
| 143 | 
            -
              { "word": "ANT", "clue": "Social insect worker" },
         | 
| 144 | 
            -
              { "word": "BEE", "clue": "Honey-making insect" },
         | 
| 145 | 
            -
              { "word": "WASP", "clue": "Stinging flying insect" },
         | 
| 146 | 
            -
              { "word": "HORNET", "clue": "Large aggressive wasp" },
         | 
| 147 | 
            -
              { "word": "FLY", "clue": "Common buzzing insect" },
         | 
| 148 | 
            -
              { "word": "MOSQUITO", "clue": "Blood-sucking insect" },
         | 
| 149 | 
            -
              { "word": "BEETLE", "clue": "Hard-shelled insect" },
         | 
| 150 | 
            -
              { "word": "LADYBUG", "clue": "Red spotted beneficial insect" },
         | 
| 151 | 
            -
              { "word": "DRAGONFLY", "clue": "Large-winged flying insect" },
         | 
| 152 | 
            -
              { "word": "GRASSHOPPER", "clue": "Jumping green insect" },
         | 
| 153 | 
            -
              { "word": "CRICKET", "clue": "Chirping insect" },
         | 
| 154 | 
            -
              { "word": "MANTIS", "clue": "Praying insect predator" },
         | 
| 155 | 
            -
              { "word": "MOTH", "clue": "Nocturnal butterfly relative" },
         | 
| 156 | 
            -
              { "word": "CATERPILLAR", "clue": "Butterfly larva" },
         | 
| 157 | 
            -
              { "word": "COCOON", "clue": "Insect transformation casing" },
         | 
| 158 | 
            -
              { "word": "TERMITE", "clue": "Wood-eating social insect" },
         | 
| 159 | 
            -
              { "word": "TICK", "clue": "Blood-sucking parasite" },
         | 
| 160 | 
            -
              { "word": "FLEA", "clue": "Jumping parasite" },
         | 
| 161 | 
            -
              { "word": "LOUSE", "clue": "Small parasitic insect" },
         | 
| 162 | 
            -
              { "word": "APHID", "clue": "Plant-sucking insect" },
         | 
| 163 | 
            -
              { "word": "MAGGOT", "clue": "Fly larva" },
         | 
| 164 | 
            -
              { "word": "GRUB", "clue": "Beetle larva" }
         | 
| 165 | 
            -
            ]
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/data/word-lists/geography.json
    DELETED
    
    | @@ -1,161 +0,0 @@ | |
| 1 | 
            -
            [
         | 
| 2 | 
            -
              { "word": "MOUNTAIN", "clue": "High elevation landform" },
         | 
| 3 | 
            -
              { "word": "OCEAN", "clue": "Large body of salt water" },
         | 
| 4 | 
            -
              { "word": "DESERT", "clue": "Dry, arid region" },
         | 
| 5 | 
            -
              { "word": "CONTINENT", "clue": "Large landmass" },
         | 
| 6 | 
            -
              { "word": "RIVER", "clue": "Flowing body of water" },
         | 
| 7 | 
            -
              { "word": "ISLAND", "clue": "Land surrounded by water" },
         | 
| 8 | 
            -
              { "word": "FOREST", "clue": "Dense area of trees" },
         | 
| 9 | 
            -
              { "word": "VALLEY", "clue": "Low area between hills" },
         | 
| 10 | 
            -
              { "word": "LAKE", "clue": "Body of freshwater" },
         | 
| 11 | 
            -
              { "word": "BEACH", "clue": "Sandy shore by water" },
         | 
| 12 | 
            -
              { "word": "CLIFF", "clue": "Steep rock face" },
         | 
| 13 | 
            -
              { "word": "PLATEAU", "clue": "Elevated flat area" },
         | 
| 14 | 
            -
              { "word": "CANYON", "clue": "Deep gorge with steep sides" },
         | 
| 15 | 
            -
              { "word": "GLACIER", "clue": "Moving mass of ice" },
         | 
| 16 | 
            -
              { "word": "VOLCANO", "clue": "Mountain that erupts" },
         | 
| 17 | 
            -
              { "word": "PENINSULA", "clue": "Land surrounded by water on three sides" },
         | 
| 18 | 
            -
              { "word": "ARCHIPELAGO", "clue": "Group of islands" },
         | 
| 19 | 
            -
              { "word": "PRAIRIE", "clue": "Grassland plain" },
         | 
| 20 | 
            -
              { "word": "TUNDRA", "clue": "Cold, treeless region" },
         | 
| 21 | 
            -
              { "word": "SAVANNA", "clue": "Tropical grassland" },
         | 
| 22 | 
            -
              { "word": "EQUATOR", "clue": "Earth's middle line" },
         | 
| 23 | 
            -
              { "word": "LATITUDE", "clue": "Distance from equator" },
         | 
| 24 | 
            -
              { "word": "LONGITUDE", "clue": "Distance from prime meridian" },
         | 
| 25 | 
            -
              { "word": "CLIMATE", "clue": "Long-term weather pattern" },
         | 
| 26 | 
            -
              { "word": "MONSOON", "clue": "Seasonal wind pattern" },
         | 
| 27 | 
            -
              { "word": "CAPITAL", "clue": "Main city of country" },
         | 
| 28 | 
            -
              { "word": "BORDER", "clue": "Boundary between countries" },
         | 
| 29 | 
            -
              { "word": "COAST", "clue": "Land meeting the sea" },
         | 
| 30 | 
            -
              { "word": "STRAIT", "clue": "Narrow water passage" },
         | 
| 31 | 
            -
              { "word": "DELTA", "clue": "River mouth formation" },
         | 
| 32 | 
            -
              { "word": "FJORD", "clue": "Narrow inlet between cliffs" },
         | 
| 33 | 
            -
              { "word": "ATOLL", "clue": "Ring-shaped coral island" },
         | 
| 34 | 
            -
              { "word": "MESA", "clue": "Flat-topped hill" },
         | 
| 35 | 
            -
              { "word": "BUTTE", "clue": "Isolated hill with steep sides" },
         | 
| 36 | 
            -
              { "word": "GORGE", "clue": "Deep narrow valley" },
         | 
| 37 | 
            -
              { "word": "RAVINE", "clue": "Small narrow gorge" },
         | 
| 38 | 
            -
              { "word": "RIDGE", "clue": "Long narrow hilltop" },
         | 
| 39 | 
            -
              { "word": "PEAK", "clue": "Mountain summit" },
         | 
| 40 | 
            -
              { "word": "SUMMIT", "clue": "Highest point" },
         | 
| 41 | 
            -
              { "word": "FOOTHILLS", "clue": "Hills at base of mountains" },
         | 
| 42 | 
            -
              { "word": "RANGE", "clue": "Chain of mountains" },
         | 
| 43 | 
            -
              { "word": "BASIN", "clue": "Low-lying area" },
         | 
| 44 | 
            -
              { "word": "WATERSHED", "clue": "Drainage area" },
         | 
| 45 | 
            -
              { "word": "ESTUARY", "clue": "Where river meets sea" },
         | 
| 46 | 
            -
              { "word": "BAY", "clue": "Curved inlet of water" },
         | 
| 47 | 
            -
              { "word": "GULF", "clue": "Large bay" },
         | 
| 48 | 
            -
              { "word": "CAPE", "clue": "Point of land into water" },
         | 
| 49 | 
            -
              { "word": "HEADLAND", "clue": "High point of land" },
         | 
| 50 | 
            -
              { "word": "LAGOON", "clue": "Shallow coastal body of water" },
         | 
| 51 | 
            -
              { "word": "REEF", "clue": "Underwater rock formation" },
         | 
| 52 | 
            -
              { "word": "SHOAL", "clue": "Shallow area in water" },
         | 
| 53 | 
            -
              { "word": "CHANNEL", "clue": "Deep water passage" },
         | 
| 54 | 
            -
              { "word": "SOUND", "clue": "Large sea inlet" },
         | 
| 55 | 
            -
              { "word": "HARBOR", "clue": "Sheltered port area" },
         | 
| 56 | 
            -
              { "word": "INLET", "clue": "Small bay" },
         | 
| 57 | 
            -
              { "word": "COVE", "clue": "Small sheltered bay" },
         | 
| 58 | 
            -
              { "word": "MARSH", "clue": "Wetland area" },
         | 
| 59 | 
            -
              { "word": "SWAMP", "clue": "Forested wetland" },
         | 
| 60 | 
            -
              { "word": "BOG", "clue": "Acidic wetland" },
         | 
| 61 | 
            -
              { "word": "OASIS", "clue": "Fertile spot in desert" },
         | 
| 62 | 
            -
              { "word": "DUNE", "clue": "Sand hill" },
         | 
| 63 | 
            -
              { "word": "PLAIN", "clue": "Flat grassland" },
         | 
| 64 | 
            -
              { "word": "STEPPE", "clue": "Dry grassland" },
         | 
| 65 | 
            -
              { "word": "TAIGA", "clue": "Northern coniferous forest" },
         | 
| 66 | 
            -
              { "word": "RAINFOREST", "clue": "Dense tropical forest" },
         | 
| 67 | 
            -
              { "word": "JUNGLE", "clue": "Dense tropical vegetation" },
         | 
| 68 | 
            -
              { "word": "WOODLAND", "clue": "Area with scattered trees" },
         | 
| 69 | 
            -
              { "word": "GROVE", "clue": "Small group of trees" },
         | 
| 70 | 
            -
              { "word": "MEADOW", "clue": "Grassy field" },
         | 
| 71 | 
            -
              { "word": "PASTURE", "clue": "Grazing land" },
         | 
| 72 | 
            -
              { "word": "FIELD", "clue": "Open area of land" },
         | 
| 73 | 
            -
              { "word": "MOOR", "clue": "Open uncultivated land" },
         | 
| 74 | 
            -
              { "word": "HEATH", "clue": "Shrubland area" },
         | 
| 75 | 
            -
              { "word": "ARCTIC", "clue": "Cold northern region" },
         | 
| 76 | 
            -
              { "word": "ANTARCTIC", "clue": "Cold southern region" },
         | 
| 77 | 
            -
              { "word": "POLAR", "clue": "Of the poles" },
         | 
| 78 | 
            -
              { "word": "TROPICAL", "clue": "Hot humid climate zone" },
         | 
| 79 | 
            -
              { "word": "TEMPERATE", "clue": "Moderate climate zone" },
         | 
| 80 | 
            -
              { "word": "ARID", "clue": "Very dry" },
         | 
| 81 | 
            -
              { "word": "HUMID", "clue": "Moist air" },
         | 
| 82 | 
            -
              { "word": "ALTITUDE", "clue": "Height above sea level" },
         | 
| 83 | 
            -
              { "word": "ELEVATION", "clue": "Height of land" },
         | 
| 84 | 
            -
              { "word": "TERRAIN", "clue": "Physical features of land" },
         | 
| 85 | 
            -
              { "word": "TOPOGRAPHY", "clue": "Surface features of area" },
         | 
| 86 | 
            -
              { "word": "GEOGRAPHY", "clue": "Study of Earth's features" },
         | 
| 87 | 
            -
              { "word": "CARTOGRAPHY", "clue": "Map making" },
         | 
| 88 | 
            -
              { "word": "MERIDIAN", "clue": "Longitude line" },
         | 
| 89 | 
            -
              { "word": "PARALLEL", "clue": "Latitude line" },
         | 
| 90 | 
            -
              { "word": "HEMISPHERE", "clue": "Half of Earth" },
         | 
| 91 | 
            -
              { "word": "TROPICS", "clue": "Hot climate zone" },
         | 
| 92 | 
            -
              { "word": "POLES", "clue": "Earth's endpoints" },
         | 
| 93 | 
            -
              { "word": "AXIS", "clue": "Earth's rotation line" },
         | 
| 94 | 
            -
              { "word": "ORBIT", "clue": "Path around sun" },
         | 
| 95 | 
            -
              { "word": "SEASON", "clue": "Time of year" },
         | 
| 96 | 
            -
              { "word": "SOLSTICE", "clue": "Longest or shortest day" },
         | 
| 97 | 
            -
              { "word": "EQUINOX", "clue": "Equal day and night" },
         | 
| 98 | 
            -
              { "word": "COMPASS", "clue": "Direction-finding tool" },
         | 
| 99 | 
            -
              { "word": "NAVIGATION", "clue": "Finding your way" },
         | 
| 100 | 
            -
              { "word": "BEARING", "clue": "Direction or course" },
         | 
| 101 | 
            -
              { "word": "AZIMUTH", "clue": "Compass direction" },
         | 
| 102 | 
            -
              { "word": "SCALE", "clue": "Map size ratio" },
         | 
| 103 | 
            -
              { "word": "LEGEND", "clue": "Map symbol key" },
         | 
| 104 | 
            -
              { "word": "CONTOUR", "clue": "Elevation line on map" },
         | 
| 105 | 
            -
              { "word": "GRID", "clue": "Map reference system" },
         | 
| 106 | 
            -
              { "word": "PROJECTION", "clue": "Map flattening method" },
         | 
| 107 | 
            -
              { "word": "SURVEY", "clue": "Land measurement" },
         | 
| 108 | 
            -
              { "word": "BOUNDARY", "clue": "Dividing line" },
         | 
| 109 | 
            -
              { "word": "FRONTIER", "clue": "Border region" },
         | 
| 110 | 
            -
              { "word": "TERRITORY", "clue": "Area of land" },
         | 
| 111 | 
            -
              { "word": "REGION", "clue": "Geographic area" },
         | 
| 112 | 
            -
              { "word": "ZONE", "clue": "Designated area" },
         | 
| 113 | 
            -
              { "word": "DISTRICT", "clue": "Administrative area" },
         | 
| 114 | 
            -
              { "word": "PROVINCE", "clue": "Political subdivision" },
         | 
| 115 | 
            -
              { "word": "STATE", "clue": "Political entity" },
         | 
| 116 | 
            -
              { "word": "COUNTY", "clue": "Local government area" },
         | 
| 117 | 
            -
              { "word": "CITY", "clue": "Large urban area" },
         | 
| 118 | 
            -
              { "word": "TOWN", "clue": "Small urban area" },
         | 
| 119 | 
            -
              { "word": "VILLAGE", "clue": "Small rural community" },
         | 
| 120 | 
            -
              { "word": "HAMLET", "clue": "Very small village" },
         | 
| 121 | 
            -
              { "word": "SUBURB", "clue": "Residential area outside city" },
         | 
| 122 | 
            -
              { "word": "URBAN", "clue": "City-like" },
         | 
| 123 | 
            -
              { "word": "RURAL", "clue": "Countryside" },
         | 
| 124 | 
            -
              { "word": "METROPOLITAN", "clue": "Large city area" },
         | 
| 125 | 
            -
              { "word": "POPULATION", "clue": "Number of people" },
         | 
| 126 | 
            -
              { "word": "DENSITY", "clue": "Crowdedness" },
         | 
| 127 | 
            -
              { "word": "SETTLEMENT", "clue": "Place where people live" },
         | 
| 128 | 
            -
              { "word": "COLONY", "clue": "Overseas territory" },
         | 
| 129 | 
            -
              { "word": "NATION", "clue": "Country" },
         | 
| 130 | 
            -
              { "word": "REPUBLIC", "clue": "Democratic state" },
         | 
| 131 | 
            -
              { "word": "KINGDOM", "clue": "Monarchy" },
         | 
| 132 | 
            -
              { "word": "EMPIRE", "clue": "Large political entity" },
         | 
| 133 | 
            -
              { "word": "FEDERATION", "clue": "Union of states" },
         | 
| 134 | 
            -
              { "word": "ALLIANCE", "clue": "Partnership of nations" },
         | 
| 135 | 
            -
              { "word": "TREATY", "clue": "International agreement" },
         | 
| 136 | 
            -
              { "word": "TRADE", "clue": "Commercial exchange" },
         | 
| 137 | 
            -
              { "word": "EXPORT", "clue": "Goods sent abroad" },
         | 
| 138 | 
            -
              { "word": "IMPORT", "clue": "Goods brought in" },
         | 
| 139 | 
            -
              { "word": "COMMERCE", "clue": "Business activity" },
         | 
| 140 | 
            -
              { "word": "INDUSTRY", "clue": "Manufacturing" },
         | 
| 141 | 
            -
              { "word": "AGRICULTURE", "clue": "Farming" },
         | 
| 142 | 
            -
              { "word": "MINING", "clue": "Extracting minerals" },
         | 
| 143 | 
            -
              { "word": "FORESTRY", "clue": "Tree management" },
         | 
| 144 | 
            -
              { "word": "FISHING", "clue": "Catching fish" },
         | 
| 145 | 
            -
              { "word": "TOURISM", "clue": "Travel industry" },
         | 
| 146 | 
            -
              { "word": "TRANSPORTATION", "clue": "Moving people and goods" },
         | 
| 147 | 
            -
              { "word": "INFRASTRUCTURE", "clue": "Basic facilities" },
         | 
| 148 | 
            -
              { "word": "COMMUNICATION", "clue": "Information exchange" },
         | 
| 149 | 
            -
              { "word": "CULTURE", "clue": "Way of life" },
         | 
| 150 | 
            -
              { "word": "LANGUAGE", "clue": "Communication system" },
         | 
| 151 | 
            -
              { "word": "RELIGION", "clue": "Belief system" },
         | 
| 152 | 
            -
              { "word": "ETHNICITY", "clue": "Cultural group" },
         | 
| 153 | 
            -
              { "word": "MIGRATION", "clue": "Movement of people" },
         | 
| 154 | 
            -
              { "word": "IMMIGRATION", "clue": "Moving into country" },
         | 
| 155 | 
            -
              { "word": "EMIGRATION", "clue": "Moving out of country" },
         | 
| 156 | 
            -
              { "word": "DIASPORA", "clue": "Scattered population" },
         | 
| 157 | 
            -
              { "word": "NOMAD", "clue": "Wandering person" },
         | 
| 158 | 
            -
              { "word": "REFUGEE", "clue": "Displaced person" },
         | 
| 159 | 
            -
              { "word": "CENSUS", "clue": "Population count" },
         | 
| 160 | 
            -
              { "word": "DEMOGRAPHIC", "clue": "Population characteristic" }
         | 
| 161 | 
            -
            ]
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/data/word-lists/science.json
    DELETED
    
    | @@ -1,170 +0,0 @@ | |
| 1 | 
            -
            [
         | 
| 2 | 
            -
              { "word": "ATOM", "clue": "Smallest unit of matter" },
         | 
| 3 | 
            -
              { "word": "GRAVITY", "clue": "Force that pulls objects down" },
         | 
| 4 | 
            -
              { "word": "MOLECULE", "clue": "Group of atoms bonded together" },
         | 
| 5 | 
            -
              { "word": "PHOTON", "clue": "Particle of light" },
         | 
| 6 | 
            -
              { "word": "CHEMISTRY", "clue": "Study of matter and reactions" },
         | 
| 7 | 
            -
              { "word": "PHYSICS", "clue": "Study of matter and energy" },
         | 
| 8 | 
            -
              { "word": "BIOLOGY", "clue": "Study of living organisms" },
         | 
| 9 | 
            -
              { "word": "ELEMENT", "clue": "Pure chemical substance" },
         | 
| 10 | 
            -
              { "word": "OXYGEN", "clue": "Gas essential for breathing" },
         | 
| 11 | 
            -
              { "word": "CARBON", "clue": "Element found in all life" },
         | 
| 12 | 
            -
              { "word": "HYDROGEN", "clue": "Lightest chemical element" },
         | 
| 13 | 
            -
              { "word": "ENERGY", "clue": "Capacity to do work" },
         | 
| 14 | 
            -
              { "word": "FORCE", "clue": "Push or pull on an object" },
         | 
| 15 | 
            -
              { "word": "VELOCITY", "clue": "Speed with direction" },
         | 
| 16 | 
            -
              { "word": "MASS", "clue": "Amount of matter in object" },
         | 
| 17 | 
            -
              { "word": "VOLUME", "clue": "Amount of space occupied" },
         | 
| 18 | 
            -
              { "word": "DENSITY", "clue": "Mass per unit volume" },
         | 
| 19 | 
            -
              { "word": "PRESSURE", "clue": "Force per unit area" },
         | 
| 20 | 
            -
              { "word": "TEMPERATURE", "clue": "Measure of heat" },
         | 
| 21 | 
            -
              { "word": "ELECTRON", "clue": "Negatively charged particle" },
         | 
| 22 | 
            -
              { "word": "PROTON", "clue": "Positively charged particle" },
         | 
| 23 | 
            -
              { "word": "NEUTRON", "clue": "Neutral atomic particle" },
         | 
| 24 | 
            -
              { "word": "NUCLEUS", "clue": "Center of an atom" },
         | 
| 25 | 
            -
              { "word": "CELL", "clue": "Basic unit of life" },
         | 
| 26 | 
            -
              { "word": "DNA", "clue": "Genetic blueprint molecule" },
         | 
| 27 | 
            -
              { "word": "PROTEIN", "clue": "Complex biological molecule" },
         | 
| 28 | 
            -
              { "word": "ENZYME", "clue": "Biological catalyst" },
         | 
| 29 | 
            -
              { "word": "VIRUS", "clue": "Infectious agent" },
         | 
| 30 | 
            -
              { "word": "BACTERIA", "clue": "Single-celled organisms" },
         | 
| 31 | 
            -
              { "word": "EVOLUTION", "clue": "Change in species over time" },
         | 
| 32 | 
            -
              { "word": "ISOTOPE", "clue": "Atom variant with different neutrons" },
         | 
| 33 | 
            -
              { "word": "ION", "clue": "Charged atom or molecule" },
         | 
| 34 | 
            -
              { "word": "COMPOUND", "clue": "Chemical combination of elements" },
         | 
| 35 | 
            -
              { "word": "MIXTURE", "clue": "Combined substances retaining properties" },
         | 
| 36 | 
            -
              { "word": "SOLUTION", "clue": "Dissolved mixture" },
         | 
| 37 | 
            -
              { "word": "ACID", "clue": "Sour chemical with low pH" },
         | 
| 38 | 
            -
              { "word": "BASE", "clue": "Alkaline substance with high pH" },
         | 
| 39 | 
            -
              { "word": "SALT", "clue": "Ionic compound from acid-base reaction" },
         | 
| 40 | 
            -
              { "word": "CATALYST", "clue": "Substance that speeds reactions" },
         | 
| 41 | 
            -
              { "word": "RNA", "clue": "Genetic messenger molecule" },
         | 
| 42 | 
            -
              { "word": "GENE", "clue": "Heredity unit on chromosome" },
         | 
| 43 | 
            -
              { "word": "CHROMOSOME", "clue": "Gene-carrying structure" },
         | 
| 44 | 
            -
              { "word": "TISSUE", "clue": "Group of similar cells" },
         | 
| 45 | 
            -
              { "word": "ORGAN", "clue": "Body part with specific function" },
         | 
| 46 | 
            -
              { "word": "SYSTEM", "clue": "Group of organs working together" },
         | 
| 47 | 
            -
              { "word": "ORGANISM", "clue": "Living individual entity" },
         | 
| 48 | 
            -
              { "word": "SPECIES", "clue": "Group of similar organisms" },
         | 
| 49 | 
            -
              { "word": "ADAPTATION", "clue": "Survival-enhancing change" },
         | 
| 50 | 
            -
              { "word": "MUTATION", "clue": "Genetic change in DNA" },
         | 
| 51 | 
            -
              { "word": "HEREDITY", "clue": "Passing traits to offspring" },
         | 
| 52 | 
            -
              { "word": "ECOSYSTEM", "clue": "Community and environment" },
         | 
| 53 | 
            -
              { "word": "HABITAT", "clue": "Natural living environment" },
         | 
| 54 | 
            -
              { "word": "BIODIVERSITY", "clue": "Variety of life forms" },
         | 
| 55 | 
            -
              { "word": "PHOTOSYNTHESIS", "clue": "Plant energy-making process" },
         | 
| 56 | 
            -
              { "word": "RESPIRATION", "clue": "Cellular breathing process" },
         | 
| 57 | 
            -
              { "word": "METABOLISM", "clue": "Chemical processes in body" },
         | 
| 58 | 
            -
              { "word": "HOMEOSTASIS", "clue": "Body's internal balance" },
         | 
| 59 | 
            -
              { "word": "MITOSIS", "clue": "Cell division for growth" },
         | 
| 60 | 
            -
              { "word": "MEIOSIS", "clue": "Cell division for reproduction" },
         | 
| 61 | 
            -
              { "word": "EMBRYO", "clue": "Early development stage" },
         | 
| 62 | 
            -
              { "word": "FOSSIL", "clue": "Preserved ancient remains" },
         | 
| 63 | 
            -
              { "word": "GEOLOGY", "clue": "Study of Earth's structure" },
         | 
| 64 | 
            -
              { "word": "MINERAL", "clue": "Natural inorganic crystal" },
         | 
| 65 | 
            -
              { "word": "ROCK", "clue": "Solid earth material" },
         | 
| 66 | 
            -
              { "word": "SEDIMENT", "clue": "Settled particles" },
         | 
| 67 | 
            -
              { "word": "EROSION", "clue": "Gradual wearing away" },
         | 
| 68 | 
            -
              { "word": "VOLCANO", "clue": "Earth opening spewing lava" },
         | 
| 69 | 
            -
              { "word": "EARTHQUAKE", "clue": "Ground shaking from plate movement" },
         | 
| 70 | 
            -
              { "word": "PLATE", "clue": "Earth's crust section" },
         | 
| 71 | 
            -
              { "word": "MAGMA", "clue": "Molten rock beneath surface" },
         | 
| 72 | 
            -
              { "word": "LAVA", "clue": "Molten rock on surface" },
         | 
| 73 | 
            -
              { "word": "CRYSTAL", "clue": "Ordered atomic structure" },
         | 
| 74 | 
            -
              { "word": "ATMOSPHERE", "clue": "Layer of gases around Earth" },
         | 
| 75 | 
            -
              { "word": "CLIMATE", "clue": "Long-term weather pattern" },
         | 
| 76 | 
            -
              { "word": "WEATHER", "clue": "Short-term atmospheric conditions" },
         | 
| 77 | 
            -
              { "word": "PRECIPITATION", "clue": "Water falling from clouds" },
         | 
| 78 | 
            -
              { "word": "HUMIDITY", "clue": "Moisture in air" },
         | 
| 79 | 
            -
              { "word": "WIND", "clue": "Moving air mass" },
         | 
| 80 | 
            -
              { "word": "STORM", "clue": "Violent weather event" },
         | 
| 81 | 
            -
              { "word": "HURRICANE", "clue": "Powerful tropical cyclone" },
         | 
| 82 | 
            -
              { "word": "TORNADO", "clue": "Rotating column of air" },
         | 
| 83 | 
            -
              { "word": "LIGHTNING", "clue": "Electrical discharge in sky" },
         | 
| 84 | 
            -
              { "word": "THUNDER", "clue": "Sound of lightning" },
         | 
| 85 | 
            -
              { "word": "RAINBOW", "clue": "Spectrum of light in sky" },
         | 
| 86 | 
            -
              { "word": "ASTRONOMY", "clue": "Study of celestial objects" },
         | 
| 87 | 
            -
              { "word": "GALAXY", "clue": "Collection of stars and planets" },
         | 
| 88 | 
            -
              { "word": "PLANET", "clue": "Large orbiting celestial body" },
         | 
| 89 | 
            -
              { "word": "STAR", "clue": "Self-luminous celestial body" },
         | 
| 90 | 
            -
              { "word": "MOON", "clue": "Natural satellite of planet" },
         | 
| 91 | 
            -
              { "word": "COMET", "clue": "Icy body with tail" },
         | 
| 92 | 
            -
              { "word": "ASTEROID", "clue": "Rocky space object" },
         | 
| 93 | 
            -
              { "word": "METEOR", "clue": "Space rock entering atmosphere" },
         | 
| 94 | 
            -
              { "word": "ORBIT", "clue": "Curved path around object" },
         | 
| 95 | 
            -
              { "word": "LIGHT", "clue": "Electromagnetic radiation" },
         | 
| 96 | 
            -
              { "word": "SPECTRUM", "clue": "Range of electromagnetic radiation" },
         | 
| 97 | 
            -
              { "word": "WAVELENGTH", "clue": "Distance between wave peaks" },
         | 
| 98 | 
            -
              { "word": "FREQUENCY", "clue": "Waves per unit time" },
         | 
| 99 | 
            -
              { "word": "AMPLITUDE", "clue": "Wave height or intensity" },
         | 
| 100 | 
            -
              { "word": "SOUND", "clue": "Vibrations in air" },
         | 
| 101 | 
            -
              { "word": "ECHO", "clue": "Reflected sound" },
         | 
| 102 | 
            -
              { "word": "RESONANCE", "clue": "Vibration amplification" },
         | 
| 103 | 
            -
              { "word": "DOPPLER", "clue": "Wave frequency shift effect" },
         | 
| 104 | 
            -
              { "word": "MOTION", "clue": "Change in position" },
         | 
| 105 | 
            -
              { "word": "ACCELERATION", "clue": "Change in velocity" },
         | 
| 106 | 
            -
              { "word": "MOMENTUM", "clue": "Mass times velocity" },
         | 
| 107 | 
            -
              { "word": "INERTIA", "clue": "Resistance to motion change" },
         | 
| 108 | 
            -
              { "word": "FRICTION", "clue": "Resistance to sliding" },
         | 
| 109 | 
            -
              { "word": "HEAT", "clue": "Thermal energy transfer" },
         | 
| 110 | 
            -
              { "word": "COMBUSTION", "clue": "Burning chemical reaction" },
         | 
| 111 | 
            -
              { "word": "OXIDATION", "clue": "Reaction with oxygen" },
         | 
| 112 | 
            -
              { "word": "REDUCTION", "clue": "Gain of electrons" },
         | 
| 113 | 
            -
              { "word": "ELECTROLYSIS", "clue": "Chemical breakdown by electricity" },
         | 
| 114 | 
            -
              { "word": "CONDUCTIVITY", "clue": "Ability to transfer energy" },
         | 
| 115 | 
            -
              { "word": "INSULATOR", "clue": "Material blocking energy flow" },
         | 
| 116 | 
            -
              { "word": "SEMICONDUCTOR", "clue": "Partial electrical conductor" },
         | 
| 117 | 
            -
              { "word": "MAGNETISM", "clue": "Force of magnetic attraction" },
         | 
| 118 | 
            -
              { "word": "FIELD", "clue": "Region of force influence" },
         | 
| 119 | 
            -
              { "word": "CIRCUIT", "clue": "Closed electrical path" },
         | 
| 120 | 
            -
              { "word": "CURRENT", "clue": "Flow of electric charge" },
         | 
| 121 | 
            -
              { "word": "VOLTAGE", "clue": "Electric potential difference" },
         | 
| 122 | 
            -
              { "word": "RESISTANCE", "clue": "Opposition to current flow" },
         | 
| 123 | 
            -
              { "word": "CAPACITOR", "clue": "Device storing electric charge" },
         | 
| 124 | 
            -
              { "word": "INDUCTOR", "clue": "Device storing magnetic energy" },
         | 
| 125 | 
            -
              { "word": "TRANSISTOR", "clue": "Electronic switching device" },
         | 
| 126 | 
            -
              { "word": "LASER", "clue": "Focused beam of light" },
         | 
| 127 | 
            -
              { "word": "RADAR", "clue": "Radio detection system" },
         | 
| 128 | 
            -
              { "word": "SONAR", "clue": "Sound detection system" },
         | 
| 129 | 
            -
              { "word": "TELESCOPE", "clue": "Instrument for viewing distant objects" },
         | 
| 130 | 
            -
              { "word": "MICROSCOPE", "clue": "Instrument for viewing small objects" },
         | 
| 131 | 
            -
              { "word": "HYPOTHESIS", "clue": "Testable scientific prediction" },
         | 
| 132 | 
            -
              { "word": "THEORY", "clue": "Well-tested scientific explanation" },
         | 
| 133 | 
            -
              { "word": "LAW", "clue": "Consistently observed scientific rule" },
         | 
| 134 | 
            -
              { "word": "EXPERIMENT", "clue": "Controlled scientific test" },
         | 
| 135 | 
            -
              { "word": "OBSERVATION", "clue": "Careful scientific watching" },
         | 
| 136 | 
            -
              { "word": "MEASUREMENT", "clue": "Quantified observation" },
         | 
| 137 | 
            -
              { "word": "ANALYSIS", "clue": "Detailed examination of data" },
         | 
| 138 | 
            -
              { "word": "SYNTHESIS", "clue": "Combining elements into whole" },
         | 
| 139 | 
            -
              { "word": "VARIABLE", "clue": "Factor that can change" },
         | 
| 140 | 
            -
              { "word": "CONTROL", "clue": "Unchanged comparison group" },
         | 
| 141 | 
            -
              { "word": "DATA", "clue": "Information collected from tests" },
         | 
| 142 | 
            -
              { "word": "STATISTICS", "clue": "Mathematical analysis of data" },
         | 
| 143 | 
            -
              { "word": "PROBABILITY", "clue": "Likelihood of occurrence" },
         | 
| 144 | 
            -
              { "word": "PRECISION", "clue": "Exactness of measurement" },
         | 
| 145 | 
            -
              { "word": "ACCURACY", "clue": "Correctness of measurement" },
         | 
| 146 | 
            -
              { "word": "ERROR", "clue": "Difference from true value" },
         | 
| 147 | 
            -
              { "word": "UNCERTAINTY", "clue": "Range of doubt in measurement" },
         | 
| 148 | 
            -
              { "word": "CALIBRATION", "clue": "Adjusting instrument accuracy" },
         | 
| 149 | 
            -
              { "word": "STANDARD", "clue": "Reference for measurement" },
         | 
| 150 | 
            -
              { "word": "UNIT", "clue": "Base measure of quantity" },
         | 
| 151 | 
            -
              { "word": "METRIC", "clue": "Decimal measurement system" },
         | 
| 152 | 
            -
              { "word": "WEIGHT", "clue": "Force of gravity on mass" },
         | 
| 153 | 
            -
              { "word": "CONCENTRATION", "clue": "Amount of substance per volume" },
         | 
| 154 | 
            -
              { "word": "MOLARITY", "clue": "Moles of solute per liter" },
         | 
| 155 | 
            -
              { "word": "EQUILIBRIUM", "clue": "State of balanced forces" },
         | 
| 156 | 
            -
              { "word": "STABILITY", "clue": "Resistance to change" },
         | 
| 157 | 
            -
              { "word": "DECAY", "clue": "Gradual breakdown process" },
         | 
| 158 | 
            -
              { "word": "RADIATION", "clue": "Energy emitted from source" },
         | 
| 159 | 
            -
              { "word": "RADIOACTIVE", "clue": "Emitting nuclear radiation" },
         | 
| 160 | 
            -
              { "word": "HALFLIFE", "clue": "Time for half to decay" },
         | 
| 161 | 
            -
              { "word": "FUSION", "clue": "Nuclear combining reaction" },
         | 
| 162 | 
            -
              { "word": "FISSION", "clue": "Nuclear splitting reaction" },
         | 
| 163 | 
            -
              { "word": "QUANTUM", "clue": "Discrete packet of energy" },
         | 
| 164 | 
            -
              { "word": "PARTICLE", "clue": "Tiny piece of matter" },
         | 
| 165 | 
            -
              { "word": "WAVE", "clue": "Energy transfer disturbance" },
         | 
| 166 | 
            -
              { "word": "INTERFERENCE", "clue": "Wave interaction effect" },
         | 
| 167 | 
            -
              { "word": "DIFFRACTION", "clue": "Wave bending around obstacle" },
         | 
| 168 | 
            -
              { "word": "REFLECTION", "clue": "Bouncing back of waves" },
         | 
| 169 | 
            -
              { "word": "REFRACTION", "clue": "Bending of waves through medium" }
         | 
| 170 | 
            -
            ]
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/data/word-lists/technology.json
    DELETED
    
    | @@ -1,221 +0,0 @@ | |
| 1 | 
            -
            [
         | 
| 2 | 
            -
              { "word": "COMPUTER", "clue": "Electronic processing device" },
         | 
| 3 | 
            -
              { "word": "INTERNET", "clue": "Global computer network" },
         | 
| 4 | 
            -
              { "word": "ALGORITHM", "clue": "Set of rules for solving problems" },
         | 
| 5 | 
            -
              { "word": "DATABASE", "clue": "Organized collection of data" },
         | 
| 6 | 
            -
              { "word": "SOFTWARE", "clue": "Computer programs" },
         | 
| 7 | 
            -
              { "word": "HARDWARE", "clue": "Physical computer components" },
         | 
| 8 | 
            -
              { "word": "NETWORK", "clue": "Connected system of computers" },
         | 
| 9 | 
            -
              { "word": "CODE", "clue": "Programming instructions" },
         | 
| 10 | 
            -
              { "word": "ROBOT", "clue": "Automated machine" },
         | 
| 11 | 
            -
              { "word": "ARTIFICIAL", "clue": "Made by humans, not natural" },
         | 
| 12 | 
            -
              { "word": "DIGITAL", "clue": "Using binary data" },
         | 
| 13 | 
            -
              { "word": "BINARY", "clue": "Base-2 number system" },
         | 
| 14 | 
            -
              { "word": "PROCESSOR", "clue": "Computer's brain" },
         | 
| 15 | 
            -
              { "word": "MEMORY", "clue": "Data storage component" },
         | 
| 16 | 
            -
              { "word": "KEYBOARD", "clue": "Input device with keys" },
         | 
| 17 | 
            -
              { "word": "MONITOR", "clue": "Computer display screen" },
         | 
| 18 | 
            -
              { "word": "MOUSE", "clue": "Pointing input device" },
         | 
| 19 | 
            -
              { "word": "PRINTER", "clue": "Device that prints documents" },
         | 
| 20 | 
            -
              { "word": "SCANNER", "clue": "Device that digitizes images" },
         | 
| 21 | 
            -
              { "word": "CAMERA", "clue": "Device that captures images" },
         | 
| 22 | 
            -
              { "word": "SMARTPHONE", "clue": "Portable computing device" },
         | 
| 23 | 
            -
              { "word": "TABLET", "clue": "Touchscreen computing device" },
         | 
| 24 | 
            -
              { "word": "LAPTOP", "clue": "Portable computer" },
         | 
| 25 | 
            -
              { "word": "SERVER", "clue": "Computer that serves data" },
         | 
| 26 | 
            -
              { "word": "CLOUD", "clue": "Internet-based computing" },
         | 
| 27 | 
            -
              { "word": "WEBSITE", "clue": "Collection of web pages" },
         | 
| 28 | 
            -
              { "word": "EMAIL", "clue": "Electronic mail" },
         | 
| 29 | 
            -
              { "word": "BROWSER", "clue": "Web navigation software" },
         | 
| 30 | 
            -
              { "word": "SEARCH", "clue": "Look for information" },
         | 
| 31 | 
            -
              { "word": "DOWNLOAD", "clue": "Transfer data to device" },
         | 
| 32 | 
            -
              { "word": "UPLOAD", "clue": "Transfer data from device" },
         | 
| 33 | 
            -
              { "word": "BANDWIDTH", "clue": "Data transfer capacity" },
         | 
| 34 | 
            -
              { "word": "PROTOCOL", "clue": "Communication rules" },
         | 
| 35 | 
            -
              { "word": "FIREWALL", "clue": "Network security barrier" },
         | 
| 36 | 
            -
              { "word": "ENCRYPTION", "clue": "Data scrambling for security" },
         | 
| 37 | 
            -
              { "word": "PASSWORD", "clue": "Secret access code" },
         | 
| 38 | 
            -
              { "word": "SECURITY", "clue": "Protection from threats" },
         | 
| 39 | 
            -
              { "word": "VIRUS", "clue": "Malicious computer program" },
         | 
| 40 | 
            -
              { "word": "MALWARE", "clue": "Harmful software" },
         | 
| 41 | 
            -
              { "word": "ANTIVIRUS", "clue": "Protection software" },
         | 
| 42 | 
            -
              { "word": "BACKUP", "clue": "Data safety copy" },
         | 
| 43 | 
            -
              { "word": "RECOVERY", "clue": "Data restoration process" },
         | 
| 44 | 
            -
              { "word": "STORAGE", "clue": "Data keeping capacity" },
         | 
| 45 | 
            -
              { "word": "HARDDRIVE", "clue": "Magnetic storage device" },
         | 
| 46 | 
            -
              { "word": "FLASH", "clue": "Solid state storage" },
         | 
| 47 | 
            -
              { "word": "RAM", "clue": "Random access memory" },
         | 
| 48 | 
            -
              { "word": "ROM", "clue": "Read-only memory" },
         | 
| 49 | 
            -
              { "word": "CPU", "clue": "Central processing unit" },
         | 
| 50 | 
            -
              { "word": "GPU", "clue": "Graphics processing unit" },
         | 
| 51 | 
            -
              { "word": "MOTHERBOARD", "clue": "Main circuit board" },
         | 
| 52 | 
            -
              { "word": "CHIP", "clue": "Integrated circuit" },
         | 
| 53 | 
            -
              { "word": "CIRCUIT", "clue": "Electronic pathway" },
         | 
| 54 | 
            -
              { "word": "TRANSISTOR", "clue": "Electronic switch" },
         | 
| 55 | 
            -
              { "word": "SILICON", "clue": "Semiconductor material" },
         | 
| 56 | 
            -
              { "word": "NANOTECHNOLOGY", "clue": "Extremely small scale tech" },
         | 
| 57 | 
            -
              { "word": "AUTOMATION", "clue": "Self-operating technology" },
         | 
| 58 | 
            -
              { "word": "MACHINE", "clue": "Mechanical device" },
         | 
| 59 | 
            -
              { "word": "SENSOR", "clue": "Detection device" },
         | 
| 60 | 
            -
              { "word": "ACTUATOR", "clue": "Movement device" },
         | 
| 61 | 
            -
              { "word": "FEEDBACK", "clue": "System response information" },
         | 
| 62 | 
            -
              { "word": "PROGRAMMING", "clue": "Writing computer instructions" },
         | 
| 63 | 
            -
              { "word": "FUNCTION", "clue": "Reusable code block" },
         | 
| 64 | 
            -
              { "word": "VARIABLE", "clue": "Data storage container" },
         | 
| 65 | 
            -
              { "word": "LOOP", "clue": "Repeating code structure" },
         | 
| 66 | 
            -
              { "word": "CONDITION", "clue": "Decision-making logic" },
         | 
| 67 | 
            -
              { "word": "DEBUG", "clue": "Find and fix errors" },
         | 
| 68 | 
            -
              { "word": "COMPILE", "clue": "Convert code to executable" },
         | 
| 69 | 
            -
              { "word": "RUNTIME", "clue": "Program execution time" },
         | 
| 70 | 
            -
              { "word": "API", "clue": "Application programming interface" },
         | 
| 71 | 
            -
              { "word": "FRAMEWORK", "clue": "Code structure foundation" },
         | 
| 72 | 
            -
              { "word": "LIBRARY", "clue": "Reusable code collection" },
         | 
| 73 | 
            -
              { "word": "MODULE", "clue": "Self-contained code unit" },
         | 
| 74 | 
            -
              { "word": "OBJECT", "clue": "Data and methods container" },
         | 
| 75 | 
            -
              { "word": "CLASS", "clue": "Object blueprint" },
         | 
| 76 | 
            -
              { "word": "INHERITANCE", "clue": "Code reuse mechanism" },
         | 
| 77 | 
            -
              { "word": "INTERFACE", "clue": "System interaction boundary" },
         | 
| 78 | 
            -
              { "word": "PROTOCOL", "clue": "Communication standard" },
         | 
| 79 | 
            -
              { "word": "FORMAT", "clue": "Data structure standard" },
         | 
| 80 | 
            -
              { "word": "SYNTAX", "clue": "Language rules" },
         | 
| 81 | 
            -
              { "word": "SEMANTIC", "clue": "Meaning in code" },
         | 
| 82 | 
            -
              { "word": "PARSING", "clue": "Analyzing code structure" },
         | 
| 83 | 
            -
              { "word": "COMPILER", "clue": "Code translation program" },
         | 
| 84 | 
            -
              { "word": "INTERPRETER", "clue": "Code execution program" },
         | 
| 85 | 
            -
              { "word": "VIRTUAL", "clue": "Simulated environment" },
         | 
| 86 | 
            -
              { "word": "SIMULATION", "clue": "Computer modeling" },
         | 
| 87 | 
            -
              { "word": "EMULATION", "clue": "System imitation" },
         | 
| 88 | 
            -
              { "word": "OPTIMIZATION", "clue": "Performance improvement" },
         | 
| 89 | 
            -
              { "word": "EFFICIENCY", "clue": "Resource usage effectiveness" },
         | 
| 90 | 
            -
              { "word": "PERFORMANCE", "clue": "System speed and quality" },
         | 
| 91 | 
            -
              { "word": "BENCHMARK", "clue": "Performance measurement" },
         | 
| 92 | 
            -
              { "word": "TESTING", "clue": "Quality verification process" },
         | 
| 93 | 
            -
              { "word": "VALIDATION", "clue": "Correctness checking" },
         | 
| 94 | 
            -
              { "word": "VERIFICATION", "clue": "Accuracy confirmation" },
         | 
| 95 | 
            -
              { "word": "QUALITY", "clue": "Standard of excellence" },
         | 
| 96 | 
            -
              { "word": "MAINTENANCE", "clue": "System upkeep" },
         | 
| 97 | 
            -
              { "word": "UPDATE", "clue": "Software improvement" },
         | 
| 98 | 
            -
              { "word": "PATCH", "clue": "Software fix" },
         | 
| 99 | 
            -
              { "word": "VERSION", "clue": "Software release number" },
         | 
| 100 | 
            -
              { "word": "RELEASE", "clue": "Software distribution" },
         | 
| 101 | 
            -
              { "word": "DEPLOYMENT", "clue": "Software installation" },
         | 
| 102 | 
            -
              { "word": "CONFIGURATION", "clue": "System setup" },
         | 
| 103 | 
            -
              { "word": "INSTALLATION", "clue": "Software setup process" },
         | 
| 104 | 
            -
              { "word": "MIGRATION", "clue": "System transition" },
         | 
| 105 | 
            -
              { "word": "INTEGRATION", "clue": "System combination" },
         | 
| 106 | 
            -
              { "word": "COMPATIBILITY", "clue": "System cooperation ability" },
         | 
| 107 | 
            -
              { "word": "INTEROPERABILITY", "clue": "Cross-system communication" },
         | 
| 108 | 
            -
              { "word": "SCALABILITY", "clue": "Growth accommodation ability" },
         | 
| 109 | 
            -
              { "word": "RELIABILITY", "clue": "Consistent performance" },
         | 
| 110 | 
            -
              { "word": "AVAILABILITY", "clue": "System accessibility" },
         | 
| 111 | 
            -
              { "word": "REDUNDANCY", "clue": "Backup system duplication" },
         | 
| 112 | 
            -
              { "word": "FAULT", "clue": "System error condition" },
         | 
| 113 | 
            -
              { "word": "TOLERANCE", "clue": "Error handling ability" },
         | 
| 114 | 
            -
              { "word": "RECOVERY", "clue": "System restoration" },
         | 
| 115 | 
            -
              { "word": "MONITORING", "clue": "System observation" },
         | 
| 116 | 
            -
              { "word": "LOGGING", "clue": "Event recording" },
         | 
| 117 | 
            -
              { "word": "ANALYTICS", "clue": "Data analysis" },
         | 
| 118 | 
            -
              { "word": "METRICS", "clue": "Measurement data" },
         | 
| 119 | 
            -
              { "word": "DASHBOARD", "clue": "Information display panel" },
         | 
| 120 | 
            -
              { "word": "INTERFACE", "clue": "User interaction design" },
         | 
| 121 | 
            -
              { "word": "EXPERIENCE", "clue": "User interaction quality" },
         | 
| 122 | 
            -
              { "word": "USABILITY", "clue": "Ease of use" },
         | 
| 123 | 
            -
              { "word": "ACCESSIBILITY", "clue": "Universal design principle" },
         | 
| 124 | 
            -
              { "word": "RESPONSIVE", "clue": "Adaptive design" },
         | 
| 125 | 
            -
              { "word": "MOBILE", "clue": "Portable device category" },
         | 
| 126 | 
            -
              { "word": "TOUCHSCREEN", "clue": "Touch-sensitive display" },
         | 
| 127 | 
            -
              { "word": "GESTURE", "clue": "Touch movement command" },
         | 
| 128 | 
            -
              { "word": "VOICE", "clue": "Speech interaction" },
         | 
| 129 | 
            -
              { "word": "RECOGNITION", "clue": "Pattern identification" },
         | 
| 130 | 
            -
              { "word": "LEARNING", "clue": "Adaptive improvement" },
         | 
| 131 | 
            -
              { "word": "INTELLIGENCE", "clue": "Artificial reasoning" },
         | 
| 132 | 
            -
              { "word": "NEURAL", "clue": "Brain-inspired network" },
         | 
| 133 | 
            -
              { "word": "DEEP", "clue": "Multi-layered learning" },
         | 
| 134 | 
            -
              { "word": "MACHINE", "clue": "Automated learning system" },
         | 
| 135 | 
            -
              { "word": "DATA", "clue": "Information collection" },
         | 
| 136 | 
            -
              { "word": "BIG", "clue": "Large scale data" },
         | 
| 137 | 
            -
              { "word": "MINING", "clue": "Data pattern extraction" },
         | 
| 138 | 
            -
              { "word": "ANALYSIS", "clue": "Data examination" },
         | 
| 139 | 
            -
              { "word": "VISUALIZATION", "clue": "Data graphic representation" },
         | 
| 140 | 
            -
              { "word": "DASHBOARD", "clue": "Data monitoring panel" },
         | 
| 141 | 
            -
              { "word": "REPORT", "clue": "Data summary document" },
         | 
| 142 | 
            -
              { "word": "QUERY", "clue": "Data search request" },
         | 
| 143 | 
            -
              { "word": "INDEX", "clue": "Data location reference" },
         | 
| 144 | 
            -
              { "word": "SCHEMA", "clue": "Data structure blueprint" },
         | 
| 145 | 
            -
              { "word": "TABLE", "clue": "Data organization structure" },
         | 
| 146 | 
            -
              { "word": "RECORD", "clue": "Data entry" },
         | 
| 147 | 
            -
              { "word": "FIELD", "clue": "Data element" },
         | 
| 148 | 
            -
              { "word": "PRIMARY", "clue": "Main identifier key" },
         | 
| 149 | 
            -
              { "word": "FOREIGN", "clue": "Reference relationship key" },
         | 
| 150 | 
            -
              { "word": "RELATION", "clue": "Data connection" },
         | 
| 151 | 
            -
              { "word": "JOIN", "clue": "Data combination operation" },
         | 
| 152 | 
            -
              { "word": "TRANSACTION", "clue": "Data operation sequence" },
         | 
| 153 | 
            -
              { "word": "COMMIT", "clue": "Data change confirmation" },
         | 
| 154 | 
            -
              { "word": "ROLLBACK", "clue": "Data change reversal" },
         | 
| 155 | 
            -
              { "word": "CONCURRENCY", "clue": "Simultaneous access handling" },
         | 
| 156 | 
            -
              { "word": "LOCK", "clue": "Data access control" },
         | 
| 157 | 
            -
              { "word": "SYNCHRONIZATION", "clue": "Timing coordination" },
         | 
| 158 | 
            -
              { "word": "THREAD", "clue": "Execution sequence" },
         | 
| 159 | 
            -
              { "word": "PROCESS", "clue": "Running program instance" },
         | 
| 160 | 
            -
              { "word": "MULTITASKING", "clue": "Multiple process handling" },
         | 
| 161 | 
            -
              { "word": "PARALLEL", "clue": "Simultaneous execution" },
         | 
| 162 | 
            -
              { "word": "DISTRIBUTED", "clue": "Spread across multiple systems" },
         | 
| 163 | 
            -
              { "word": "CLUSTER", "clue": "Group of connected computers" },
         | 
| 164 | 
            -
              { "word": "GRID", "clue": "Distributed computing network" },
         | 
| 165 | 
            -
              { "word": "PEER", "clue": "Equal network participant" },
         | 
| 166 | 
            -
              { "word": "CLIENT", "clue": "Service requesting system" },
         | 
| 167 | 
            -
              { "word": "SERVICE", "clue": "System functionality provider" },
         | 
| 168 | 
            -
              { "word": "MICROSERVICE", "clue": "Small independent service" },
         | 
| 169 | 
            -
              { "word": "CONTAINER", "clue": "Isolated application environment" },
         | 
| 170 | 
            -
              { "word": "DOCKER", "clue": "Containerization platform" },
         | 
| 171 | 
            -
              { "word": "KUBERNETES", "clue": "Container orchestration" },
         | 
| 172 | 
            -
              { "word": "DEVOPS", "clue": "Development operations practice" },
         | 
| 173 | 
            -
              { "word": "AGILE", "clue": "Flexible development method" },
         | 
| 174 | 
            -
              { "word": "SCRUM", "clue": "Iterative development framework" },
         | 
| 175 | 
            -
              { "word": "SPRINT", "clue": "Short development cycle" },
         | 
| 176 | 
            -
              { "word": "KANBAN", "clue": "Visual workflow management" },
         | 
| 177 | 
            -
              { "word": "CONTINUOUS", "clue": "Ongoing integration practice" },
         | 
| 178 | 
            -
              { "word": "PIPELINE", "clue": "Automated workflow" },
         | 
| 179 | 
            -
              { "word": "BUILD", "clue": "Software compilation process" },
         | 
| 180 | 
            -
              { "word": "TESTING", "clue": "Quality assurance process" },
         | 
| 181 | 
            -
              { "word": "AUTOMATION", "clue": "Manual task elimination" },
         | 
| 182 | 
            -
              { "word": "SCRIPT", "clue": "Automated task sequence" },
         | 
| 183 | 
            -
              { "word": "BATCH", "clue": "Group processing" },
         | 
| 184 | 
            -
              { "word": "STREAMING", "clue": "Continuous data flow" },
         | 
| 185 | 
            -
              { "word": "REALTIME", "clue": "Immediate processing" },
         | 
| 186 | 
            -
              { "word": "LATENCY", "clue": "Response delay time" },
         | 
| 187 | 
            -
              { "word": "THROUGHPUT", "clue": "Processing capacity" },
         | 
| 188 | 
            -
              { "word": "BOTTLENECK", "clue": "Performance limitation point" },
         | 
| 189 | 
            -
              { "word": "CACHE", "clue": "Fast temporary storage" },
         | 
| 190 | 
            -
              { "word": "BUFFER", "clue": "Temporary data holder" },
         | 
| 191 | 
            -
              { "word": "QUEUE", "clue": "Ordered waiting line" },
         | 
| 192 | 
            -
              { "word": "STACK", "clue": "Last-in-first-out structure" },
         | 
| 193 | 
            -
              { "word": "HEAP", "clue": "Dynamic memory area" },
         | 
| 194 | 
            -
              { "word": "POINTER", "clue": "Memory address reference" },
         | 
| 195 | 
            -
              { "word": "REFERENCE", "clue": "Object location indicator" },
         | 
| 196 | 
            -
              { "word": "GARBAGE", "clue": "Unused memory collection" },
         | 
| 197 | 
            -
              { "word": "ALLOCATION", "clue": "Memory assignment" },
         | 
| 198 | 
            -
              { "word": "DEALLOCATION", "clue": "Memory release" },
         | 
| 199 | 
            -
              { "word": "LEAK", "clue": "Memory usage error" },
         | 
| 200 | 
            -
              { "word": "OVERFLOW", "clue": "Capacity exceeding error" },
         | 
| 201 | 
            -
              { "word": "UNDERFLOW", "clue": "Insufficient data error" },
         | 
| 202 | 
            -
              { "word": "EXCEPTION", "clue": "Error handling mechanism" },
         | 
| 203 | 
            -
              { "word": "INTERRUPT", "clue": "Process suspension signal" },
         | 
| 204 | 
            -
              { "word": "SIGNAL", "clue": "Process communication" },
         | 
| 205 | 
            -
              { "word": "EVENT", "clue": "System occurrence" },
         | 
| 206 | 
            -
              { "word": "HANDLER", "clue": "Event processing function" },
         | 
| 207 | 
            -
              { "word": "CALLBACK", "clue": "Function reference" },
         | 
| 208 | 
            -
              { "word": "PROMISE", "clue": "Future value placeholder" },
         | 
| 209 | 
            -
              { "word": "ASYNC", "clue": "Non-blocking operation" },
         | 
| 210 | 
            -
              { "word": "AWAIT", "clue": "Pause for completion" },
         | 
| 211 | 
            -
              { "word": "YIELD", "clue": "Temporary function pause" },
         | 
| 212 | 
            -
              { "word": "GENERATOR", "clue": "Value sequence producer" },
         | 
| 213 | 
            -
              { "word": "ITERATOR", "clue": "Sequential access pattern" },
         | 
| 214 | 
            -
              { "word": "RECURSION", "clue": "Self-calling function" },
         | 
| 215 | 
            -
              { "word": "CLOSURE", "clue": "Function scope retention" },
         | 
| 216 | 
            -
              { "word": "LAMBDA", "clue": "Anonymous function" },
         | 
| 217 | 
            -
              { "word": "FUNCTIONAL", "clue": "Function-based programming" },
         | 
| 218 | 
            -
              { "word": "PROCEDURAL", "clue": "Step-by-step programming" },
         | 
| 219 | 
            -
              { "word": "DECLARATIVE", "clue": "What-not-how programming" },
         | 
| 220 | 
            -
              { "word": "IMPERATIVE", "clue": "Command-based programming" }
         | 
| 221 | 
            -
            ]
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/debug_full_generation.py
    DELETED
    
    | @@ -1,316 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
            """
         | 
| 3 | 
            -
            Debug the complete crossword generation process to identify display/numbering issues.
         | 
| 4 | 
            -
            """
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            import asyncio
         | 
| 7 | 
            -
            import sys
         | 
| 8 | 
            -
            import json
         | 
| 9 | 
            -
            from pathlib import Path
         | 
| 10 | 
            -
             | 
| 11 | 
            -
            # Add project root to path
         | 
| 12 | 
            -
            project_root = Path(__file__).parent
         | 
| 13 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
         | 
| 16 | 
            -
             | 
| 17 | 
            -
            async def debug_complete_generation():
         | 
| 18 | 
            -
                """Debug the complete crossword generation process."""
         | 
| 19 | 
            -
                
         | 
| 20 | 
            -
                print("🔍 Debugging Complete Crossword Generation Process\n")
         | 
| 21 | 
            -
                
         | 
| 22 | 
            -
                # Create generator with no vector service to use static words
         | 
| 23 | 
            -
                generator = CrosswordGeneratorFixed(vector_service=None)
         | 
| 24 | 
            -
                
         | 
| 25 | 
            -
                # Override the word selection to use controlled test words
         | 
| 26 | 
            -
                test_words = [
         | 
| 27 | 
            -
                    {"word": "MACHINE", "clue": "Device with moving parts"},
         | 
| 28 | 
            -
                    {"word": "COMPUTER", "clue": "Electronic device"}, 
         | 
| 29 | 
            -
                    {"word": "EXPERT", "clue": "Person with specialized knowledge"},
         | 
| 30 | 
            -
                    {"word": "SCIENCE", "clue": "Systematic study"},
         | 
| 31 | 
            -
                    {"word": "TECHNOLOGY", "clue": "Applied science"},
         | 
| 32 | 
            -
                    {"word": "RESEARCH", "clue": "Systematic investigation"},
         | 
| 33 | 
            -
                    {"word": "ANALYSIS", "clue": "Detailed examination"},
         | 
| 34 | 
            -
                    {"word": "METHOD", "clue": "Systematic approach"}
         | 
| 35 | 
            -
                ]
         | 
| 36 | 
            -
                
         | 
| 37 | 
            -
                # Mock the word selection method
         | 
| 38 | 
            -
                async def mock_select_words(topics, difficulty, use_ai):
         | 
| 39 | 
            -
                    return test_words
         | 
| 40 | 
            -
                generator._select_words = mock_select_words
         | 
| 41 | 
            -
                
         | 
| 42 | 
            -
                print("=" * 70)
         | 
| 43 | 
            -
                print("GENERATING COMPLETE CROSSWORD")
         | 
| 44 | 
            -
                print("=" * 70)
         | 
| 45 | 
            -
                
         | 
| 46 | 
            -
                try:
         | 
| 47 | 
            -
                    result = await generator.generate_puzzle(["technology"], "medium", use_ai=False)
         | 
| 48 | 
            -
                    
         | 
| 49 | 
            -
                    if result:
         | 
| 50 | 
            -
                        print("✅ Crossword generation successful!")
         | 
| 51 | 
            -
                        
         | 
| 52 | 
            -
                        # Analyze the complete result
         | 
| 53 | 
            -
                        analyze_crossword_result(result)
         | 
| 54 | 
            -
                    else:
         | 
| 55 | 
            -
                        print("❌ Crossword generation failed - returned None")
         | 
| 56 | 
            -
                        
         | 
| 57 | 
            -
                except Exception as e:
         | 
| 58 | 
            -
                    print(f"❌ Crossword generation failed with error: {e}")
         | 
| 59 | 
            -
                    import traceback
         | 
| 60 | 
            -
                    traceback.print_exc()
         | 
| 61 | 
            -
             | 
| 62 | 
            -
            def analyze_crossword_result(result):
         | 
| 63 | 
            -
                """Analyze the complete crossword result for potential issues."""
         | 
| 64 | 
            -
                
         | 
| 65 | 
            -
                print("\n" + "=" * 70)
         | 
| 66 | 
            -
                print("CROSSWORD RESULT ANALYSIS")
         | 
| 67 | 
            -
                print("=" * 70)
         | 
| 68 | 
            -
                
         | 
| 69 | 
            -
                # Print basic metadata
         | 
| 70 | 
            -
                metadata = result.get("metadata", {})
         | 
| 71 | 
            -
                print("Metadata:")
         | 
| 72 | 
            -
                for key, value in metadata.items():
         | 
| 73 | 
            -
                    print(f"  {key}: {value}")
         | 
| 74 | 
            -
                
         | 
| 75 | 
            -
                # Analyze the grid
         | 
| 76 | 
            -
                grid = result.get("grid", [])
         | 
| 77 | 
            -
                print(f"\nGrid dimensions: {len(grid)}x{len(grid[0]) if grid else 0}")
         | 
| 78 | 
            -
                
         | 
| 79 | 
            -
                print("\nGrid layout:")
         | 
| 80 | 
            -
                print_numbered_grid(grid)
         | 
| 81 | 
            -
                
         | 
| 82 | 
            -
                # Analyze placed words vs clues
         | 
| 83 | 
            -
                clues = result.get("clues", [])
         | 
| 84 | 
            -
                print(f"\nNumber of clues generated: {len(clues)}")
         | 
| 85 | 
            -
                
         | 
| 86 | 
            -
                print("\nClue analysis:")
         | 
| 87 | 
            -
                for i, clue in enumerate(clues):
         | 
| 88 | 
            -
                    print(f"  Clue {i+1}:")
         | 
| 89 | 
            -
                    print(f"    Number: {clue.get('number', 'MISSING')}")
         | 
| 90 | 
            -
                    print(f"    Word: {clue.get('word', 'MISSING')}")
         | 
| 91 | 
            -
                    print(f"    Direction: {clue.get('direction', 'MISSING')}")
         | 
| 92 | 
            -
                    print(f"    Position: {clue.get('position', 'MISSING')}")
         | 
| 93 | 
            -
                    print(f"    Text: {clue.get('text', 'MISSING')}")
         | 
| 94 | 
            -
                
         | 
| 95 | 
            -
                # Check for potential issues
         | 
| 96 | 
            -
                print("\n" + "=" * 70)
         | 
| 97 | 
            -
                print("ISSUE DETECTION")
         | 
| 98 | 
            -
                print("=" * 70)
         | 
| 99 | 
            -
                
         | 
| 100 | 
            -
                check_word_boundary_consistency(grid, clues)
         | 
| 101 | 
            -
                check_numbering_consistency(clues)
         | 
| 102 | 
            -
                check_grid_word_alignment(grid, clues)
         | 
| 103 | 
            -
             | 
| 104 | 
            -
            def print_numbered_grid(grid):
         | 
| 105 | 
            -
                """Print grid with coordinates for analysis."""
         | 
| 106 | 
            -
                if not grid:
         | 
| 107 | 
            -
                    print("  Empty grid")
         | 
| 108 | 
            -
                    return
         | 
| 109 | 
            -
                
         | 
| 110 | 
            -
                # Print column headers
         | 
| 111 | 
            -
                print("    ", end="")
         | 
| 112 | 
            -
                for c in range(len(grid[0])):
         | 
| 113 | 
            -
                    print(f"{c:2d}", end="")
         | 
| 114 | 
            -
                print()
         | 
| 115 | 
            -
                
         | 
| 116 | 
            -
                # Print rows with row numbers
         | 
| 117 | 
            -
                for r in range(len(grid)):
         | 
| 118 | 
            -
                    print(f" {r:2d}: ", end="")
         | 
| 119 | 
            -
                    for c in range(len(grid[0])):
         | 
| 120 | 
            -
                        cell = grid[r][c]
         | 
| 121 | 
            -
                        if cell == ".":
         | 
| 122 | 
            -
                            print(" .", end="")
         | 
| 123 | 
            -
                        else:
         | 
| 124 | 
            -
                            print(f" {cell}", end="")
         | 
| 125 | 
            -
                    print()
         | 
| 126 | 
            -
             | 
| 127 | 
            -
            def check_word_boundary_consistency(grid, clues):
         | 
| 128 | 
            -
                """Check if words in clues match what's actually in the grid."""
         | 
| 129 | 
            -
                
         | 
| 130 | 
            -
                print("Checking word boundary consistency:")
         | 
| 131 | 
            -
                
         | 
| 132 | 
            -
                issues_found = []
         | 
| 133 | 
            -
                
         | 
| 134 | 
            -
                for clue in clues:
         | 
| 135 | 
            -
                    word = clue.get("word", "")
         | 
| 136 | 
            -
                    position = clue.get("position", {})
         | 
| 137 | 
            -
                    direction = clue.get("direction", "")
         | 
| 138 | 
            -
                    
         | 
| 139 | 
            -
                    if not all([word, position, direction]):
         | 
| 140 | 
            -
                        issues_found.append(f"Incomplete clue data: {clue}")
         | 
| 141 | 
            -
                        continue
         | 
| 142 | 
            -
                    
         | 
| 143 | 
            -
                    row = position.get("row", -1)
         | 
| 144 | 
            -
                    col = position.get("col", -1)
         | 
| 145 | 
            -
                    
         | 
| 146 | 
            -
                    if row < 0 or col < 0:
         | 
| 147 | 
            -
                        issues_found.append(f"Invalid position for word '{word}': {position}")
         | 
| 148 | 
            -
                        continue
         | 
| 149 | 
            -
                    
         | 
| 150 | 
            -
                    # Extract the actual word from the grid
         | 
| 151 | 
            -
                    grid_word = extract_word_from_grid(grid, row, col, direction, len(word))
         | 
| 152 | 
            -
                    
         | 
| 153 | 
            -
                    if grid_word != word:
         | 
| 154 | 
            -
                        issues_found.append(f"Mismatch for '{word}' at ({row}, {col}) {direction}: grid shows '{grid_word}'")
         | 
| 155 | 
            -
                
         | 
| 156 | 
            -
                if issues_found:
         | 
| 157 | 
            -
                    print("  ❌ Issues found:")
         | 
| 158 | 
            -
                    for issue in issues_found:
         | 
| 159 | 
            -
                        print(f"    {issue}")
         | 
| 160 | 
            -
                else:
         | 
| 161 | 
            -
                    print("  ✅ All words match grid positions")
         | 
| 162 | 
            -
             | 
| 163 | 
            -
            def extract_word_from_grid(grid, row, col, direction, expected_length):
         | 
| 164 | 
            -
                """Extract a word from the grid at the given position and direction."""
         | 
| 165 | 
            -
                
         | 
| 166 | 
            -
                if row >= len(grid) or col >= len(grid[0]):
         | 
| 167 | 
            -
                    return "OUT_OF_BOUNDS"
         | 
| 168 | 
            -
                
         | 
| 169 | 
            -
                word = ""
         | 
| 170 | 
            -
                
         | 
| 171 | 
            -
                if direction == "across":  # horizontal
         | 
| 172 | 
            -
                    for i in range(expected_length):
         | 
| 173 | 
            -
                        if col + i >= len(grid[0]):
         | 
| 174 | 
            -
                            return word + "TRUNCATED"
         | 
| 175 | 
            -
                        word += grid[row][col + i]
         | 
| 176 | 
            -
                
         | 
| 177 | 
            -
                elif direction == "down":  # vertical
         | 
| 178 | 
            -
                    for i in range(expected_length):
         | 
| 179 | 
            -
                        if row + i >= len(grid):
         | 
| 180 | 
            -
                            return word + "TRUNCATED"
         | 
| 181 | 
            -
                        word += grid[row + i][col]
         | 
| 182 | 
            -
                
         | 
| 183 | 
            -
                return word
         | 
| 184 | 
            -
             | 
| 185 | 
            -
            def check_numbering_consistency(clues):
         | 
| 186 | 
            -
                """Check if clue numbering is consistent and logical."""
         | 
| 187 | 
            -
                
         | 
| 188 | 
            -
                print("\nChecking numbering consistency:")
         | 
| 189 | 
            -
                
         | 
| 190 | 
            -
                numbers = [clue.get("number", -1) for clue in clues]
         | 
| 191 | 
            -
                issues = []
         | 
| 192 | 
            -
                
         | 
| 193 | 
            -
                # Check for duplicate numbers
         | 
| 194 | 
            -
                if len(numbers) != len(set(numbers)):
         | 
| 195 | 
            -
                    issues.append("Duplicate clue numbers found")
         | 
| 196 | 
            -
                
         | 
| 197 | 
            -
                # Check for missing numbers in sequence
         | 
| 198 | 
            -
                if numbers:
         | 
| 199 | 
            -
                    min_num = min(numbers)
         | 
| 200 | 
            -
                    max_num = max(numbers)
         | 
| 201 | 
            -
                    expected = set(range(min_num, max_num + 1))
         | 
| 202 | 
            -
                    actual = set(numbers)
         | 
| 203 | 
            -
                    
         | 
| 204 | 
            -
                    if expected != actual:
         | 
| 205 | 
            -
                        missing = expected - actual
         | 
| 206 | 
            -
                        extra = actual - expected
         | 
| 207 | 
            -
                        if missing:
         | 
| 208 | 
            -
                            issues.append(f"Missing numbers: {sorted(missing)}")
         | 
| 209 | 
            -
                        if extra:
         | 
| 210 | 
            -
                            issues.append(f"Extra numbers: {sorted(extra)}")
         | 
| 211 | 
            -
                
         | 
| 212 | 
            -
                if issues:
         | 
| 213 | 
            -
                    print("  ❌ Numbering issues:")
         | 
| 214 | 
            -
                    for issue in issues:
         | 
| 215 | 
            -
                        print(f"    {issue}")
         | 
| 216 | 
            -
                else:
         | 
| 217 | 
            -
                    print("  ✅ Numbering is consistent")
         | 
| 218 | 
            -
             | 
| 219 | 
            -
            def check_grid_word_alignment(grid, clues):
         | 
| 220 | 
            -
                """Check if all words are properly aligned and don't create unintended extensions."""
         | 
| 221 | 
            -
                
         | 
| 222 | 
            -
                print("\nChecking grid word alignment:")
         | 
| 223 | 
            -
                
         | 
| 224 | 
            -
                # Find all letter sequences in the grid
         | 
| 225 | 
            -
                horizontal_sequences = find_horizontal_sequences(grid)
         | 
| 226 | 
            -
                vertical_sequences = find_vertical_sequences(grid)
         | 
| 227 | 
            -
                
         | 
| 228 | 
            -
                print(f"  Found {len(horizontal_sequences)} horizontal sequences")
         | 
| 229 | 
            -
                print(f"  Found {len(vertical_sequences)} vertical sequences")
         | 
| 230 | 
            -
                
         | 
| 231 | 
            -
                # Check if each sequence corresponds to a clue
         | 
| 232 | 
            -
                clue_words = {}
         | 
| 233 | 
            -
                for clue in clues:
         | 
| 234 | 
            -
                    pos = clue.get("position", {})
         | 
| 235 | 
            -
                    key = (pos.get("row"), pos.get("col"), clue.get("direction"))
         | 
| 236 | 
            -
                    clue_words[key] = clue.get("word", "")
         | 
| 237 | 
            -
                
         | 
| 238 | 
            -
                issues = []
         | 
| 239 | 
            -
                
         | 
| 240 | 
            -
                # Check horizontal sequences
         | 
| 241 | 
            -
                for seq in horizontal_sequences:
         | 
| 242 | 
            -
                    row, start_col, word = seq
         | 
| 243 | 
            -
                    key = (row, start_col, "across")
         | 
| 244 | 
            -
                    if key not in clue_words:
         | 
| 245 | 
            -
                        issues.append(f"Unaccounted horizontal sequence: '{word}' at ({row}, {start_col})")
         | 
| 246 | 
            -
                    elif clue_words[key] != word:
         | 
| 247 | 
            -
                        issues.append(f"Mismatch: clue says '{clue_words[key]}' but grid shows '{word}' at ({row}, {start_col})")
         | 
| 248 | 
            -
                
         | 
| 249 | 
            -
                # Check vertical sequences
         | 
| 250 | 
            -
                for seq in vertical_sequences:
         | 
| 251 | 
            -
                    col, start_row, word = seq
         | 
| 252 | 
            -
                    key = (start_row, col, "down")
         | 
| 253 | 
            -
                    if key not in clue_words:
         | 
| 254 | 
            -
                        issues.append(f"Unaccounted vertical sequence: '{word}' at ({start_row}, {col})")
         | 
| 255 | 
            -
                    elif clue_words[key] != word:
         | 
| 256 | 
            -
                        issues.append(f"Mismatch: clue says '{clue_words[key]}' but grid shows '{word}' at ({start_row}, {col})")
         | 
| 257 | 
            -
                
         | 
| 258 | 
            -
                if issues:
         | 
| 259 | 
            -
                    print("  ❌ Alignment issues found:")
         | 
| 260 | 
            -
                    for issue in issues:
         | 
| 261 | 
            -
                        print(f"    {issue}")
         | 
| 262 | 
            -
                else:
         | 
| 263 | 
            -
                    print("  ✅ All words are properly aligned")
         | 
| 264 | 
            -
             | 
| 265 | 
            -
            def find_horizontal_sequences(grid):
         | 
| 266 | 
            -
                """Find all horizontal letter sequences of length > 1."""
         | 
| 267 | 
            -
                sequences = []
         | 
| 268 | 
            -
                
         | 
| 269 | 
            -
                for r in range(len(grid)):
         | 
| 270 | 
            -
                    current_word = ""
         | 
| 271 | 
            -
                    start_col = None
         | 
| 272 | 
            -
                    
         | 
| 273 | 
            -
                    for c in range(len(grid[0])):
         | 
| 274 | 
            -
                        if grid[r][c] != ".":
         | 
| 275 | 
            -
                            if start_col is None:
         | 
| 276 | 
            -
                                start_col = c
         | 
| 277 | 
            -
                            current_word += grid[r][c]
         | 
| 278 | 
            -
                        else:
         | 
| 279 | 
            -
                            if current_word and len(current_word) > 1:
         | 
| 280 | 
            -
                                sequences.append((r, start_col, current_word))
         | 
| 281 | 
            -
                            current_word = ""
         | 
| 282 | 
            -
                            start_col = None
         | 
| 283 | 
            -
                    
         | 
| 284 | 
            -
                    # Handle word at end of row
         | 
| 285 | 
            -
                    if current_word and len(current_word) > 1:
         | 
| 286 | 
            -
                        sequences.append((r, start_col, current_word))
         | 
| 287 | 
            -
                
         | 
| 288 | 
            -
                return sequences
         | 
| 289 | 
            -
             | 
| 290 | 
            -
            def find_vertical_sequences(grid):
         | 
| 291 | 
            -
                """Find all vertical letter sequences of length > 1."""
         | 
| 292 | 
            -
                sequences = []
         | 
| 293 | 
            -
                
         | 
| 294 | 
            -
                for c in range(len(grid[0])):
         | 
| 295 | 
            -
                    current_word = ""
         | 
| 296 | 
            -
                    start_row = None
         | 
| 297 | 
            -
                    
         | 
| 298 | 
            -
                    for r in range(len(grid)):
         | 
| 299 | 
            -
                        if grid[r][c] != ".":
         | 
| 300 | 
            -
                            if start_row is None:
         | 
| 301 | 
            -
                                start_row = r
         | 
| 302 | 
            -
                            current_word += grid[r][c]
         | 
| 303 | 
            -
                        else:
         | 
| 304 | 
            -
                            if current_word and len(current_word) > 1:
         | 
| 305 | 
            -
                                sequences.append((c, start_row, current_word))
         | 
| 306 | 
            -
                            current_word = ""
         | 
| 307 | 
            -
                            start_row = None
         | 
| 308 | 
            -
                    
         | 
| 309 | 
            -
                    # Handle word at end of column
         | 
| 310 | 
            -
                    if current_word and len(current_word) > 1:
         | 
| 311 | 
            -
                        sequences.append((c, start_row, current_word))
         | 
| 312 | 
            -
                
         | 
| 313 | 
            -
                return sequences
         | 
| 314 | 
            -
             | 
| 315 | 
            -
            if __name__ == "__main__":
         | 
| 316 | 
            -
                asyncio.run(debug_complete_generation())
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/debug_grid_direct.py
    DELETED
    
    | @@ -1,293 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
            """
         | 
| 3 | 
            -
            Direct grid generation test to identify word boundary/display issues.
         | 
| 4 | 
            -
            """
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            import sys
         | 
| 7 | 
            -
            from pathlib import Path
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            # Add project root to path
         | 
| 10 | 
            -
            project_root = Path(__file__).parent
         | 
| 11 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 12 | 
            -
             | 
| 13 | 
            -
            from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            def test_direct_grid_generation():
         | 
| 16 | 
            -
                """Test grid generation directly with controlled words."""
         | 
| 17 | 
            -
                
         | 
| 18 | 
            -
                print("🔍 Direct Grid Generation Test\n")
         | 
| 19 | 
            -
                
         | 
| 20 | 
            -
                generator = CrosswordGeneratorFixed(vector_service=None)
         | 
| 21 | 
            -
                
         | 
| 22 | 
            -
                # Test words that might cause the issues seen in the images
         | 
| 23 | 
            -
                test_words = [
         | 
| 24 | 
            -
                    {"word": "MACHINE", "clue": "Device with moving parts"},
         | 
| 25 | 
            -
                    {"word": "COMPUTER", "clue": "Electronic device"}, 
         | 
| 26 | 
            -
                    {"word": "EXPERT", "clue": "Person with specialized knowledge"},
         | 
| 27 | 
            -
                    {"word": "SCIENCE", "clue": "Systematic study"},
         | 
| 28 | 
            -
                    {"word": "CAMERA", "clue": "Device for taking photos"},
         | 
| 29 | 
            -
                    {"word": "METHOD", "clue": "Systematic approach"}
         | 
| 30 | 
            -
                ]
         | 
| 31 | 
            -
                
         | 
| 32 | 
            -
                print("=" * 60)
         | 
| 33 | 
            -
                print("TEST 1: Direct grid creation")
         | 
| 34 | 
            -
                print("=" * 60)
         | 
| 35 | 
            -
                
         | 
| 36 | 
            -
                # Test the _create_grid method directly
         | 
| 37 | 
            -
                result = generator._create_grid(test_words)
         | 
| 38 | 
            -
                
         | 
| 39 | 
            -
                if result:
         | 
| 40 | 
            -
                    print("✅ Grid generation successful!")
         | 
| 41 | 
            -
                    
         | 
| 42 | 
            -
                    grid = result["grid"]
         | 
| 43 | 
            -
                    placed_words = result["placed_words"]
         | 
| 44 | 
            -
                    clues = result["clues"]
         | 
| 45 | 
            -
                    
         | 
| 46 | 
            -
                    print(f"Grid size: {len(grid)}x{len(grid[0])}")
         | 
| 47 | 
            -
                    print(f"Words placed: {len(placed_words)}")
         | 
| 48 | 
            -
                    print(f"Clues generated: {len(clues)}")
         | 
| 49 | 
            -
                    
         | 
| 50 | 
            -
                    # Print the grid
         | 
| 51 | 
            -
                    print("\nGenerated Grid:")
         | 
| 52 | 
            -
                    print_grid_with_coordinates(grid)
         | 
| 53 | 
            -
                    
         | 
| 54 | 
            -
                    # Print placed words details
         | 
| 55 | 
            -
                    print("\nPlaced Words:")
         | 
| 56 | 
            -
                    for i, word_info in enumerate(placed_words):
         | 
| 57 | 
            -
                        print(f"  {i+1}. {word_info['word']} at ({word_info['row']}, {word_info['col']}) {word_info['direction']}")
         | 
| 58 | 
            -
                    
         | 
| 59 | 
            -
                    # Print clues
         | 
| 60 | 
            -
                    print("\nGenerated Clues:")
         | 
| 61 | 
            -
                    for clue in clues:
         | 
| 62 | 
            -
                        print(f"  {clue['number']}. {clue['direction']}: {clue['word']} - {clue['text']}")
         | 
| 63 | 
            -
                    
         | 
| 64 | 
            -
                    # Analyze for potential issues
         | 
| 65 | 
            -
                    print("\n" + "=" * 60)
         | 
| 66 | 
            -
                    print("ANALYSIS")
         | 
| 67 | 
            -
                    print("=" * 60)
         | 
| 68 | 
            -
                    
         | 
| 69 | 
            -
                    analyze_grid_issues(grid, placed_words, clues)
         | 
| 70 | 
            -
                
         | 
| 71 | 
            -
                else:
         | 
| 72 | 
            -
                    print("❌ Grid generation failed")
         | 
| 73 | 
            -
                
         | 
| 74 | 
            -
                # Test another scenario that might reproduce the image issues
         | 
| 75 | 
            -
                print("\n" + "=" * 60)
         | 
| 76 | 
            -
                print("TEST 2: Scenario with potential extension words")
         | 
| 77 | 
            -
                print("=" * 60)
         | 
| 78 | 
            -
                
         | 
| 79 | 
            -
                # Words that might create the "MACHINERY" type issue
         | 
| 80 | 
            -
                extension_words = [
         | 
| 81 | 
            -
                    {"word": "MACHINE", "clue": "Device with moving parts"},
         | 
| 82 | 
            -
                    {"word": "MACHINERY", "clue": "Mechanical equipment"},  # Might cause confusion
         | 
| 83 | 
            -
                    {"word": "EXPERT", "clue": "Specialist"},
         | 
| 84 | 
            -
                    {"word": "TECHNOLOGY", "clue": "Applied science"},
         | 
| 85 | 
            -
                ]
         | 
| 86 | 
            -
                
         | 
| 87 | 
            -
                result2 = generator._create_grid(extension_words)
         | 
| 88 | 
            -
                
         | 
| 89 | 
            -
                if result2:
         | 
| 90 | 
            -
                    print("✅ Extension test grid generated!")
         | 
| 91 | 
            -
                    
         | 
| 92 | 
            -
                    grid2 = result2["grid"]
         | 
| 93 | 
            -
                    placed_words2 = result2["placed_words"]
         | 
| 94 | 
            -
                    
         | 
| 95 | 
            -
                    print("\nExtension Test Grid:")
         | 
| 96 | 
            -
                    print_grid_with_coordinates(grid2)
         | 
| 97 | 
            -
                    
         | 
| 98 | 
            -
                    print("\nPlaced Words:")
         | 
| 99 | 
            -
                    for i, word_info in enumerate(placed_words2):
         | 
| 100 | 
            -
                        print(f"  {i+1}. {word_info['word']} at ({word_info['row']}, {word_info['col']}) {word_info['direction']}")
         | 
| 101 | 
            -
                    
         | 
| 102 | 
            -
                    # Check specifically for MACHINE vs MACHINERY issues
         | 
| 103 | 
            -
                    check_machine_machinery_issue(grid2, placed_words2)
         | 
| 104 | 
            -
                
         | 
| 105 | 
            -
                else:
         | 
| 106 | 
            -
                    print("❌ Extension test grid generation failed")
         | 
| 107 | 
            -
             | 
| 108 | 
            -
            def print_grid_with_coordinates(grid):
         | 
| 109 | 
            -
                """Print grid with row and column coordinates."""
         | 
| 110 | 
            -
                if not grid:
         | 
| 111 | 
            -
                    print("  Empty grid")
         | 
| 112 | 
            -
                    return
         | 
| 113 | 
            -
                
         | 
| 114 | 
            -
                # Print column headers
         | 
| 115 | 
            -
                print("     ", end="")
         | 
| 116 | 
            -
                for c in range(len(grid[0])):
         | 
| 117 | 
            -
                    print(f"{c:2d}", end="")
         | 
| 118 | 
            -
                print()
         | 
| 119 | 
            -
                
         | 
| 120 | 
            -
                # Print rows
         | 
| 121 | 
            -
                for r in range(len(grid)):
         | 
| 122 | 
            -
                    print(f" {r:2d}: ", end="")
         | 
| 123 | 
            -
                    for c in range(len(grid[0])):
         | 
| 124 | 
            -
                        cell = grid[r][c]
         | 
| 125 | 
            -
                        if cell == ".":
         | 
| 126 | 
            -
                            print(" .", end="")
         | 
| 127 | 
            -
                        else:
         | 
| 128 | 
            -
                            print(f" {cell}", end="")
         | 
| 129 | 
            -
                    print()
         | 
| 130 | 
            -
             | 
| 131 | 
            -
            def analyze_grid_issues(grid, placed_words, clues):
         | 
| 132 | 
            -
                """Analyze the grid for potential boundary/display issues."""
         | 
| 133 | 
            -
                
         | 
| 134 | 
            -
                print("Checking for potential issues...")
         | 
| 135 | 
            -
                
         | 
| 136 | 
            -
                issues = []
         | 
| 137 | 
            -
                
         | 
| 138 | 
            -
                # Check 1: Verify each placed word actually exists in the grid
         | 
| 139 | 
            -
                for word_info in placed_words:
         | 
| 140 | 
            -
                    word = word_info["word"]
         | 
| 141 | 
            -
                    row = word_info["row"]
         | 
| 142 | 
            -
                    col = word_info["col"]
         | 
| 143 | 
            -
                    direction = word_info["direction"]
         | 
| 144 | 
            -
                    
         | 
| 145 | 
            -
                    grid_word = extract_word_from_grid(grid, row, col, direction, len(word))
         | 
| 146 | 
            -
                    
         | 
| 147 | 
            -
                    if grid_word != word:
         | 
| 148 | 
            -
                        issues.append(f"Word mismatch: '{word}' expected at ({row},{col}) {direction}, but grid shows '{grid_word}'")
         | 
| 149 | 
            -
                
         | 
| 150 | 
            -
                # Check 2: Look for unintended letter sequences
         | 
| 151 | 
            -
                all_sequences = find_all_letter_sequences(grid)
         | 
| 152 | 
            -
                intended_words = {(w["row"], w["col"], w["direction"]): w["word"] for w in placed_words}
         | 
| 153 | 
            -
                
         | 
| 154 | 
            -
                for seq_info in all_sequences:
         | 
| 155 | 
            -
                    row, col, direction, seq_word = seq_info
         | 
| 156 | 
            -
                    key = (row, col, direction)
         | 
| 157 | 
            -
                    
         | 
| 158 | 
            -
                    if key not in intended_words:
         | 
| 159 | 
            -
                        if len(seq_word) > 1:  # Only care about multi-letter sequences
         | 
| 160 | 
            -
                            issues.append(f"Unintended sequence: '{seq_word}' at ({row},{col}) {direction}")
         | 
| 161 | 
            -
                    elif intended_words[key] != seq_word:
         | 
| 162 | 
            -
                        issues.append(f"Sequence mismatch: expected '{intended_words[key]}' but found '{seq_word}' at ({row},{col}) {direction}")
         | 
| 163 | 
            -
                
         | 
| 164 | 
            -
                # Check 3: Verify clue consistency
         | 
| 165 | 
            -
                for clue in clues:
         | 
| 166 | 
            -
                    clue_word = clue["word"]
         | 
| 167 | 
            -
                    pos = clue["position"]
         | 
| 168 | 
            -
                    clue_row = pos["row"]
         | 
| 169 | 
            -
                    clue_col = pos["col"]
         | 
| 170 | 
            -
                    clue_direction = clue["direction"]
         | 
| 171 | 
            -
                    
         | 
| 172 | 
            -
                    # Convert direction format if needed
         | 
| 173 | 
            -
                    direction_map = {"across": "horizontal", "down": "vertical"}
         | 
| 174 | 
            -
                    normalized_direction = direction_map.get(clue_direction, clue_direction)
         | 
| 175 | 
            -
                    
         | 
| 176 | 
            -
                    grid_word = extract_word_from_grid(grid, clue_row, clue_col, normalized_direction, len(clue_word))
         | 
| 177 | 
            -
                    
         | 
| 178 | 
            -
                    if grid_word != clue_word:
         | 
| 179 | 
            -
                        issues.append(f"Clue mismatch: clue says '{clue_word}' at ({clue_row},{clue_col}) {clue_direction}, but grid shows '{grid_word}'")
         | 
| 180 | 
            -
                
         | 
| 181 | 
            -
                # Report results
         | 
| 182 | 
            -
                if issues:
         | 
| 183 | 
            -
                    print("❌ Issues found:")
         | 
| 184 | 
            -
                    for issue in issues:
         | 
| 185 | 
            -
                        print(f"  {issue}")
         | 
| 186 | 
            -
                else:
         | 
| 187 | 
            -
                    print("✅ No issues detected - grid appears consistent")
         | 
| 188 | 
            -
             | 
| 189 | 
            -
            def extract_word_from_grid(grid, row, col, direction, expected_length):
         | 
| 190 | 
            -
                """Extract word from grid at given position and direction."""
         | 
| 191 | 
            -
                if row >= len(grid) or col >= len(grid[0]) or row < 0 or col < 0:
         | 
| 192 | 
            -
                    return "OUT_OF_BOUNDS"
         | 
| 193 | 
            -
                
         | 
| 194 | 
            -
                word = ""
         | 
| 195 | 
            -
                
         | 
| 196 | 
            -
                if direction in ["horizontal", "across"]:
         | 
| 197 | 
            -
                    for i in range(expected_length):
         | 
| 198 | 
            -
                        if col + i >= len(grid[0]):
         | 
| 199 | 
            -
                            return word + "[TRUNCATED]"
         | 
| 200 | 
            -
                        word += grid[row][col + i]
         | 
| 201 | 
            -
                elif direction in ["vertical", "down"]:
         | 
| 202 | 
            -
                    for i in range(expected_length):
         | 
| 203 | 
            -
                        if row + i >= len(grid):
         | 
| 204 | 
            -
                            return word + "[TRUNCATED]"
         | 
| 205 | 
            -
                        word += grid[row + i][col]
         | 
| 206 | 
            -
                
         | 
| 207 | 
            -
                return word
         | 
| 208 | 
            -
             | 
| 209 | 
            -
            def find_all_letter_sequences(grid):
         | 
| 210 | 
            -
                """Find all letter sequences (horizontal and vertical) in the grid."""
         | 
| 211 | 
            -
                sequences = []
         | 
| 212 | 
            -
                
         | 
| 213 | 
            -
                # Horizontal sequences
         | 
| 214 | 
            -
                for r in range(len(grid)):
         | 
| 215 | 
            -
                    current_word = ""
         | 
| 216 | 
            -
                    start_col = None
         | 
| 217 | 
            -
                    
         | 
| 218 | 
            -
                    for c in range(len(grid[0])):
         | 
| 219 | 
            -
                        if grid[r][c] != ".":
         | 
| 220 | 
            -
                            if start_col is None:
         | 
| 221 | 
            -
                                start_col = c
         | 
| 222 | 
            -
                            current_word += grid[r][c]
         | 
| 223 | 
            -
                        else:
         | 
| 224 | 
            -
                            if current_word and len(current_word) > 1:
         | 
| 225 | 
            -
                                sequences.append((r, start_col, "horizontal", current_word))
         | 
| 226 | 
            -
                            current_word = ""
         | 
| 227 | 
            -
                            start_col = None
         | 
| 228 | 
            -
                    
         | 
| 229 | 
            -
                    # Handle end of row
         | 
| 230 | 
            -
                    if current_word and len(current_word) > 1:
         | 
| 231 | 
            -
                        sequences.append((r, start_col, "horizontal", current_word))
         | 
| 232 | 
            -
                
         | 
| 233 | 
            -
                # Vertical sequences
         | 
| 234 | 
            -
                for c in range(len(grid[0])):
         | 
| 235 | 
            -
                    current_word = ""
         | 
| 236 | 
            -
                    start_row = None
         | 
| 237 | 
            -
                    
         | 
| 238 | 
            -
                    for r in range(len(grid)):
         | 
| 239 | 
            -
                        if grid[r][c] != ".":
         | 
| 240 | 
            -
                            if start_row is None:
         | 
| 241 | 
            -
                                start_row = r
         | 
| 242 | 
            -
                            current_word += grid[r][c]
         | 
| 243 | 
            -
                        else:
         | 
| 244 | 
            -
                            if current_word and len(current_word) > 1:
         | 
| 245 | 
            -
                                sequences.append((start_row, c, "vertical", current_word))
         | 
| 246 | 
            -
                            current_word = ""
         | 
| 247 | 
            -
                            start_row = None
         | 
| 248 | 
            -
                    
         | 
| 249 | 
            -
                    # Handle end of column
         | 
| 250 | 
            -
                    if current_word and len(current_word) > 1:
         | 
| 251 | 
            -
                        sequences.append((start_row, c, "vertical", current_word))
         | 
| 252 | 
            -
                
         | 
| 253 | 
            -
                return sequences
         | 
| 254 | 
            -
             | 
| 255 | 
            -
            def check_machine_machinery_issue(grid, placed_words):
         | 
| 256 | 
            -
                """Specifically check for MACHINE vs MACHINERY confusion."""
         | 
| 257 | 
            -
                
         | 
| 258 | 
            -
                print("\nChecking for MACHINE/MACHINERY issue:")
         | 
| 259 | 
            -
                
         | 
| 260 | 
            -
                machine_words = [w for w in placed_words if "MACHINE" in w["word"]]
         | 
| 261 | 
            -
                
         | 
| 262 | 
            -
                if not machine_words:
         | 
| 263 | 
            -
                    print("  No MACHINE-related words found")
         | 
| 264 | 
            -
                    return
         | 
| 265 | 
            -
                
         | 
| 266 | 
            -
                for word_info in machine_words:
         | 
| 267 | 
            -
                    word = word_info["word"]
         | 
| 268 | 
            -
                    row = word_info["row"]
         | 
| 269 | 
            -
                    col = word_info["col"]
         | 
| 270 | 
            -
                    direction = word_info["direction"]
         | 
| 271 | 
            -
                    
         | 
| 272 | 
            -
                    print(f"  Found: '{word}' at ({row},{col}) {direction}")
         | 
| 273 | 
            -
                    
         | 
| 274 | 
            -
                    # Check what's actually in the grid at this location
         | 
| 275 | 
            -
                    grid_word = extract_word_from_grid(grid, row, col, direction, len(word))
         | 
| 276 | 
            -
                    print(f"    Grid shows: '{grid_word}'")
         | 
| 277 | 
            -
                    
         | 
| 278 | 
            -
                    # Check if there are extra letters that might create confusion
         | 
| 279 | 
            -
                    if direction == "horizontal":
         | 
| 280 | 
            -
                        # Check for letters after the word
         | 
| 281 | 
            -
                        end_col = col + len(word)
         | 
| 282 | 
            -
                        if end_col < len(grid[0]) and grid[row][end_col] != ".":
         | 
| 283 | 
            -
                            extra_letters = ""
         | 
| 284 | 
            -
                            check_col = end_col
         | 
| 285 | 
            -
                            while check_col < len(grid[0]) and grid[row][check_col] != ".":
         | 
| 286 | 
            -
                                extra_letters += grid[row][check_col]
         | 
| 287 | 
            -
                                check_col += 1
         | 
| 288 | 
            -
                            if extra_letters:
         | 
| 289 | 
            -
                                print(f"    ⚠️ Extra letters after word: '{extra_letters}'")
         | 
| 290 | 
            -
                                print(f"    This might make '{word}' appear as '{word + extra_letters}'")
         | 
| 291 | 
            -
             | 
| 292 | 
            -
            if __name__ == "__main__":
         | 
| 293 | 
            -
                test_direct_grid_generation()
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/debug_index_error.py
    DELETED
    
    | @@ -1,307 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
            """
         | 
| 3 | 
            -
            Debug the recurring index error by adding comprehensive bounds checking.
         | 
| 4 | 
            -
            """
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            import asyncio
         | 
| 7 | 
            -
            import sys
         | 
| 8 | 
            -
            import logging
         | 
| 9 | 
            -
            from pathlib import Path
         | 
| 10 | 
            -
             | 
| 11 | 
            -
            # Add project root to path
         | 
| 12 | 
            -
            project_root = Path(__file__).parent
         | 
| 13 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
         | 
| 16 | 
            -
            from src.services.vector_search import VectorSearchService
         | 
| 17 | 
            -
             | 
| 18 | 
            -
            # Enable debug logging
         | 
| 19 | 
            -
            logging.basicConfig(level=logging.DEBUG)
         | 
| 20 | 
            -
            logger = logging.getLogger(__name__)
         | 
| 21 | 
            -
             | 
| 22 | 
            -
            class DebugCrosswordGenerator(CrosswordGeneratorFixed):
         | 
| 23 | 
            -
                """Debug version with comprehensive bounds checking."""
         | 
| 24 | 
            -
                
         | 
| 25 | 
            -
                def _can_place_word(self, grid, word, row, col, direction):
         | 
| 26 | 
            -
                    """Enhanced _can_place_word with comprehensive bounds checking."""
         | 
| 27 | 
            -
                    try:
         | 
| 28 | 
            -
                        size = len(grid)
         | 
| 29 | 
            -
                        logger.debug(f"_can_place_word: word={word}, row={row}, col={col}, direction={direction}, grid_size={size}")
         | 
| 30 | 
            -
                        
         | 
| 31 | 
            -
                        # Check initial boundaries
         | 
| 32 | 
            -
                        if row < 0 or col < 0 or row >= size or col >= size:
         | 
| 33 | 
            -
                            logger.debug(f"Initial bounds check failed: row={row}, col={col}, size={size}")
         | 
| 34 | 
            -
                            return False
         | 
| 35 | 
            -
                        
         | 
| 36 | 
            -
                        if direction == "horizontal":
         | 
| 37 | 
            -
                            if col + len(word) > size:
         | 
| 38 | 
            -
                                logger.debug(f"Horizontal bounds check failed: col+len(word)={col + len(word)} > size={size}")
         | 
| 39 | 
            -
                                return False
         | 
| 40 | 
            -
                            
         | 
| 41 | 
            -
                            # Check word boundaries (no adjacent letters) - with bounds check
         | 
| 42 | 
            -
                            if col > 0:
         | 
| 43 | 
            -
                                if row >= size or col - 1 >= size or row < 0 or col - 1 < 0:
         | 
| 44 | 
            -
                                    logger.debug(f"Horizontal left boundary check failed: row={row}, col-1={col-1}, size={size}")
         | 
| 45 | 
            -
                                    return False
         | 
| 46 | 
            -
                                if grid[row][col - 1] != ".":
         | 
| 47 | 
            -
                                    logger.debug(f"Horizontal left boundary has adjacent letter")
         | 
| 48 | 
            -
                                    return False
         | 
| 49 | 
            -
                            
         | 
| 50 | 
            -
                            if col + len(word) < size:
         | 
| 51 | 
            -
                                if row >= size or col + len(word) >= size or row < 0 or col + len(word) < 0:
         | 
| 52 | 
            -
                                    logger.debug(f"Horizontal right boundary check failed: row={row}, col+len={col + len(word)}, size={size}")
         | 
| 53 | 
            -
                                    return False
         | 
| 54 | 
            -
                                if grid[row][col + len(word)] != ".":
         | 
| 55 | 
            -
                                    logger.debug(f"Horizontal right boundary has adjacent letter")
         | 
| 56 | 
            -
                                    return False
         | 
| 57 | 
            -
                            
         | 
| 58 | 
            -
                            # Check each letter position
         | 
| 59 | 
            -
                            for i, letter in enumerate(word):
         | 
| 60 | 
            -
                                check_row = row
         | 
| 61 | 
            -
                                check_col = col + i
         | 
| 62 | 
            -
                                if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 63 | 
            -
                                    logger.debug(f"Horizontal letter position check failed: letter {i}, row={check_row}, col={check_col}, size={size}")
         | 
| 64 | 
            -
                                    return False
         | 
| 65 | 
            -
                                current_cell = grid[check_row][check_col]
         | 
| 66 | 
            -
                                if current_cell != "." and current_cell != letter:
         | 
| 67 | 
            -
                                    logger.debug(f"Horizontal letter conflict: expected {letter}, found {current_cell}")
         | 
| 68 | 
            -
                                    return False
         | 
| 69 | 
            -
                        
         | 
| 70 | 
            -
                        else:  # vertical
         | 
| 71 | 
            -
                            if row + len(word) > size:
         | 
| 72 | 
            -
                                logger.debug(f"Vertical bounds check failed: row+len(word)={row + len(word)} > size={size}")
         | 
| 73 | 
            -
                                return False
         | 
| 74 | 
            -
                            
         | 
| 75 | 
            -
                            # Check word boundaries - with bounds check
         | 
| 76 | 
            -
                            if row > 0:
         | 
| 77 | 
            -
                                if row - 1 >= size or col >= size or row - 1 < 0 or col < 0:
         | 
| 78 | 
            -
                                    logger.debug(f"Vertical top boundary check failed: row-1={row-1}, col={col}, size={size}")
         | 
| 79 | 
            -
                                    return False
         | 
| 80 | 
            -
                                if grid[row - 1][col] != ".":
         | 
| 81 | 
            -
                                    logger.debug(f"Vertical top boundary has adjacent letter")
         | 
| 82 | 
            -
                                    return False
         | 
| 83 | 
            -
                            
         | 
| 84 | 
            -
                            if row + len(word) < size:
         | 
| 85 | 
            -
                                if row + len(word) >= size or col >= size or row + len(word) < 0 or col < 0:
         | 
| 86 | 
            -
                                    logger.debug(f"Vertical bottom boundary check failed: row+len={row + len(word)}, col={col}, size={size}")
         | 
| 87 | 
            -
                                    return False
         | 
| 88 | 
            -
                                if grid[row + len(word)][col] != ".":
         | 
| 89 | 
            -
                                    logger.debug(f"Vertical bottom boundary has adjacent letter")
         | 
| 90 | 
            -
                                    return False
         | 
| 91 | 
            -
                            
         | 
| 92 | 
            -
                            # Check each letter position
         | 
| 93 | 
            -
                            for i, letter in enumerate(word):
         | 
| 94 | 
            -
                                check_row = row + i
         | 
| 95 | 
            -
                                check_col = col
         | 
| 96 | 
            -
                                if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 97 | 
            -
                                    logger.debug(f"Vertical letter position check failed: letter {i}, row={check_row}, col={check_col}, size={size}")
         | 
| 98 | 
            -
                                    return False
         | 
| 99 | 
            -
                                current_cell = grid[check_row][check_col]
         | 
| 100 | 
            -
                                if current_cell != "." and current_cell != letter:
         | 
| 101 | 
            -
                                    logger.debug(f"Vertical letter conflict: expected {letter}, found {current_cell}")
         | 
| 102 | 
            -
                                    return False
         | 
| 103 | 
            -
                        
         | 
| 104 | 
            -
                        logger.debug(f"_can_place_word: SUCCESS for word={word}")
         | 
| 105 | 
            -
                        return True
         | 
| 106 | 
            -
                        
         | 
| 107 | 
            -
                    except Exception as e:
         | 
| 108 | 
            -
                        logger.error(f"❌ ERROR in _can_place_word: {e}")
         | 
| 109 | 
            -
                        logger.error(f"   word={word}, row={row}, col={col}, direction={direction}")
         | 
| 110 | 
            -
                        logger.error(f"   grid_size={len(grid) if grid else 'None'}")
         | 
| 111 | 
            -
                        import traceback
         | 
| 112 | 
            -
                        traceback.print_exc()
         | 
| 113 | 
            -
                        return False
         | 
| 114 | 
            -
                
         | 
| 115 | 
            -
                def _place_word(self, grid, word, row, col, direction):
         | 
| 116 | 
            -
                    """Enhanced _place_word with comprehensive bounds checking."""
         | 
| 117 | 
            -
                    try:
         | 
| 118 | 
            -
                        size = len(grid)
         | 
| 119 | 
            -
                        logger.debug(f"_place_word: word={word}, row={row}, col={col}, direction={direction}, grid_size={size}")
         | 
| 120 | 
            -
                        
         | 
| 121 | 
            -
                        original_state = []
         | 
| 122 | 
            -
                        
         | 
| 123 | 
            -
                        if direction == "horizontal":
         | 
| 124 | 
            -
                            for i, letter in enumerate(word):
         | 
| 125 | 
            -
                                check_row = row
         | 
| 126 | 
            -
                                check_col = col + i
         | 
| 127 | 
            -
                                if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 128 | 
            -
                                    logger.error(f"❌ _place_word horizontal bounds error: row={check_row}, col={check_col}, size={size}")
         | 
| 129 | 
            -
                                    raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
         | 
| 130 | 
            -
                                
         | 
| 131 | 
            -
                                original_state.append({
         | 
| 132 | 
            -
                                    "row": check_row,
         | 
| 133 | 
            -
                                    "col": check_col,
         | 
| 134 | 
            -
                                    "value": grid[check_row][check_col]
         | 
| 135 | 
            -
                                })
         | 
| 136 | 
            -
                                grid[check_row][check_col] = letter
         | 
| 137 | 
            -
                        else:
         | 
| 138 | 
            -
                            for i, letter in enumerate(word):
         | 
| 139 | 
            -
                                check_row = row + i
         | 
| 140 | 
            -
                                check_col = col
         | 
| 141 | 
            -
                                if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 142 | 
            -
                                    logger.error(f"❌ _place_word vertical bounds error: row={check_row}, col={check_col}, size={size}")
         | 
| 143 | 
            -
                                    raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
         | 
| 144 | 
            -
                                
         | 
| 145 | 
            -
                                original_state.append({
         | 
| 146 | 
            -
                                    "row": check_row,
         | 
| 147 | 
            -
                                    "col": check_col,
         | 
| 148 | 
            -
                                    "value": grid[check_row][check_col]
         | 
| 149 | 
            -
                                })
         | 
| 150 | 
            -
                                grid[check_row][check_col] = letter
         | 
| 151 | 
            -
                        
         | 
| 152 | 
            -
                        logger.debug(f"_place_word: SUCCESS for word={word}")
         | 
| 153 | 
            -
                        return original_state
         | 
| 154 | 
            -
                        
         | 
| 155 | 
            -
                    except Exception as e:
         | 
| 156 | 
            -
                        logger.error(f"❌ ERROR in _place_word: {e}")
         | 
| 157 | 
            -
                        logger.error(f"   word={word}, row={row}, col={col}, direction={direction}")
         | 
| 158 | 
            -
                        logger.error(f"   grid_size={len(grid) if grid else 'None'}")
         | 
| 159 | 
            -
                        import traceback
         | 
| 160 | 
            -
                        traceback.print_exc()
         | 
| 161 | 
            -
                        raise
         | 
| 162 | 
            -
                
         | 
| 163 | 
            -
                def _remove_word(self, grid, original_state):
         | 
| 164 | 
            -
                    """Enhanced _remove_word with comprehensive bounds checking."""
         | 
| 165 | 
            -
                    try:
         | 
| 166 | 
            -
                        size = len(grid)
         | 
| 167 | 
            -
                        logger.debug(f"_remove_word: restoring {len(original_state)} positions, grid_size={size}")
         | 
| 168 | 
            -
                        
         | 
| 169 | 
            -
                        for state in original_state:
         | 
| 170 | 
            -
                            check_row = state["row"]
         | 
| 171 | 
            -
                            check_col = state["col"]
         | 
| 172 | 
            -
                            if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 173 | 
            -
                                logger.error(f"❌ _remove_word bounds error: row={check_row}, col={check_col}, size={size}")
         | 
| 174 | 
            -
                                raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
         | 
| 175 | 
            -
                            
         | 
| 176 | 
            -
                            grid[check_row][check_col] = state["value"]
         | 
| 177 | 
            -
                        
         | 
| 178 | 
            -
                        logger.debug(f"_remove_word: SUCCESS")
         | 
| 179 | 
            -
                        
         | 
| 180 | 
            -
                    except Exception as e:
         | 
| 181 | 
            -
                        logger.error(f"❌ ERROR in _remove_word: {e}")
         | 
| 182 | 
            -
                        logger.error(f"   grid_size={len(grid) if grid else 'None'}")
         | 
| 183 | 
            -
                        logger.error(f"   original_state={original_state}")
         | 
| 184 | 
            -
                        import traceback
         | 
| 185 | 
            -
                        traceback.print_exc()
         | 
| 186 | 
            -
                        raise
         | 
| 187 | 
            -
                
         | 
| 188 | 
            -
                def _create_simple_cross(self, word_list, word_objs):
         | 
| 189 | 
            -
                    """Enhanced _create_simple_cross with comprehensive bounds checking."""
         | 
| 190 | 
            -
                    try:
         | 
| 191 | 
            -
                        logger.debug(f"_create_simple_cross: words={word_list}")
         | 
| 192 | 
            -
                        
         | 
| 193 | 
            -
                        if len(word_list) < 2:
         | 
| 194 | 
            -
                            logger.debug("Not enough words for simple cross")
         | 
| 195 | 
            -
                            return None
         | 
| 196 | 
            -
                        
         | 
| 197 | 
            -
                        word1, word2 = word_list[0], word_list[1]
         | 
| 198 | 
            -
                        intersections = self._find_word_intersections(word1, word2)
         | 
| 199 | 
            -
                        
         | 
| 200 | 
            -
                        if not intersections:
         | 
| 201 | 
            -
                            logger.debug("No intersections found")
         | 
| 202 | 
            -
                            return None
         | 
| 203 | 
            -
                        
         | 
| 204 | 
            -
                        # Use first intersection
         | 
| 205 | 
            -
                        intersection = intersections[0]
         | 
| 206 | 
            -
                        size = max(len(word1), len(word2)) + 4
         | 
| 207 | 
            -
                        logger.debug(f"Creating grid of size {size} for simple cross")
         | 
| 208 | 
            -
                        
         | 
| 209 | 
            -
                        grid = [["." for _ in range(size)] for _ in range(size)]
         | 
| 210 | 
            -
                        
         | 
| 211 | 
            -
                        # Place first word horizontally in center
         | 
| 212 | 
            -
                        center_row = size // 2
         | 
| 213 | 
            -
                        center_col = (size - len(word1)) // 2
         | 
| 214 | 
            -
                        
         | 
| 215 | 
            -
                        logger.debug(f"Placing word1 '{word1}' at row={center_row}, col={center_col}")
         | 
| 216 | 
            -
                        
         | 
| 217 | 
            -
                        for i, letter in enumerate(word1):
         | 
| 218 | 
            -
                            check_row = center_row
         | 
| 219 | 
            -
                            check_col = center_col + i
         | 
| 220 | 
            -
                            if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 221 | 
            -
                                logger.error(f"❌ _create_simple_cross word1 bounds error: row={check_row}, col={check_col}, size={size}")
         | 
| 222 | 
            -
                                raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
         | 
| 223 | 
            -
                            grid[check_row][check_col] = letter
         | 
| 224 | 
            -
                        
         | 
| 225 | 
            -
                        # Place second word vertically at intersection
         | 
| 226 | 
            -
                        intersection_col = center_col + intersection["word_pos"]
         | 
| 227 | 
            -
                        word2_start_row = center_row - intersection["placed_pos"]
         | 
| 228 | 
            -
                        
         | 
| 229 | 
            -
                        logger.debug(f"Placing word2 '{word2}' at row={word2_start_row}, col={intersection_col}")
         | 
| 230 | 
            -
                        
         | 
| 231 | 
            -
                        for i, letter in enumerate(word2):
         | 
| 232 | 
            -
                            check_row = word2_start_row + i
         | 
| 233 | 
            -
                            check_col = intersection_col
         | 
| 234 | 
            -
                            if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
         | 
| 235 | 
            -
                                logger.error(f"❌ _create_simple_cross word2 bounds error: row={check_row}, col={check_col}, size={size}")
         | 
| 236 | 
            -
                                raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
         | 
| 237 | 
            -
                            grid[check_row][check_col] = letter
         | 
| 238 | 
            -
                        
         | 
| 239 | 
            -
                        placed_words = [
         | 
| 240 | 
            -
                            {"word": word1, "row": center_row, "col": center_col, "direction": "horizontal", "number": 1},
         | 
| 241 | 
            -
                            {"word": word2, "row": word2_start_row, "col": intersection_col, "direction": "vertical", "number": 2}
         | 
| 242 | 
            -
                        ]
         | 
| 243 | 
            -
                        
         | 
| 244 | 
            -
                        logger.debug(f"_create_simple_cross: SUCCESS")
         | 
| 245 | 
            -
                        
         | 
| 246 | 
            -
                        trimmed = self._trim_grid(grid, placed_words)
         | 
| 247 | 
            -
                        clues = self._generate_clues(word_objs[:2], trimmed["placed_words"])
         | 
| 248 | 
            -
                        
         | 
| 249 | 
            -
                        return {
         | 
| 250 | 
            -
                            "grid": trimmed["grid"],
         | 
| 251 | 
            -
                            "placed_words": trimmed["placed_words"],
         | 
| 252 | 
            -
                            "clues": clues
         | 
| 253 | 
            -
                        }
         | 
| 254 | 
            -
                        
         | 
| 255 | 
            -
                    except Exception as e:
         | 
| 256 | 
            -
                        logger.error(f"❌ ERROR in _create_simple_cross: {e}")
         | 
| 257 | 
            -
                        import traceback
         | 
| 258 | 
            -
                        traceback.print_exc()
         | 
| 259 | 
            -
                        raise
         | 
| 260 | 
            -
             | 
| 261 | 
            -
            async def test_debug_generator():
         | 
| 262 | 
            -
                """Test the debug generator to catch index errors."""
         | 
| 263 | 
            -
                try:
         | 
| 264 | 
            -
                    print("🧪 Testing debug crossword generator...")
         | 
| 265 | 
            -
                    
         | 
| 266 | 
            -
                    # Create mock vector service
         | 
| 267 | 
            -
                    vector_service = VectorSearchService()
         | 
| 268 | 
            -
                    
         | 
| 269 | 
            -
                    # Create debug generator
         | 
| 270 | 
            -
                    generator = DebugCrosswordGenerator(vector_service)
         | 
| 271 | 
            -
                    
         | 
| 272 | 
            -
                    # Test with various topics and difficulties
         | 
| 273 | 
            -
                    test_cases = [
         | 
| 274 | 
            -
                        (["animals"], "medium"),
         | 
| 275 | 
            -
                        (["science"], "hard"),
         | 
| 276 | 
            -
                        (["technology"], "easy"),
         | 
| 277 | 
            -
                        (["animals", "science"], "medium"),
         | 
| 278 | 
            -
                    ]
         | 
| 279 | 
            -
                    
         | 
| 280 | 
            -
                    for i, (topics, difficulty) in enumerate(test_cases):
         | 
| 281 | 
            -
                        print(f"\n🔬 Test {i+1}: topics={topics}, difficulty={difficulty}")
         | 
| 282 | 
            -
                        try:
         | 
| 283 | 
            -
                            result = await generator.generate_puzzle(topics, difficulty, use_ai=False)
         | 
| 284 | 
            -
                            if result:
         | 
| 285 | 
            -
                                print(f"✅ Test {i+1} succeeded")
         | 
| 286 | 
            -
                                grid_size = len(result['grid'])
         | 
| 287 | 
            -
                                word_count = len(result['clues'])
         | 
| 288 | 
            -
                                print(f"   Grid: {grid_size}x{grid_size}, Words: {word_count}")
         | 
| 289 | 
            -
                            else:
         | 
| 290 | 
            -
                                print(f"⚠️ Test {i+1} returned None")
         | 
| 291 | 
            -
                        except Exception as e:
         | 
| 292 | 
            -
                            print(f"❌ Test {i+1} failed: {e}")
         | 
| 293 | 
            -
                            import traceback
         | 
| 294 | 
            -
                            traceback.print_exc()
         | 
| 295 | 
            -
                            return False
         | 
| 296 | 
            -
                    
         | 
| 297 | 
            -
                    print(f"\n✅ All debug tests completed!")
         | 
| 298 | 
            -
                    return True
         | 
| 299 | 
            -
                    
         | 
| 300 | 
            -
                except Exception as e:
         | 
| 301 | 
            -
                    print(f"❌ Debug test setup failed: {e}")
         | 
| 302 | 
            -
                    import traceback
         | 
| 303 | 
            -
                    traceback.print_exc()
         | 
| 304 | 
            -
                    return False
         | 
| 305 | 
            -
             | 
| 306 | 
            -
            if __name__ == "__main__":
         | 
| 307 | 
            -
                asyncio.run(test_debug_generator())
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/debug_simple.py
    DELETED
    
    | @@ -1,142 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
            """
         | 
| 3 | 
            -
            Simple debug test for crossword generator index errors.
         | 
| 4 | 
            -
            """
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            import asyncio
         | 
| 7 | 
            -
            import sys
         | 
| 8 | 
            -
            import logging
         | 
| 9 | 
            -
            from pathlib import Path
         | 
| 10 | 
            -
             | 
| 11 | 
            -
            # Add project root to path
         | 
| 12 | 
            -
            project_root = Path(__file__).parent
         | 
| 13 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
         | 
| 16 | 
            -
             | 
| 17 | 
            -
            # Enable debug logging
         | 
| 18 | 
            -
            logging.basicConfig(level=logging.DEBUG)
         | 
| 19 | 
            -
            logger = logging.getLogger(__name__)
         | 
| 20 | 
            -
             | 
| 21 | 
            -
            async def test_with_static_words():
         | 
| 22 | 
            -
                """Test generator with static word lists."""
         | 
| 23 | 
            -
                
         | 
| 24 | 
            -
                # Create generator without vector service
         | 
| 25 | 
            -
                generator = CrosswordGeneratorFixed(vector_service=None)
         | 
| 26 | 
            -
                
         | 
| 27 | 
            -
                # Create test words
         | 
| 28 | 
            -
                test_words = [
         | 
| 29 | 
            -
                    {"word": "CAT", "clue": "Feline pet"},
         | 
| 30 | 
            -
                    {"word": "DOG", "clue": "Man's best friend"}, 
         | 
| 31 | 
            -
                    {"word": "BIRD", "clue": "Flying animal"},
         | 
| 32 | 
            -
                    {"word": "FISH", "clue": "Aquatic animal"},
         | 
| 33 | 
            -
                    {"word": "ELEPHANT", "clue": "Large mammal"},
         | 
| 34 | 
            -
                    {"word": "TIGER", "clue": "Striped cat"},
         | 
| 35 | 
            -
                    {"word": "HORSE", "clue": "Riding animal"},
         | 
| 36 | 
            -
                    {"word": "BEAR", "clue": "Large carnivore"}
         | 
| 37 | 
            -
                ]
         | 
| 38 | 
            -
                
         | 
| 39 | 
            -
                print(f"🧪 Testing crossword generation with {len(test_words)} words...")
         | 
| 40 | 
            -
                
         | 
| 41 | 
            -
                try:
         | 
| 42 | 
            -
                    # Test multiple times to catch intermittent errors
         | 
| 43 | 
            -
                    for attempt in range(10):
         | 
| 44 | 
            -
                        print(f"\n🔬 Attempt {attempt + 1}/10")
         | 
| 45 | 
            -
                        
         | 
| 46 | 
            -
                        # Shuffle words to create different scenarios
         | 
| 47 | 
            -
                        import random
         | 
| 48 | 
            -
                        random.shuffle(test_words)
         | 
| 49 | 
            -
                        
         | 
| 50 | 
            -
                        # Override the word selection to use our test words
         | 
| 51 | 
            -
                        generator._select_words = lambda topics, difficulty, use_ai: test_words
         | 
| 52 | 
            -
                        
         | 
| 53 | 
            -
                        result = await generator.generate_puzzle(["animals"], "medium", use_ai=False)
         | 
| 54 | 
            -
                        
         | 
| 55 | 
            -
                        if result:
         | 
| 56 | 
            -
                            grid_size = len(result['grid'])
         | 
| 57 | 
            -
                            word_count = len(result['clues'])
         | 
| 58 | 
            -
                            print(f"✅ Attempt {attempt + 1} succeeded: {grid_size}x{grid_size} grid, {word_count} words")
         | 
| 59 | 
            -
                        else:
         | 
| 60 | 
            -
                            print(f"⚠️ Attempt {attempt + 1} returned None")
         | 
| 61 | 
            -
                            
         | 
| 62 | 
            -
                except IndexError as e:
         | 
| 63 | 
            -
                    print(f"❌ INDEX ERROR caught on attempt {attempt + 1}: {e}")
         | 
| 64 | 
            -
                    import traceback
         | 
| 65 | 
            -
                    traceback.print_exc()
         | 
| 66 | 
            -
                    return False
         | 
| 67 | 
            -
                except Exception as e:
         | 
| 68 | 
            -
                    print(f"❌ Other error on attempt {attempt + 1}: {e}")
         | 
| 69 | 
            -
                    import traceback
         | 
| 70 | 
            -
                    traceback.print_exc()
         | 
| 71 | 
            -
                    return False
         | 
| 72 | 
            -
                
         | 
| 73 | 
            -
                print(f"\n✅ All 10 attempts completed successfully!")
         | 
| 74 | 
            -
                return True
         | 
| 75 | 
            -
             | 
| 76 | 
            -
            async def test_grid_placement_directly():
         | 
| 77 | 
            -
                """Test grid placement functions directly with problematic data."""
         | 
| 78 | 
            -
                
         | 
| 79 | 
            -
                generator = CrosswordGeneratorFixed(vector_service=None)
         | 
| 80 | 
            -
                
         | 
| 81 | 
            -
                # Test data that might cause issues
         | 
| 82 | 
            -
                test_cases = [
         | 
| 83 | 
            -
                    {
         | 
| 84 | 
            -
                        "words": ["A", "I"],  # Very short words
         | 
| 85 | 
            -
                        "description": "Very short words"
         | 
| 86 | 
            -
                    },
         | 
| 87 | 
            -
                    {
         | 
| 88 | 
            -
                        "words": ["VERYLONGWORDTHATMIGHTCAUSEISSUES", "SHORT"],
         | 
| 89 | 
            -
                        "description": "Very long word with short word"
         | 
| 90 | 
            -
                    },
         | 
| 91 | 
            -
                    {
         | 
| 92 | 
            -
                        "words": ["ABCDEFGHIJKLMNOP", "QRSTUVWXYZ"],  # Long words
         | 
| 93 | 
            -
                        "description": "Two long words"
         | 
| 94 | 
            -
                    },
         | 
| 95 | 
            -
                    {
         | 
| 96 | 
            -
                        "words": ["TEST", "SETS", "NETS", "PETS"],  # Multiple similar words
         | 
| 97 | 
            -
                        "description": "Similar words with same endings"
         | 
| 98 | 
            -
                    }
         | 
| 99 | 
            -
                ]
         | 
| 100 | 
            -
                
         | 
| 101 | 
            -
                for i, test_case in enumerate(test_cases):
         | 
| 102 | 
            -
                    print(f"\n🔬 Grid test {i+1}: {test_case['description']}")
         | 
| 103 | 
            -
                    
         | 
| 104 | 
            -
                    try:
         | 
| 105 | 
            -
                        word_list = test_case["words"]
         | 
| 106 | 
            -
                        word_objs = [{"word": w, "clue": f"Clue for {w}"} for w in word_list]
         | 
| 107 | 
            -
                        
         | 
| 108 | 
            -
                        result = generator._create_grid(word_objs)
         | 
| 109 | 
            -
                        
         | 
| 110 | 
            -
                        if result:
         | 
| 111 | 
            -
                            grid_size = len(result['grid'])
         | 
| 112 | 
            -
                            word_count = len(result['placed_words'])
         | 
| 113 | 
            -
                            print(f"✅ Grid test {i+1} succeeded: {grid_size}x{grid_size} grid, {word_count} words")
         | 
| 114 | 
            -
                        else:
         | 
| 115 | 
            -
                            print(f"⚠️ Grid test {i+1} returned None")
         | 
| 116 | 
            -
                            
         | 
| 117 | 
            -
                    except IndexError as e:
         | 
| 118 | 
            -
                        print(f"❌ INDEX ERROR in grid test {i+1}: {e}")
         | 
| 119 | 
            -
                        import traceback
         | 
| 120 | 
            -
                        traceback.print_exc()
         | 
| 121 | 
            -
                        return False
         | 
| 122 | 
            -
                    except Exception as e:
         | 
| 123 | 
            -
                        print(f"❌ Other error in grid test {i+1}: {e}")
         | 
| 124 | 
            -
                        import traceback
         | 
| 125 | 
            -
                        traceback.print_exc()
         | 
| 126 | 
            -
                        return False
         | 
| 127 | 
            -
                
         | 
| 128 | 
            -
                return True
         | 
| 129 | 
            -
             | 
| 130 | 
            -
            if __name__ == "__main__":
         | 
| 131 | 
            -
                print("🧪 Starting debug tests for crossword generator...")
         | 
| 132 | 
            -
                
         | 
| 133 | 
            -
                async def run_tests():
         | 
| 134 | 
            -
                    success1 = await test_with_static_words()
         | 
| 135 | 
            -
                    success2 = await test_grid_placement_directly()
         | 
| 136 | 
            -
                    
         | 
| 137 | 
            -
                    if success1 and success2:
         | 
| 138 | 
            -
                        print("\n🎉 All debug tests passed! No index errors detected.")
         | 
| 139 | 
            -
                    else:
         | 
| 140 | 
            -
                        print("\n❌ Some debug tests failed.")
         | 
| 141 | 
            -
                
         | 
| 142 | 
            -
                asyncio.run(run_tests())
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/public/assets/index-2XJqMaqu.js
    ADDED
    
    | @@ -0,0 +1,10 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import{r as p,a as C,R as P}from"./vendor-nf7bT_Uh.js";(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))n(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function c(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(t){if(t.ep)return;t.ep=!0;const r=c(t);fetch(t.href,r)}})();var w={exports:{}},z={};/**
         | 
| 2 | 
            +
             * @license React
         | 
| 3 | 
            +
             * react-jsx-runtime.production.min.js
         | 
| 4 | 
            +
             *
         | 
| 5 | 
            +
             * Copyright (c) Facebook, Inc. and its affiliates.
         | 
| 6 | 
            +
             *
         | 
| 7 | 
            +
             * This source code is licensed under the MIT license found in the
         | 
| 8 | 
            +
             * LICENSE file in the root directory of this source tree.
         | 
| 9 | 
            +
             */var R=p,_=Symbol.for("react.element"),$=Symbol.for("react.fragment"),E=Object.prototype.hasOwnProperty,O=R.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,k={key:!0,ref:!0,__self:!0,__source:!0};function S(s,a,c){var n,t={},r=null,o=null;c!==void 0&&(r=""+c),a.key!==void 0&&(r=""+a.key),a.ref!==void 0&&(o=a.ref);for(n in a)E.call(a,n)&&!k.hasOwnProperty(n)&&(t[n]=a[n]);if(s&&s.defaultProps)for(n in a=s.defaultProps,a)t[n]===void 0&&(t[n]=a[n]);return{$$typeof:_,type:s,key:r,ref:o,props:t,_owner:O.current}}z.Fragment=$;z.jsx=S;z.jsxs=S;w.exports=z;var e=w.exports,N={},b=C;N.createRoot=b.createRoot,N.hydrateRoot=b.hydrateRoot;const T=({onTopicsChange:s,availableTopics:a=[],selectedTopics:c=[]})=>{const n=t=>{const r=c.includes(t)?c.filter(o=>o!==t):[...c,t];s(r)};return e.jsxs("div",{className:"topic-selector",children:[e.jsx("h3",{children:"Select Topics"}),e.jsx("div",{className:"topic-buttons",children:a.map(t=>e.jsx("button",{className:`topic-btn ${c.includes(t.name)?"selected":""}`,onClick:()=>n(t.name),children:t.name},t.id))}),e.jsxs("p",{className:"selected-count",children:[c.length," topic",c.length!==1?"s":""," selected"]})]})},L=({grid:s,clues:a,showSolution:c,onCellChange:n})=>{const[t,r]=p.useState({}),o=(u,l,i)=>{const d=`${u}-${l}`,h={...t,[d]:i.toUpperCase()};r(h),n&&n(u,l,i)},f=(u,l)=>{if(c&&!m(u,l))return s[u][l];const i=`${u}-${l}`;return t[i]||""},m=(u,l)=>s[u][l]===".",g=(u,l)=>{if(!a)return null;const i=a.find(d=>d.position.row===u&&d.position.col===l);return i?i.number:null};if(!s||s.length===0)return e.jsx("div",{className:"puzzle-grid",children:"No puzzle loaded"});const x=s.length,y=s[0]?s[0].length:0;return e.jsx("div",{className:"puzzle-container",children:e.jsx("div",{className:"puzzle-grid",style:{gridTemplateColumns:`repeat(${y}, 35px)`,gridTemplateRows:`repeat(${x}, 35px)`},children:s.map((u,l)=>u.map((i,d)=>{const h=g(l,d);return m(l,d)?e.jsx("div",{className:"grid-cell empty-cell",style:{visibility:"hidden"}},`${l}-${d}`):e.jsxs("div",{className:"grid-cell white-cell",children:[h&&e.jsx("span",{className:"cell-number",children:h}),e.jsx("input",{type:"text",maxLength:"1",value:f(l,d),onChange:v=>o(l,d,v.target.value),className:`cell-input ${c?"solution-text":""}`,disabled:c})]},`${l}-${d}`)}))})})},A=({clues:s=[]})=>{const a=s.filter(t=>t.direction==="across"),c=s.filter(t=>t.direction==="down"),n=({title:t,clueList:r})=>e.jsxs("div",{className:"clue-section",children:[e.jsx("h4",{children:t}),e.jsx("ol",{children:r.map(o=>e.jsxs("li",{className:"clue-item",children:[e.jsx("span",{className:"clue-number",children:o.number}),e.jsx("span",{className:"clue-text",children:o.text})]},`${o.number}-${o.direction}`))})]});return e.jsxs("div",{className:"clue-list",children:[e.jsx(n,{title:"Across",clueList:a}),e.jsx(n,{title:"Down",clueList:c})]})},D=({message:s="Generating puzzle..."})=>e.jsxs("div",{className:"loading-spinner",children:[e.jsx("div",{className:"spinner"}),e.jsx("p",{className:"loading-message",children:s})]}),F=()=>{const[s,a]=p.useState(null),[c,n]=p.useState(!1),[t,r]=p.useState(null),[o,f]=p.useState([]),m="",g=p.useCallback(async()=>{try{n(!0);const l=await fetch(`${m}/api/topics`);if(!l.ok)throw new Error("Failed to fetch topics");const i=await l.json();f(i)}catch(l){r(l.message)}finally{n(!1)}},[m]),x=p.useCallback(async(l,i="medium",d=!1)=>{try{n(!0),r(null);const h=await fetch(`${m}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({topics:l,difficulty:i,useAI:d})});if(!h.ok){const v=await h.json().catch(()=>({}));throw new Error(v.message||"Failed to generate puzzle")}const j=await h.json();return a(j),j}catch(h){return r(h.message),null}finally{n(!1)}},[m]),y=p.useCallback(async l=>{try{const i=await fetch(`${m}/api/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({puzzle:s,answers:l})});if(!i.ok)throw new Error("Failed to validate answers");return await i.json()}catch(i){return r(i.message),null}},[m,s]),u=p.useCallback(()=>{a(null),r(null)},[]);return{puzzle:s,loading:c,error:t,topics:o,fetchTopics:g,generatePuzzle:x,validateAnswers:y,resetPuzzle:u}};function G(){const[s,a]=p.useState([]),[c,n]=p.useState("medium"),[t,r]=p.useState(!1),{puzzle:o,loading:f,error:m,topics:g,fetchTopics:x,generatePuzzle:y,resetPuzzle:u}=F();p.useEffect(()=>{x()},[x]);const l=async()=>{if(s.length===0){alert("Please select at least one topic");return}await y(s,c,!1)},i=j=>{a(j)},d=()=>{u(),a([]),r(!1),n("medium")},h=()=>{r(!0)};return e.jsxs("div",{className:"crossword-app",children:[e.jsxs("header",{className:"app-header",children:[e.jsx("h1",{className:"app-title",children:"Crossword Puzzle Generator"}),e.jsx("p",{children:"Select topics and generate your custom crossword puzzle!"})]}),e.jsx(T,{onTopicsChange:i,availableTopics:g,selectedTopics:s}),e.jsxs("div",{className:"puzzle-controls",children:[e.jsxs("select",{value:c,onChange:j=>n(j.target.value),className:"control-btn",children:[e.jsx("option",{value:"easy",children:"Easy"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"hard",children:"Hard"})]}),e.jsx("button",{onClick:l,disabled:f||s.length===0,className:"control-btn generate-btn",children:f?"Generating...":"Generate Puzzle"}),e.jsx("button",{onClick:d,className:"control-btn reset-btn",children:"Reset"}),o&&!t&&e.jsx("button",{onClick:h,className:"control-btn reveal-btn",children:"Reveal Solution"})]}),m&&e.jsxs("div",{className:"error-message",children:["Error: ",m]}),f&&e.jsx(D,{}),o&&!f&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"puzzle-info",children:e.jsxs("span",{className:"puzzle-stats",children:[o.metadata.wordCount," words • ",o.metadata.size,"×",o.metadata.size," grid"]})}),e.jsxs("div",{className:"puzzle-layout",children:[e.jsx(L,{grid:o.grid,clues:o.clues,showSolution:t}),e.jsx(A,{clues:o.clues})]})]}),!o&&!f&&!m&&e.jsx("div",{style:{textAlign:"center",padding:"40px",color:"#7f8c8d"},children:'Select topics and click "Generate Puzzle" to start!'})]})}N.createRoot(document.getElementById("root")).render(e.jsx(P.StrictMode,{children:e.jsx(G,{})}));
         | 
| 10 | 
            +
            //# sourceMappingURL=index-2XJqMaqu.js.map
         | 
    	
        crossword-app/backend-py/public/assets/index-2XJqMaqu.js.map
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            {"version":3,"file":"index-2XJqMaqu.js","sources":["../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/react-dom/client.js","../../src/components/TopicSelector.jsx","../../src/components/PuzzleGrid.jsx","../../src/components/ClueList.jsx","../../src/components/LoadingSpinner.jsx","../../src/hooks/useCrossword.js","../../src/App.jsx","../../src/main.jsx"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n  module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n  module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n  exports.createRoot = m.createRoot;\n  exports.hydrateRoot = m.hydrateRoot;\n} else {\n  var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n  exports.createRoot = function(c, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.createRoot(c, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n  exports.hydrateRoot = function(c, h, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.hydrateRoot(c, h, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n}\n","import React from 'react';\n\nconst TopicSelector = ({ \n  onTopicsChange, \n  availableTopics = [], \n  selectedTopics = []\n}) => {\n  const handleTopicToggle = (topic) => {\n    const newSelectedTopics = selectedTopics.includes(topic)\n      ? selectedTopics.filter(t => t !== topic)\n      : [...selectedTopics, topic];\n    \n    onTopicsChange(newSelectedTopics);\n  };\n\n  return (\n    <div className=\"topic-selector\">\n      <h3>Select Topics</h3>\n      <div className=\"topic-buttons\">\n        {availableTopics.map(topic => (\n          <button\n            key={topic.id}\n            className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}\n            onClick={() => handleTopicToggle(topic.name)}\n          >\n            {topic.name}\n          </button>\n        ))}\n      </div>\n      \n      <p className=\"selected-count\">\n        {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected\n      </p>\n    </div>\n  );\n};\n\nexport default TopicSelector;","import React, { useState } from 'react';\n\nconst PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {\n  const [userAnswers, setUserAnswers] = useState({});\n\n  const handleCellInput = (row, col, value) => {\n    const key = `${row}-${col}`;\n    const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };\n    setUserAnswers(newAnswers);\n    onCellChange && onCellChange(row, col, value);\n  };\n\n  const getCellValue = (row, col) => {\n    if (showSolution && !isBlackCell(row, col)) {\n      return grid[row][col];\n    }\n    const key = `${row}-${col}`;\n    return userAnswers[key] || '';\n  };\n\n  const isBlackCell = (row, col) => {\n    return grid[row][col] === '.';\n  };\n\n  const getCellNumber = (row, col) => {\n    if (!clues) return null;\n    const clue = clues.find(c => c.position.row === row && c.position.col === col);\n    return clue ? clue.number : null;\n  };\n\n  if (!grid || grid.length === 0) {\n    return <div className=\"puzzle-grid\">No puzzle loaded</div>;\n  }\n\n  const gridRows = grid.length;\n  const gridCols = grid[0] ? grid[0].length : 0;\n\n  return (\n    <div className=\"puzzle-container\">\n      <div \n        className=\"puzzle-grid\"\n        style={{\n          gridTemplateColumns: `repeat(${gridCols}, 35px)`,\n          gridTemplateRows: `repeat(${gridRows}, 35px)`\n        }}\n      >\n        {grid.map((row, rowIndex) =>\n          row.map((cell, colIndex) => {\n            const cellNumber = getCellNumber(rowIndex, colIndex);\n            const isBlack = isBlackCell(rowIndex, colIndex);\n            \n            // Only render cells that contain letters (not black/unused cells)\n            if (isBlack) {\n              return (\n                <div\n                  key={`${rowIndex}-${colIndex}`}\n                  className=\"grid-cell empty-cell\"\n                  style={{ visibility: 'hidden' }}\n                >\n                </div>\n              );\n            }\n            \n            return (\n              <div\n                key={`${rowIndex}-${colIndex}`}\n                className=\"grid-cell white-cell\"\n              >\n                {cellNumber && <span className=\"cell-number\">{cellNumber}</span>}\n                <input\n                  type=\"text\"\n                  maxLength=\"1\"\n                  value={getCellValue(rowIndex, colIndex)}\n                  onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}\n                  className={`cell-input ${showSolution ? 'solution-text' : ''}`}\n                  disabled={showSolution}\n                />\n              </div>\n            );\n          })\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default PuzzleGrid;","import React from 'react';\n\nconst ClueList = ({ clues = [] }) => {\n  const acrossClues = clues.filter(clue => clue.direction === 'across');\n  const downClues = clues.filter(clue => clue.direction === 'down');\n\n  const ClueSection = ({ title, clueList }) => (\n    <div className=\"clue-section\">\n      <h4>{title}</h4>\n      <ol>\n        {clueList.map(clue => (\n          <li key={`${clue.number}-${clue.direction}`} className=\"clue-item\">\n            <span className=\"clue-number\">{clue.number}</span>\n            <span className=\"clue-text\">{clue.text}</span>\n          </li>\n        ))}\n      </ol>\n    </div>\n  );\n\n  return (\n    <div className=\"clue-list\">\n      <ClueSection title=\"Across\" clueList={acrossClues} />\n      <ClueSection title=\"Down\" clueList={downClues} />\n    </div>\n  );\n};\n\nexport default ClueList;","import React from 'react';\n\nconst LoadingSpinner = ({ message = \"Generating puzzle...\" }) => {\n  return (\n    <div className=\"loading-spinner\">\n      <div className=\"spinner\"></div>\n      <p className=\"loading-message\">{message}</p>\n    </div>\n  );\n};\n\nexport default LoadingSpinner;","import { useState, useCallback } from 'react';\n\nconst useCrossword = () => {\n  const [puzzle, setPuzzle] = useState(null);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [topics, setTopics] = useState([]);\n\n  const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || (import.meta.env.PROD ? '' : 'http://localhost:3000');\n\n  const fetchTopics = useCallback(async () => {\n    try {\n      setLoading(true);\n      const response = await fetch(`${API_BASE_URL}/api/topics`);\n      if (!response.ok) throw new Error('Failed to fetch topics');\n      const data = await response.json();\n      setTopics(data);\n    } catch (err) {\n      setError(err.message);\n    } finally {\n      setLoading(false);\n    }\n  }, [API_BASE_URL]);\n\n  const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false) => {\n    try {\n      setLoading(true);\n      setError(null);\n      \n      const response = await fetch(`${API_BASE_URL}/api/generate`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          topics: selectedTopics,\n          difficulty,\n          useAI\n        })\n      });\n\n      if (!response.ok) {\n        const errorData = await response.json().catch(() => ({}));\n        throw new Error(errorData.message || 'Failed to generate puzzle');\n      }\n      \n      const puzzleData = await response.json();\n      setPuzzle(puzzleData);\n      return puzzleData;\n    } catch (err) {\n      setError(err.message);\n      return null;\n    } finally {\n      setLoading(false);\n    }\n  }, [API_BASE_URL]);\n\n  const validateAnswers = useCallback(async (userAnswers) => {\n    try {\n      const response = await fetch(`${API_BASE_URL}/api/validate`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          puzzle: puzzle,\n          answers: userAnswers\n        })\n      });\n\n      if (!response.ok) throw new Error('Failed to validate answers');\n      \n      return await response.json();\n    } catch (err) {\n      setError(err.message);\n      return null;\n    }\n  }, [API_BASE_URL, puzzle]);\n\n  const resetPuzzle = useCallback(() => {\n    setPuzzle(null);\n    setError(null);\n  }, []);\n\n  return {\n    puzzle,\n    loading,\n    error,\n    topics,\n    fetchTopics,\n    generatePuzzle,\n    validateAnswers,\n    resetPuzzle\n  };\n};\n\nexport default useCrossword;","import React, { useState, useEffect } from 'react';\nimport TopicSelector from './components/TopicSelector';\nimport PuzzleGrid from './components/PuzzleGrid';\nimport ClueList from './components/ClueList';\nimport LoadingSpinner from './components/LoadingSpinner';\nimport useCrossword from './hooks/useCrossword';\nimport './styles/puzzle.css';\n\nfunction App() {\n  const [selectedTopics, setSelectedTopics] = useState([]);\n  const [difficulty, setDifficulty] = useState('medium');\n  const [showSolution, setShowSolution] = useState(false);\n  \n  const {\n    puzzle,\n    loading,\n    error,\n    topics,\n    fetchTopics,\n    generatePuzzle,\n    resetPuzzle\n  } = useCrossword();\n\n  useEffect(() => {\n    fetchTopics();\n  }, [fetchTopics]);\n\n  const handleGeneratePuzzle = async () => {\n    if (selectedTopics.length === 0) {\n      alert('Please select at least one topic');\n      return;\n    }\n    \n    await generatePuzzle(selectedTopics, difficulty, false);\n  };\n\n  const handleTopicsChange = (topics) => {\n    setSelectedTopics(topics);\n  };\n\n\n  const handleReset = () => {\n    resetPuzzle();\n    setSelectedTopics([]);\n    setShowSolution(false);\n    setDifficulty('medium');\n  };\n\n  const handleRevealSolution = () => {\n    setShowSolution(true);\n  };\n\n  return (\n    <div className=\"crossword-app\">\n      <header className=\"app-header\">\n        <h1 className=\"app-title\">Crossword Puzzle Generator</h1>\n        <p>Select topics and generate your custom crossword puzzle!</p>\n      </header>\n\n      <TopicSelector \n        onTopicsChange={handleTopicsChange}\n        availableTopics={topics}\n        selectedTopics={selectedTopics}\n      />\n\n      <div className=\"puzzle-controls\">\n        <select \n          value={difficulty} \n          onChange={(e) => setDifficulty(e.target.value)}\n          className=\"control-btn\"\n        >\n          <option value=\"easy\">Easy</option>\n          <option value=\"medium\">Medium</option>\n          <option value=\"hard\">Hard</option>\n        </select>\n        \n        <button\n          onClick={handleGeneratePuzzle}\n          disabled={loading || selectedTopics.length === 0}\n          className=\"control-btn generate-btn\"\n        >\n          {loading ? 'Generating...' : 'Generate Puzzle'}\n        </button>\n        \n        <button\n          onClick={handleReset}\n          className=\"control-btn reset-btn\"\n        >\n          Reset\n        </button>\n        \n        {puzzle && !showSolution && (\n          <button\n            onClick={handleRevealSolution}\n            className=\"control-btn reveal-btn\"\n          >\n            Reveal Solution\n          </button>\n        )}\n      </div>\n\n      {error && (\n        <div className=\"error-message\">\n          Error: {error}\n        </div>\n      )}\n\n      {loading && <LoadingSpinner />}\n\n      {puzzle && !loading && (\n        <>\n          <div className=\"puzzle-info\">\n            <span className=\"puzzle-stats\">\n              {puzzle.metadata.wordCount} words • {puzzle.metadata.size}×{puzzle.metadata.size} grid\n            </span>\n          </div>\n          <div className=\"puzzle-layout\">\n            <PuzzleGrid \n              grid={puzzle.grid} \n              clues={puzzle.clues}\n              showSolution={showSolution}\n            />\n            <ClueList clues={puzzle.clues} />\n          </div>\n        </>\n      )}\n\n      {!puzzle && !loading && !error && (\n        <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>\n          Select topics and click \"Generate Puzzle\" to start!\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default App;","import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App.jsx'\n\nReactDOM.createRoot(document.getElementById('root')).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n)"],"names":["f","require$$0","k","l","m","n","p","q","c","g","b","d","e","h","reactJsxRuntime_production_min","jsxRuntimeModule","client","TopicSelector","onTopicsChange","availableTopics","selectedTopics","handleTopicToggle","topic","newSelectedTopics","t","jsxs","jsx","PuzzleGrid","grid","clues","showSolution","onCellChange","userAnswers","setUserAnswers","useState","handleCellInput","row","col","value","key","newAnswers","getCellValue","isBlackCell","getCellNumber","clue","gridRows","gridCols","rowIndex","cell","colIndex","cellNumber","ClueList","acrossClues","downClues","ClueSection","title","clueList","LoadingSpinner","message","useCrossword","puzzle","setPuzzle","loading","setLoading","error","setError","topics","setTopics","API_BASE_URL","fetchTopics","useCallback","response","data","err","generatePuzzle","difficulty","useAI","errorData","puzzleData","validateAnswers","resetPuzzle","App","setSelectedTopics","setDifficulty","setShowSolution","useEffect","handleGeneratePuzzle","handleTopicsChange","handleReset","handleRevealSolution","Fragment","ReactDOM","React"],"mappings":";;;;;;;;GASa,IAAIA,EAAEC,EAAiBC,EAAE,OAAO,IAAI,eAAe,EAAEC,EAAE,OAAO,IAAI,gBAAgB,EAAEC,EAAE,OAAO,UAAU,eAAeC,EAAEL,EAAE,mDAAmD,kBAAkBM,EAAE,CAAC,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,EAAE,EAClP,SAASC,EAAEC,EAAE,EAAEC,EAAE,CAAC,IAAIC,EAAEC,EAAE,GAAGC,EAAE,KAAKC,EAAE,KAAcJ,IAAT,SAAaG,EAAE,GAAGH,GAAY,EAAE,MAAX,SAAiBG,EAAE,GAAG,EAAE,KAAc,EAAE,MAAX,SAAiBC,EAAE,EAAE,KAAK,IAAIH,KAAK,EAAEN,EAAE,KAAK,EAAEM,CAAC,GAAG,CAACJ,EAAE,eAAeI,CAAC,IAAIC,EAAED,CAAC,EAAE,EAAEA,CAAC,GAAG,GAAGF,GAAGA,EAAE,aAAa,IAAIE,KAAK,EAAEF,EAAE,aAAa,EAAWG,EAAED,CAAC,aAAIC,EAAED,CAAC,EAAE,EAAEA,CAAC,GAAG,MAAM,CAAC,SAASR,EAAE,KAAKM,EAAE,IAAII,EAAE,IAAIC,EAAE,MAAMF,EAAE,OAAON,EAAE,OAAO,CAAC,YAAkBF,EAAEW,EAAA,IAAYP,EAAEO,EAAA,KAAaP,ECPxWQ,EAAA,QAAiBd,uBCDfG,EAAIH,EAENe,EAAA,WAAqBZ,EAAE,WACvBY,EAAA,YAAsBZ,EAAE,YCH1B,MAAMa,EAAgB,CAAC,CACrB,eAAAC,EACA,gBAAAC,EAAkB,CAAA,EAClB,eAAAC,EAAiB,CAAA,CACnB,IAAM,CACJ,MAAMC,EAAqBC,GAAU,CACnC,MAAMC,EAAoBH,EAAe,SAASE,CAAK,EACnDF,EAAe,OAAOI,GAAKA,IAAMF,CAAK,EACtC,CAAC,GAAGF,EAAgBE,CAAK,EAE7BJ,EAAeK,CAAiB,CAClC,EAEA,OACEE,EAAAA,KAAC,MAAA,CAAI,UAAU,iBACb,SAAA,CAAAC,EAAAA,IAAC,MAAG,SAAA,eAAA,CAAa,QAChB,MAAA,CAAI,UAAU,gBACZ,SAAAP,EAAgB,IAAIG,GACnBI,EAAAA,IAAC,SAAA,CAEC,UAAW,aAAaN,EAAe,SAASE,EAAM,IAAI,EAAI,WAAa,EAAE,GAC7E,QAAS,IAAMD,EAAkBC,EAAM,IAAI,EAE1C,SAAAA,EAAM,IAAA,EAJFA,EAAM,EAAA,CAMd,EACH,EAEAG,EAAAA,KAAC,IAAA,CAAE,UAAU,iBACV,SAAA,CAAAL,EAAe,OAAO,SAAOA,EAAe,SAAW,EAAI,IAAM,GAAG,WAAA,CAAA,CACvE,CAAA,EACF,CAEJ,ECjCMO,EAAa,CAAC,CAAE,KAAAC,EAAM,MAAAC,EAAO,aAAAC,EAAc,aAAAC,KAAmB,CAClE,KAAM,CAACC,EAAaC,CAAc,EAAIC,EAAAA,SAAS,CAAA,CAAE,EAE3CC,EAAkB,CAACC,EAAKC,EAAKC,IAAU,CAC3C,MAAMC,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACnBG,EAAa,CAAE,GAAGR,EAAa,CAACO,CAAG,EAAGD,EAAM,aAAY,EAC9DL,EAAeO,CAAU,EACzBT,GAAgBA,EAAaK,EAAKC,EAAKC,CAAK,CAC9C,EAEMG,EAAe,CAACL,EAAKC,IAAQ,CACjC,GAAIP,GAAgB,CAACY,EAAYN,EAAKC,CAAG,EACvC,OAAOT,EAAKQ,CAAG,EAAEC,CAAG,EAEtB,MAAME,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACzB,OAAOL,EAAYO,CAAG,GAAK,EAC7B,EAEMG,EAAc,CAACN,EAAKC,IACjBT,EAAKQ,CAAG,EAAEC,CAAG,IAAM,IAGtBM,EAAgB,CAACP,EAAKC,IAAQ,CAClC,GAAI,CAACR,EAAO,OAAO,KACnB,MAAMe,EAAOf,EAAM,KAAKrB,GAAKA,EAAE,SAAS,MAAQ4B,GAAO5B,EAAE,SAAS,MAAQ6B,CAAG,EAC7E,OAAOO,EAAOA,EAAK,OAAS,IAC9B,EAEA,GAAI,CAAChB,GAAQA,EAAK,SAAW,EAC3B,OAAOF,EAAAA,IAAC,MAAA,CAAI,UAAU,cAAc,SAAA,mBAAgB,EAGtD,MAAMmB,EAAWjB,EAAK,OAChBkB,EAAWlB,EAAK,CAAC,EAAIA,EAAK,CAAC,EAAE,OAAS,EAE5C,OACEF,EAAAA,IAAC,MAAA,CAAI,UAAU,mBACb,SAAAA,EAAAA,IAAC,MAAA,CACC,UAAU,cACV,MAAO,CACL,oBAAqB,UAAUoB,CAAQ,UACvC,iBAAkB,UAAUD,CAAQ,SAAA,EAGrC,SAAAjB,EAAK,IAAI,CAACQ,EAAKW,IACdX,EAAI,IAAI,CAACY,EAAMC,IAAa,CAC1B,MAAMC,EAAaP,EAAcI,EAAUE,CAAQ,EAInD,OAHgBP,EAAYK,EAAUE,CAAQ,EAK1CvB,EAAAA,IAAC,MAAA,CAEC,UAAU,uBACV,MAAO,CAAE,WAAY,QAAA,CAAS,EAFzB,GAAGqB,CAAQ,IAAIE,CAAQ,EAAA,EAShCxB,EAAAA,KAAC,MAAA,CAEC,UAAU,uBAET,SAAA,CAAAyB,GAAcxB,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAwB,EAAW,EACzDxB,EAAAA,IAAC,QAAA,CACC,KAAK,OACL,UAAU,IACV,MAAOe,EAAaM,EAAUE,CAAQ,EACtC,SAAWrC,GAAMuB,EAAgBY,EAAUE,EAAUrC,EAAE,OAAO,KAAK,EACnE,UAAW,cAAckB,EAAe,gBAAkB,EAAE,GAC5D,SAAUA,CAAA,CAAA,CACZ,CAAA,EAXK,GAAGiB,CAAQ,IAAIE,CAAQ,EAAA,CAclC,CAAC,CAAA,CACH,CAAA,EAEJ,CAEJ,EClFME,EAAW,CAAC,CAAE,MAAAtB,EAAQ,CAAA,KAAS,CACnC,MAAMuB,EAAcvB,EAAM,OAAOe,GAAQA,EAAK,YAAc,QAAQ,EAC9DS,EAAYxB,EAAM,OAAOe,GAAQA,EAAK,YAAc,MAAM,EAE1DU,EAAc,CAAC,CAAE,MAAAC,EAAO,SAAAC,KAC5B/B,OAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAC,EAAAA,IAAC,MAAI,SAAA6B,CAAA,CAAM,EACX7B,EAAAA,IAAC,MACE,SAAA8B,EAAS,OACR/B,EAAAA,KAAC,KAAA,CAA4C,UAAU,YACrD,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAkB,EAAK,OAAO,EAC3ClB,EAAAA,IAAC,OAAA,CAAK,UAAU,YAAa,WAAK,IAAA,CAAK,CAAA,GAFhC,GAAGkB,EAAK,MAAM,IAAIA,EAAK,SAAS,EAGzC,CACD,CAAA,CACH,CAAA,EACF,EAGF,OACEnB,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAC,EAAAA,IAAC4B,EAAA,CAAY,MAAM,SAAS,SAAUF,EAAa,EACnD1B,EAAAA,IAAC4B,EAAA,CAAY,MAAM,OAAO,SAAUD,CAAA,CAAW,CAAA,EACjD,CAEJ,ECxBMI,EAAiB,CAAC,CAAE,QAAAC,EAAU,0BAEhCjC,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAC,EAAAA,IAAC,MAAA,CAAI,UAAU,SAAA,CAAU,EACzBA,EAAAA,IAAC,IAAA,CAAE,UAAU,kBAAmB,SAAAgC,CAAA,CAAQ,CAAA,EAC1C,ECLEC,EAAe,IAAM,CACzB,KAAM,CAACC,EAAQC,CAAS,EAAI3B,EAAAA,SAAS,IAAI,EACnC,CAAC4B,EAASC,CAAU,EAAI7B,EAAAA,SAAS,EAAK,EACtC,CAAC8B,EAAOC,CAAQ,EAAI/B,EAAAA,SAAS,IAAI,EACjC,CAACgC,EAAQC,CAAS,EAAIjC,EAAAA,SAAS,CAAA,CAAE,EAEjCkC,EAA4E,GAE5EC,EAAcC,EAAAA,YAAY,SAAY,CAC1C,GAAI,CACFP,EAAW,EAAI,EACf,MAAMQ,EAAW,MAAM,MAAM,GAAGH,CAAY,aAAa,EACzD,GAAI,CAACG,EAAS,GAAI,MAAM,IAAI,MAAM,wBAAwB,EAC1D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5BJ,EAAUK,CAAI,CAChB,OAASC,EAAK,CACZR,EAASQ,EAAI,OAAO,CACtB,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXM,EAAiBJ,EAAAA,YAAY,MAAOlD,EAAgBuD,EAAa,SAAUC,EAAQ,KAAU,CACjG,GAAI,CACFb,EAAW,EAAI,EACfE,EAAS,IAAI,EAEb,MAAMM,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAQhD,EACR,WAAAuD,EACA,MAAAC,CAAA,CACD,CAAA,CACF,EAED,GAAI,CAACL,EAAS,GAAI,CAChB,MAAMM,EAAY,MAAMN,EAAS,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EACxD,MAAM,IAAI,MAAMM,EAAU,SAAW,2BAA2B,CAClE,CAEA,MAAMC,EAAa,MAAMP,EAAS,KAAA,EAClC,OAAAV,EAAUiB,CAAU,EACbA,CACT,OAASL,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXW,EAAkBT,cAAY,MAAOtC,GAAgB,CACzD,GAAI,CACF,MAAMuC,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAAR,EACA,QAAS5B,CAAA,CACV,CAAA,CACF,EAED,GAAI,CAACuC,EAAS,GAAI,MAAM,IAAI,MAAM,4BAA4B,EAE9D,OAAO,MAAMA,EAAS,KAAA,CACxB,OAASE,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,CACF,EAAG,CAACL,EAAcR,CAAM,CAAC,EAEnBoB,EAAcV,EAAAA,YAAY,IAAM,CACpCT,EAAU,IAAI,EACdI,EAAS,IAAI,CACf,EAAG,CAAA,CAAE,EAEL,MAAO,CACL,OAAAL,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,gBAAAK,EACA,YAAAC,CAAA,CAEJ,ECtFA,SAASC,GAAM,CACb,KAAM,CAAC7D,EAAgB8D,CAAiB,EAAIhD,EAAAA,SAAS,CAAA,CAAE,EACjD,CAACyC,EAAYQ,CAAa,EAAIjD,EAAAA,SAAS,QAAQ,EAC/C,CAACJ,EAAcsD,CAAe,EAAIlD,EAAAA,SAAS,EAAK,EAEhD,CACJ,OAAA0B,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,YAAAM,CAAA,EACErB,EAAA,EAEJ0B,EAAAA,UAAU,IAAM,CACdhB,EAAA,CACF,EAAG,CAACA,CAAW,CAAC,EAEhB,MAAMiB,EAAuB,SAAY,CACvC,GAAIlE,EAAe,SAAW,EAAG,CAC/B,MAAM,kCAAkC,EACxC,MACF,CAEA,MAAMsD,EAAetD,EAAgBuD,EAAY,EAAK,CACxD,EAEMY,EAAsBrB,GAAW,CACrCgB,EAAkBhB,CAAM,CAC1B,EAGMsB,EAAc,IAAM,CACxBR,EAAA,EACAE,EAAkB,CAAA,CAAE,EACpBE,EAAgB,EAAK,EACrBD,EAAc,QAAQ,CACxB,EAEMM,EAAuB,IAAM,CACjCL,EAAgB,EAAI,CACtB,EAEA,OACE3D,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CAAO,UAAU,aAChB,SAAA,CAAAC,EAAAA,IAAC,KAAA,CAAG,UAAU,YAAY,SAAA,6BAA0B,EACpDA,EAAAA,IAAC,KAAE,SAAA,0DAAA,CAAwD,CAAA,EAC7D,EAEAA,EAAAA,IAACT,EAAA,CACC,eAAgBsE,EAChB,gBAAiBrB,EACjB,eAAA9C,CAAA,CAAA,EAGFK,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CACC,MAAOkD,EACP,SAAW/D,GAAMuE,EAAcvE,EAAE,OAAO,KAAK,EAC7C,UAAU,cAEV,SAAA,CAAAc,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,OAAI,EACzBA,EAAAA,IAAC,SAAA,CAAO,MAAM,SAAS,SAAA,SAAM,EAC7BA,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,MAAA,CAAI,CAAA,CAAA,CAAA,EAG3BA,EAAAA,IAAC,SAAA,CACC,QAAS4D,EACT,SAAUxB,GAAW1C,EAAe,SAAW,EAC/C,UAAU,2BAET,WAAU,gBAAkB,iBAAA,CAAA,EAG/BM,EAAAA,IAAC,SAAA,CACC,QAAS8D,EACT,UAAU,wBACX,SAAA,OAAA,CAAA,EAIA5B,GAAU,CAAC9B,GACVJ,EAAAA,IAAC,SAAA,CACC,QAAS+D,EACT,UAAU,yBACX,SAAA,iBAAA,CAAA,CAED,EAEJ,EAECzB,GACCvC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBAAgB,SAAA,CAAA,UACrBuC,CAAA,EACV,EAGDF,SAAYL,EAAA,EAAe,EAE3BG,GAAU,CAACE,GACVrC,EAAAA,KAAAiE,EAAAA,SAAA,CACE,SAAA,CAAAhE,EAAAA,IAAC,OAAI,UAAU,cACb,SAAAD,EAAAA,KAAC,OAAA,CAAK,UAAU,eACb,SAAA,CAAAmC,EAAO,SAAS,UAAU,YAAUA,EAAO,SAAS,KAAK,IAAEA,EAAO,SAAS,KAAK,OAAA,CAAA,CACnF,CAAA,CACF,EACAnC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAMiC,EAAO,KACb,MAAOA,EAAO,MACd,aAAA9B,CAAA,CAAA,EAEFJ,EAAAA,IAACyB,EAAA,CAAS,MAAOS,EAAO,KAAA,CAAO,CAAA,CAAA,CACjC,CAAA,EACF,EAGD,CAACA,GAAU,CAACE,GAAW,CAACE,GACvBtC,EAAAA,IAAC,MAAA,CAAI,MAAO,CAAE,UAAW,SAAU,QAAS,OAAQ,MAAO,SAAA,EAAa,SAAA,qDAAA,CAExE,CAAA,EAEJ,CAEJ,CClIAiE,EAAS,WAAW,SAAS,eAAe,MAAM,CAAC,EAAE,aAClDC,EAAM,WAAN,CACC,SAAAlE,MAACuD,IAAI,CAAA,CACP,CACF","x_google_ignoreList":[0,1,2]}
         | 
    	
        crossword-app/backend-py/public/assets/index-7dkEH9uQ.js
    ADDED
    
    | @@ -0,0 +1,10 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import{r as m,a as R,R as E}from"./vendor-nf7bT_Uh.js";(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))n(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&n(l)}).observe(document,{childList:!0,subtree:!0});function o(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(t){if(t.ep)return;t.ep=!0;const r=o(t);fetch(t.href,r)}})();var S={exports:{}},N={};/**
         | 
| 2 | 
            +
             * @license React
         | 
| 3 | 
            +
             * react-jsx-runtime.production.min.js
         | 
| 4 | 
            +
             *
         | 
| 5 | 
            +
             * Copyright (c) Facebook, Inc. and its affiliates.
         | 
| 6 | 
            +
             *
         | 
| 7 | 
            +
             * This source code is licensed under the MIT license found in the
         | 
| 8 | 
            +
             * LICENSE file in the root directory of this source tree.
         | 
| 9 | 
            +
             */var _=m,$=Symbol.for("react.element"),k=Symbol.for("react.fragment"),O=Object.prototype.hasOwnProperty,T=_.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,L={key:!0,ref:!0,__self:!0,__source:!0};function P(s,c,o){var n,t={},r=null,l=null;o!==void 0&&(r=""+o),c.key!==void 0&&(r=""+c.key),c.ref!==void 0&&(l=c.ref);for(n in c)O.call(c,n)&&!L.hasOwnProperty(n)&&(t[n]=c[n]);if(s&&s.defaultProps)for(n in c=s.defaultProps,c)t[n]===void 0&&(t[n]=c[n]);return{$$typeof:$,type:s,key:r,ref:l,props:t,_owner:T.current}}N.Fragment=k;N.jsx=P;N.jsxs=P;S.exports=N;var e=S.exports,w={},C=R;w.createRoot=C.createRoot,w.hydrateRoot=C.hydrateRoot;const A=({onTopicsChange:s,availableTopics:c=[],selectedTopics:o=[],customSentence:n="",onSentenceChange:t})=>{const r=l=>{const x=o.includes(l)?o.filter(i=>i!==l):[...o,l];s(x)};return e.jsxs("div",{className:"topic-selector",children:[e.jsx("h3",{children:"Select Topics"}),e.jsx("div",{className:"topic-buttons",children:c.map(l=>e.jsx("button",{className:`topic-btn ${o.includes(l.name)?"selected":""}`,onClick:()=>r(l.name),children:l.name},l.id))}),e.jsxs("div",{className:"sentence-input-container",children:[e.jsx("label",{htmlFor:"custom-sentence",className:"sentence-label",children:"Custom Sentence (optional)"}),e.jsx("textarea",{id:"custom-sentence",className:"sentence-input",value:n,onChange:l=>t&&t(l.target.value),placeholder:"Enter a sentence to influence word selection...",rows:"3",maxLength:"200"}),e.jsxs("div",{className:"sentence-info",children:[e.jsxs("span",{className:"char-count",children:[n.length,"/200 characters"]}),n&&e.jsx("button",{type:"button",className:"clear-sentence-btn",onClick:()=>t&&t(""),title:"Clear sentence",children:"Clear"})]})]}),e.jsxs("p",{className:"selected-count",children:[o.length," topic",o.length!==1?"s":""," selected"]})]})},F=({grid:s,clues:c,showSolution:o,onCellChange:n})=>{const[t,r]=m.useState({}),l=(d,a,u)=>{const p=`${d}-${a}`,f={...t,[p]:u.toUpperCase()};r(f),n&&n(d,a,u)},x=(d,a)=>{if(o&&!i(d,a))return s[d][a];const u=`${d}-${a}`;return t[u]||""},i=(d,a)=>s[d][a]===".",h=(d,a)=>{if(!c)return null;const u=c.find(p=>p.position.row===d&&p.position.col===a);return u?u.number:null};if(!s||s.length===0)return e.jsx("div",{className:"puzzle-grid",children:"No puzzle loaded"});const g=s.length,z=s[0]?s[0].length:0;return e.jsx("div",{className:"puzzle-container",children:e.jsx("div",{className:"puzzle-grid",style:{gridTemplateColumns:`repeat(${z}, 35px)`,gridTemplateRows:`repeat(${g}, 35px)`},children:s.map((d,a)=>d.map((u,p)=>{const f=h(a,p);return i(a,p)?e.jsx("div",{className:"grid-cell empty-cell",style:{visibility:"hidden"}},`${a}-${p}`):e.jsxs("div",{className:"grid-cell white-cell",children:[f&&e.jsx("span",{className:"cell-number",children:f}),e.jsx("input",{type:"text",maxLength:"1",value:x(a,p),onChange:y=>l(a,p,y.target.value),className:`cell-input ${o?"solution-text":""}`,disabled:o})]},`${a}-${p}`)}))})})},D=({clues:s=[]})=>{const c=s.filter(t=>t.direction==="across"),o=s.filter(t=>t.direction==="down"),n=({title:t,clueList:r})=>e.jsxs("div",{className:"clue-section",children:[e.jsx("h4",{children:t}),e.jsx("ol",{children:r.map(l=>e.jsxs("li",{className:"clue-item",children:[e.jsx("span",{className:"clue-number",children:l.number}),e.jsx("span",{className:"clue-text",children:l.text})]},`${l.number}-${l.direction}`))})]});return e.jsxs("div",{className:"clue-list",children:[e.jsx(n,{title:"Across",clueList:c}),e.jsx(n,{title:"Down",clueList:o})]})},G=({message:s="Generating puzzle..."})=>e.jsxs("div",{className:"loading-spinner",children:[e.jsx("div",{className:"spinner"}),e.jsx("p",{className:"loading-message",children:s})]}),B=()=>{const[s,c]=m.useState(null),[o,n]=m.useState(!1),[t,r]=m.useState(null),[l,x]=m.useState([]),i="",h=m.useCallback(async()=>{try{n(!0);const a=await fetch(`${i}/api/topics`);if(!a.ok)throw new Error("Failed to fetch topics");const u=await a.json();x(u)}catch(a){r(a.message)}finally{n(!1)}},[i]),g=m.useCallback(async(a,u="medium",p=!1,f="")=>{try{n(!0),r(null);const j=await fetch(`${i}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({topics:a,difficulty:u,useAI:p,...f&&{customSentence:f}})});if(!j.ok){const b=await j.json().catch(()=>({}));throw new Error(b.message||"Failed to generate puzzle")}const y=await j.json();return c(y),y}catch(j){return r(j.message),null}finally{n(!1)}},[i]),z=m.useCallback(async a=>{try{const u=await fetch(`${i}/api/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({puzzle:s,answers:a})});if(!u.ok)throw new Error("Failed to validate answers");return await u.json()}catch(u){return r(u.message),null}},[i,s]),d=m.useCallback(()=>{c(null),r(null)},[]);return{puzzle:s,loading:o,error:t,topics:l,fetchTopics:h,generatePuzzle:g,validateAnswers:z,resetPuzzle:d}};function U(){const[s,c]=m.useState([]),[o,n]=m.useState("medium"),[t,r]=m.useState(!1),[l,x]=m.useState(""),{puzzle:i,loading:h,error:g,topics:z,fetchTopics:d,generatePuzzle:a,resetPuzzle:u}=B();m.useEffect(()=>{d()},[d]);const p=async()=>{if(s.length===0){alert("Please select at least one topic");return}await a(s,o,!1,l)},f=v=>{c(v)},j=v=>{x(v)},y=()=>{u(),c([]),r(!1),n("medium"),x("")},b=()=>{r(!0)};return e.jsxs("div",{className:"crossword-app",children:[e.jsxs("header",{className:"app-header",children:[e.jsx("h1",{className:"app-title",children:"Crossword Puzzle Generator"}),e.jsx("p",{children:"Select topics and generate your custom crossword puzzle!"})]}),e.jsx(A,{onTopicsChange:f,availableTopics:z,selectedTopics:s,customSentence:l,onSentenceChange:j}),e.jsxs("div",{className:"puzzle-controls",children:[e.jsxs("select",{value:o,onChange:v=>n(v.target.value),className:"control-btn",children:[e.jsx("option",{value:"easy",children:"Easy"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"hard",children:"Hard"})]}),e.jsx("button",{onClick:p,disabled:h||s.length===0,className:"control-btn generate-btn",children:h?"Generating...":"Generate Puzzle"}),e.jsx("button",{onClick:y,className:"control-btn reset-btn",children:"Reset"}),i&&!t&&e.jsx("button",{onClick:b,className:"control-btn reveal-btn",children:"Reveal Solution"})]}),g&&e.jsxs("div",{className:"error-message",children:["Error: ",g]}),h&&e.jsx(G,{}),i&&!h&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"puzzle-info",children:e.jsxs("span",{className:"puzzle-stats",children:[i.metadata.wordCount," words • ",i.metadata.size,"×",i.metadata.size," grid"]})}),e.jsxs("div",{className:"puzzle-layout",children:[e.jsx(F,{grid:i.grid,clues:i.clues,showSolution:t}),e.jsx(D,{clues:i.clues})]})]}),!i&&!h&&!g&&e.jsx("div",{style:{textAlign:"center",padding:"40px",color:"#7f8c8d"},children:'Select topics and click "Generate Puzzle" to start!'})]})}w.createRoot(document.getElementById("root")).render(e.jsx(E.StrictMode,{children:e.jsx(U,{})}));
         | 
| 10 | 
            +
            //# sourceMappingURL=index-7dkEH9uQ.js.map
         | 
    	
        crossword-app/backend-py/public/assets/index-7dkEH9uQ.js.map
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            {"version":3,"file":"index-7dkEH9uQ.js","sources":["../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/react-dom/client.js","../../src/components/TopicSelector.jsx","../../src/components/PuzzleGrid.jsx","../../src/components/ClueList.jsx","../../src/components/LoadingSpinner.jsx","../../src/hooks/useCrossword.js","../../src/App.jsx","../../src/main.jsx"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n  module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n  module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n  exports.createRoot = m.createRoot;\n  exports.hydrateRoot = m.hydrateRoot;\n} else {\n  var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n  exports.createRoot = function(c, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.createRoot(c, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n  exports.hydrateRoot = function(c, h, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.hydrateRoot(c, h, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n}\n","import React from 'react';\n\nconst TopicSelector = ({ \n  onTopicsChange, \n  availableTopics = [], \n  selectedTopics = [],\n  customSentence = '',\n  onSentenceChange\n}) => {\n  const handleTopicToggle = (topic) => {\n    const newSelectedTopics = selectedTopics.includes(topic)\n      ? selectedTopics.filter(t => t !== topic)\n      : [...selectedTopics, topic];\n    \n    onTopicsChange(newSelectedTopics);\n  };\n\n  return (\n    <div className=\"topic-selector\">\n      <h3>Select Topics</h3>\n      <div className=\"topic-buttons\">\n        {availableTopics.map(topic => (\n          <button\n            key={topic.id}\n            className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}\n            onClick={() => handleTopicToggle(topic.name)}\n          >\n            {topic.name}\n          </button>\n        ))}\n      </div>\n      \n      <div className=\"sentence-input-container\">\n        <label htmlFor=\"custom-sentence\" className=\"sentence-label\">\n          Custom Sentence (optional)\n        </label>\n        <textarea\n          id=\"custom-sentence\"\n          className=\"sentence-input\"\n          value={customSentence}\n          onChange={(e) => onSentenceChange && onSentenceChange(e.target.value)}\n          placeholder=\"Enter a sentence to influence word selection...\"\n          rows=\"3\"\n          maxLength=\"200\"\n        />\n        <div className=\"sentence-info\">\n          <span className=\"char-count\">{customSentence.length}/200 characters</span>\n          {customSentence && (\n            <button \n              type=\"button\"\n              className=\"clear-sentence-btn\"\n              onClick={() => onSentenceChange && onSentenceChange('')}\n              title=\"Clear sentence\"\n            >\n              Clear\n            </button>\n          )}\n        </div>\n      </div>\n      \n      <p className=\"selected-count\">\n        {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected\n      </p>\n    </div>\n  );\n};\n\nexport default TopicSelector;","import React, { useState } from 'react';\n\nconst PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {\n  const [userAnswers, setUserAnswers] = useState({});\n\n  const handleCellInput = (row, col, value) => {\n    const key = `${row}-${col}`;\n    const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };\n    setUserAnswers(newAnswers);\n    onCellChange && onCellChange(row, col, value);\n  };\n\n  const getCellValue = (row, col) => {\n    if (showSolution && !isBlackCell(row, col)) {\n      return grid[row][col];\n    }\n    const key = `${row}-${col}`;\n    return userAnswers[key] || '';\n  };\n\n  const isBlackCell = (row, col) => {\n    return grid[row][col] === '.';\n  };\n\n  const getCellNumber = (row, col) => {\n    if (!clues) return null;\n    const clue = clues.find(c => c.position.row === row && c.position.col === col);\n    return clue ? clue.number : null;\n  };\n\n  if (!grid || grid.length === 0) {\n    return <div className=\"puzzle-grid\">No puzzle loaded</div>;\n  }\n\n  const gridRows = grid.length;\n  const gridCols = grid[0] ? grid[0].length : 0;\n\n  return (\n    <div className=\"puzzle-container\">\n      <div \n        className=\"puzzle-grid\"\n        style={{\n          gridTemplateColumns: `repeat(${gridCols}, 35px)`,\n          gridTemplateRows: `repeat(${gridRows}, 35px)`\n        }}\n      >\n        {grid.map((row, rowIndex) =>\n          row.map((cell, colIndex) => {\n            const cellNumber = getCellNumber(rowIndex, colIndex);\n            const isBlack = isBlackCell(rowIndex, colIndex);\n            \n            // Only render cells that contain letters (not black/unused cells)\n            if (isBlack) {\n              return (\n                <div\n                  key={`${rowIndex}-${colIndex}`}\n                  className=\"grid-cell empty-cell\"\n                  style={{ visibility: 'hidden' }}\n                >\n                </div>\n              );\n            }\n            \n            return (\n              <div\n                key={`${rowIndex}-${colIndex}`}\n                className=\"grid-cell white-cell\"\n              >\n                {cellNumber && <span className=\"cell-number\">{cellNumber}</span>}\n                <input\n                  type=\"text\"\n                  maxLength=\"1\"\n                  value={getCellValue(rowIndex, colIndex)}\n                  onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}\n                  className={`cell-input ${showSolution ? 'solution-text' : ''}`}\n                  disabled={showSolution}\n                />\n              </div>\n            );\n          })\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default PuzzleGrid;","import React from 'react';\n\nconst ClueList = ({ clues = [] }) => {\n  const acrossClues = clues.filter(clue => clue.direction === 'across');\n  const downClues = clues.filter(clue => clue.direction === 'down');\n\n  const ClueSection = ({ title, clueList }) => (\n    <div className=\"clue-section\">\n      <h4>{title}</h4>\n      <ol>\n        {clueList.map(clue => (\n          <li key={`${clue.number}-${clue.direction}`} className=\"clue-item\">\n            <span className=\"clue-number\">{clue.number}</span>\n            <span className=\"clue-text\">{clue.text}</span>\n          </li>\n        ))}\n      </ol>\n    </div>\n  );\n\n  return (\n    <div className=\"clue-list\">\n      <ClueSection title=\"Across\" clueList={acrossClues} />\n      <ClueSection title=\"Down\" clueList={downClues} />\n    </div>\n  );\n};\n\nexport default ClueList;","import React from 'react';\n\nconst LoadingSpinner = ({ message = \"Generating puzzle...\" }) => {\n  return (\n    <div className=\"loading-spinner\">\n      <div className=\"spinner\"></div>\n      <p className=\"loading-message\">{message}</p>\n    </div>\n  );\n};\n\nexport default LoadingSpinner;","import { useState, useCallback } from 'react';\n\nconst useCrossword = () => {\n  const [puzzle, setPuzzle] = useState(null);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [topics, setTopics] = useState([]);\n\n  const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || (import.meta.env.PROD ? '' : 'http://localhost:3000');\n\n  const fetchTopics = useCallback(async () => {\n    try {\n      setLoading(true);\n      const response = await fetch(`${API_BASE_URL}/api/topics`);\n      if (!response.ok) throw new Error('Failed to fetch topics');\n      const data = await response.json();\n      setTopics(data);\n    } catch (err) {\n      setError(err.message);\n    } finally {\n      setLoading(false);\n    }\n  }, [API_BASE_URL]);\n\n  const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false, customSentence = '') => {\n    try {\n      setLoading(true);\n      setError(null);\n      \n      const response = await fetch(`${API_BASE_URL}/api/generate`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          topics: selectedTopics,\n          difficulty,\n          useAI,\n          ...(customSentence && { customSentence })\n        })\n      });\n\n      if (!response.ok) {\n        const errorData = await response.json().catch(() => ({}));\n        throw new Error(errorData.message || 'Failed to generate puzzle');\n      }\n      \n      const puzzleData = await response.json();\n      setPuzzle(puzzleData);\n      return puzzleData;\n    } catch (err) {\n      setError(err.message);\n      return null;\n    } finally {\n      setLoading(false);\n    }\n  }, [API_BASE_URL]);\n\n  const validateAnswers = useCallback(async (userAnswers) => {\n    try {\n      const response = await fetch(`${API_BASE_URL}/api/validate`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          puzzle: puzzle,\n          answers: userAnswers\n        })\n      });\n\n      if (!response.ok) throw new Error('Failed to validate answers');\n      \n      return await response.json();\n    } catch (err) {\n      setError(err.message);\n      return null;\n    }\n  }, [API_BASE_URL, puzzle]);\n\n  const resetPuzzle = useCallback(() => {\n    setPuzzle(null);\n    setError(null);\n  }, []);\n\n  return {\n    puzzle,\n    loading,\n    error,\n    topics,\n    fetchTopics,\n    generatePuzzle,\n    validateAnswers,\n    resetPuzzle\n  };\n};\n\nexport default useCrossword;","import React, { useState, useEffect } from 'react';\nimport TopicSelector from './components/TopicSelector';\nimport PuzzleGrid from './components/PuzzleGrid';\nimport ClueList from './components/ClueList';\nimport LoadingSpinner from './components/LoadingSpinner';\nimport useCrossword from './hooks/useCrossword';\nimport './styles/puzzle.css';\n\nfunction App() {\n  const [selectedTopics, setSelectedTopics] = useState([]);\n  const [difficulty, setDifficulty] = useState('medium');\n  const [showSolution, setShowSolution] = useState(false);\n  const [customSentence, setCustomSentence] = useState('');\n  \n  const {\n    puzzle,\n    loading,\n    error,\n    topics,\n    fetchTopics,\n    generatePuzzle,\n    resetPuzzle\n  } = useCrossword();\n\n  useEffect(() => {\n    fetchTopics();\n  }, [fetchTopics]);\n\n  const handleGeneratePuzzle = async () => {\n    if (selectedTopics.length === 0) {\n      alert('Please select at least one topic');\n      return;\n    }\n    \n    await generatePuzzle(selectedTopics, difficulty, false, customSentence);\n  };\n\n  const handleTopicsChange = (topics) => {\n    setSelectedTopics(topics);\n  };\n\n  const handleSentenceChange = (sentence) => {\n    setCustomSentence(sentence);\n  };\n\n\n  const handleReset = () => {\n    resetPuzzle();\n    setSelectedTopics([]);\n    setShowSolution(false);\n    setDifficulty('medium');\n    setCustomSentence('');\n  };\n\n  const handleRevealSolution = () => {\n    setShowSolution(true);\n  };\n\n  return (\n    <div className=\"crossword-app\">\n      <header className=\"app-header\">\n        <h1 className=\"app-title\">Crossword Puzzle Generator</h1>\n        <p>Select topics and generate your custom crossword puzzle!</p>\n      </header>\n\n      <TopicSelector \n        onTopicsChange={handleTopicsChange}\n        availableTopics={topics}\n        selectedTopics={selectedTopics}\n        customSentence={customSentence}\n        onSentenceChange={handleSentenceChange}\n      />\n\n      <div className=\"puzzle-controls\">\n        <select \n          value={difficulty} \n          onChange={(e) => setDifficulty(e.target.value)}\n          className=\"control-btn\"\n        >\n          <option value=\"easy\">Easy</option>\n          <option value=\"medium\">Medium</option>\n          <option value=\"hard\">Hard</option>\n        </select>\n        \n        <button\n          onClick={handleGeneratePuzzle}\n          disabled={loading || selectedTopics.length === 0}\n          className=\"control-btn generate-btn\"\n        >\n          {loading ? 'Generating...' : 'Generate Puzzle'}\n        </button>\n        \n        <button\n          onClick={handleReset}\n          className=\"control-btn reset-btn\"\n        >\n          Reset\n        </button>\n        \n        {puzzle && !showSolution && (\n          <button\n            onClick={handleRevealSolution}\n            className=\"control-btn reveal-btn\"\n          >\n            Reveal Solution\n          </button>\n        )}\n      </div>\n\n      {error && (\n        <div className=\"error-message\">\n          Error: {error}\n        </div>\n      )}\n\n      {loading && <LoadingSpinner />}\n\n      {puzzle && !loading && (\n        <>\n          <div className=\"puzzle-info\">\n            <span className=\"puzzle-stats\">\n              {puzzle.metadata.wordCount} words • {puzzle.metadata.size}×{puzzle.metadata.size} grid\n            </span>\n          </div>\n          <div className=\"puzzle-layout\">\n            <PuzzleGrid \n              grid={puzzle.grid} \n              clues={puzzle.clues}\n              showSolution={showSolution}\n            />\n            <ClueList clues={puzzle.clues} />\n          </div>\n        </>\n      )}\n\n      {!puzzle && !loading && !error && (\n        <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>\n          Select topics and click \"Generate Puzzle\" to start!\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default App;","import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App.jsx'\n\nReactDOM.createRoot(document.getElementById('root')).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n)"],"names":["f","require$$0","k","l","m","n","p","q","c","a","g","b","d","e","h","reactJsxRuntime_production_min","jsxRuntimeModule","client","TopicSelector","onTopicsChange","availableTopics","selectedTopics","customSentence","onSentenceChange","handleTopicToggle","topic","newSelectedTopics","t","jsxs","jsx","PuzzleGrid","grid","clues","showSolution","onCellChange","userAnswers","setUserAnswers","useState","handleCellInput","row","col","value","key","newAnswers","getCellValue","isBlackCell","getCellNumber","clue","gridRows","gridCols","rowIndex","cell","colIndex","cellNumber","ClueList","acrossClues","downClues","ClueSection","title","clueList","LoadingSpinner","message","useCrossword","puzzle","setPuzzle","loading","setLoading","error","setError","topics","setTopics","API_BASE_URL","fetchTopics","useCallback","response","data","err","generatePuzzle","difficulty","useAI","errorData","puzzleData","validateAnswers","resetPuzzle","App","setSelectedTopics","setDifficulty","setShowSolution","setCustomSentence","useEffect","handleGeneratePuzzle","handleTopicsChange","handleSentenceChange","sentence","handleReset","handleRevealSolution","Fragment","ReactDOM","React"],"mappings":";;;;;;;;GASa,IAAIA,EAAEC,EAAiBC,EAAE,OAAO,IAAI,eAAe,EAAEC,EAAE,OAAO,IAAI,gBAAgB,EAAEC,EAAE,OAAO,UAAU,eAAeC,EAAEL,EAAE,mDAAmD,kBAAkBM,EAAE,CAAC,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,EAAE,EAClP,SAASC,EAAEC,EAAEC,EAAEC,EAAE,CAAC,IAAIC,EAAEC,EAAE,GAAGC,EAAE,KAAKC,EAAE,KAAcJ,IAAT,SAAaG,EAAE,GAAGH,GAAYD,EAAE,MAAX,SAAiBI,EAAE,GAAGJ,EAAE,KAAcA,EAAE,MAAX,SAAiBK,EAAEL,EAAE,KAAK,IAAIE,KAAKF,EAAEL,EAAE,KAAKK,EAAEE,CAAC,GAAG,CAACL,EAAE,eAAeK,CAAC,IAAIC,EAAED,CAAC,EAAEF,EAAEE,CAAC,GAAG,GAAGH,GAAGA,EAAE,aAAa,IAAIG,KAAKF,EAAED,EAAE,aAAaC,EAAWG,EAAED,CAAC,aAAIC,EAAED,CAAC,EAAEF,EAAEE,CAAC,GAAG,MAAM,CAAC,SAAST,EAAE,KAAKM,EAAE,IAAIK,EAAE,IAAIC,EAAE,MAAMF,EAAE,OAAOP,EAAE,OAAO,CAAC,YAAkBF,EAAEY,EAAA,IAAYR,EAAEQ,EAAA,KAAaR,ECPxWS,EAAA,QAAiBf,uBCDfG,EAAIH,EAENgB,EAAA,WAAqBb,EAAE,WACvBa,EAAA,YAAsBb,EAAE,YCH1B,MAAMc,EAAgB,CAAC,CACrB,eAAAC,EACA,gBAAAC,EAAkB,CAAA,EAClB,eAAAC,EAAiB,CAAA,EACjB,eAAAC,EAAiB,GACjB,iBAAAC,CACF,IAAM,CACJ,MAAMC,EAAqBC,GAAU,CACnC,MAAMC,EAAoBL,EAAe,SAASI,CAAK,EACnDJ,EAAe,OAAOM,GAAKA,IAAMF,CAAK,EACtC,CAAC,GAAGJ,EAAgBI,CAAK,EAE7BN,EAAeO,CAAiB,CAClC,EAEA,OACEE,EAAAA,KAAC,MAAA,CAAI,UAAU,iBACb,SAAA,CAAAC,EAAAA,IAAC,MAAG,SAAA,eAAA,CAAa,QAChB,MAAA,CAAI,UAAU,gBACZ,SAAAT,EAAgB,IAAIK,GACnBI,EAAAA,IAAC,SAAA,CAEC,UAAW,aAAaR,EAAe,SAASI,EAAM,IAAI,EAAI,WAAa,EAAE,GAC7E,QAAS,IAAMD,EAAkBC,EAAM,IAAI,EAE1C,SAAAA,EAAM,IAAA,EAJFA,EAAM,EAAA,CAMd,EACH,EAEAG,EAAAA,KAAC,MAAA,CAAI,UAAU,2BACb,SAAA,CAAAC,MAAC,QAAA,CAAM,QAAQ,kBAAkB,UAAU,iBAAiB,SAAA,6BAE5D,EACAA,EAAAA,IAAC,WAAA,CACC,GAAG,kBACH,UAAU,iBACV,MAAOP,EACP,SAAWT,GAAMU,GAAoBA,EAAiBV,EAAE,OAAO,KAAK,EACpE,YAAY,kDACZ,KAAK,IACL,UAAU,KAAA,CAAA,EAEZe,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAc,SAAA,CAAAN,EAAe,OAAO,iBAAA,EAAe,EAClEA,GACCO,EAAAA,IAAC,SAAA,CACC,KAAK,SACL,UAAU,qBACV,QAAS,IAAMN,GAAoBA,EAAiB,EAAE,EACtD,MAAM,iBACP,SAAA,OAAA,CAAA,CAED,CAAA,CAEJ,CAAA,EACF,EAEAK,EAAAA,KAAC,IAAA,CAAE,UAAU,iBACV,SAAA,CAAAP,EAAe,OAAO,SAAOA,EAAe,SAAW,EAAI,IAAM,GAAG,WAAA,CAAA,CACvE,CAAA,EACF,CAEJ,EC/DMS,EAAa,CAAC,CAAE,KAAAC,EAAM,MAAAC,EAAO,aAAAC,EAAc,aAAAC,KAAmB,CAClE,KAAM,CAACC,EAAaC,CAAc,EAAIC,EAAAA,SAAS,CAAA,CAAE,EAE3CC,EAAkB,CAACC,EAAKC,EAAKC,IAAU,CAC3C,MAAMC,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACnBG,EAAa,CAAE,GAAGR,EAAa,CAACO,CAAG,EAAGD,EAAM,aAAY,EAC9DL,EAAeO,CAAU,EACzBT,GAAgBA,EAAaK,EAAKC,EAAKC,CAAK,CAC9C,EAEMG,EAAe,CAACL,EAAKC,IAAQ,CACjC,GAAIP,GAAgB,CAACY,EAAYN,EAAKC,CAAG,EACvC,OAAOT,EAAKQ,CAAG,EAAEC,CAAG,EAEtB,MAAME,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACzB,OAAOL,EAAYO,CAAG,GAAK,EAC7B,EAEMG,EAAc,CAACN,EAAKC,IACjBT,EAAKQ,CAAG,EAAEC,CAAG,IAAM,IAGtBM,EAAgB,CAACP,EAAKC,IAAQ,CAClC,GAAI,CAACR,EAAO,OAAO,KACnB,MAAMe,EAAOf,EAAM,KAAKxB,GAAKA,EAAE,SAAS,MAAQ+B,GAAO/B,EAAE,SAAS,MAAQgC,CAAG,EAC7E,OAAOO,EAAOA,EAAK,OAAS,IAC9B,EAEA,GAAI,CAAChB,GAAQA,EAAK,SAAW,EAC3B,OAAOF,EAAAA,IAAC,MAAA,CAAI,UAAU,cAAc,SAAA,mBAAgB,EAGtD,MAAMmB,EAAWjB,EAAK,OAChBkB,EAAWlB,EAAK,CAAC,EAAIA,EAAK,CAAC,EAAE,OAAS,EAE5C,OACEF,EAAAA,IAAC,MAAA,CAAI,UAAU,mBACb,SAAAA,EAAAA,IAAC,MAAA,CACC,UAAU,cACV,MAAO,CACL,oBAAqB,UAAUoB,CAAQ,UACvC,iBAAkB,UAAUD,CAAQ,SAAA,EAGrC,SAAAjB,EAAK,IAAI,CAACQ,EAAKW,IACdX,EAAI,IAAI,CAACY,EAAMC,IAAa,CAC1B,MAAMC,EAAaP,EAAcI,EAAUE,CAAQ,EAInD,OAHgBP,EAAYK,EAAUE,CAAQ,EAK1CvB,EAAAA,IAAC,MAAA,CAEC,UAAU,uBACV,MAAO,CAAE,WAAY,QAAA,CAAS,EAFzB,GAAGqB,CAAQ,IAAIE,CAAQ,EAAA,EAShCxB,EAAAA,KAAC,MAAA,CAEC,UAAU,uBAET,SAAA,CAAAyB,GAAcxB,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAwB,EAAW,EACzDxB,EAAAA,IAAC,QAAA,CACC,KAAK,OACL,UAAU,IACV,MAAOe,EAAaM,EAAUE,CAAQ,EACtC,SAAWvC,GAAMyB,EAAgBY,EAAUE,EAAUvC,EAAE,OAAO,KAAK,EACnE,UAAW,cAAcoB,EAAe,gBAAkB,EAAE,GAC5D,SAAUA,CAAA,CAAA,CACZ,CAAA,EAXK,GAAGiB,CAAQ,IAAIE,CAAQ,EAAA,CAclC,CAAC,CAAA,CACH,CAAA,EAEJ,CAEJ,EClFME,EAAW,CAAC,CAAE,MAAAtB,EAAQ,CAAA,KAAS,CACnC,MAAMuB,EAAcvB,EAAM,OAAOe,GAAQA,EAAK,YAAc,QAAQ,EAC9DS,EAAYxB,EAAM,OAAOe,GAAQA,EAAK,YAAc,MAAM,EAE1DU,EAAc,CAAC,CAAE,MAAAC,EAAO,SAAAC,KAC5B/B,OAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAC,EAAAA,IAAC,MAAI,SAAA6B,CAAA,CAAM,EACX7B,EAAAA,IAAC,MACE,SAAA8B,EAAS,OACR/B,EAAAA,KAAC,KAAA,CAA4C,UAAU,YACrD,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAkB,EAAK,OAAO,EAC3ClB,EAAAA,IAAC,OAAA,CAAK,UAAU,YAAa,WAAK,IAAA,CAAK,CAAA,GAFhC,GAAGkB,EAAK,MAAM,IAAIA,EAAK,SAAS,EAGzC,CACD,CAAA,CACH,CAAA,EACF,EAGF,OACEnB,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAC,EAAAA,IAAC4B,EAAA,CAAY,MAAM,SAAS,SAAUF,EAAa,EACnD1B,EAAAA,IAAC4B,EAAA,CAAY,MAAM,OAAO,SAAUD,CAAA,CAAW,CAAA,EACjD,CAEJ,ECxBMI,EAAiB,CAAC,CAAE,QAAAC,EAAU,0BAEhCjC,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAC,EAAAA,IAAC,MAAA,CAAI,UAAU,SAAA,CAAU,EACzBA,EAAAA,IAAC,IAAA,CAAE,UAAU,kBAAmB,SAAAgC,CAAA,CAAQ,CAAA,EAC1C,ECLEC,EAAe,IAAM,CACzB,KAAM,CAACC,EAAQC,CAAS,EAAI3B,EAAAA,SAAS,IAAI,EACnC,CAAC4B,EAASC,CAAU,EAAI7B,EAAAA,SAAS,EAAK,EACtC,CAAC8B,EAAOC,CAAQ,EAAI/B,EAAAA,SAAS,IAAI,EACjC,CAACgC,EAAQC,CAAS,EAAIjC,EAAAA,SAAS,CAAA,CAAE,EAEjCkC,EAA4E,GAE5EC,EAAcC,EAAAA,YAAY,SAAY,CAC1C,GAAI,CACFP,EAAW,EAAI,EACf,MAAMQ,EAAW,MAAM,MAAM,GAAGH,CAAY,aAAa,EACzD,GAAI,CAACG,EAAS,GAAI,MAAM,IAAI,MAAM,wBAAwB,EAC1D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5BJ,EAAUK,CAAI,CAChB,OAASC,EAAK,CACZR,EAASQ,EAAI,OAAO,CACtB,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXM,EAAiBJ,cAAY,MAAOpD,EAAgByD,EAAa,SAAUC,EAAQ,GAAOzD,EAAiB,KAAO,CACtH,GAAI,CACF4C,EAAW,EAAI,EACfE,EAAS,IAAI,EAEb,MAAMM,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAQlD,EACR,WAAAyD,EACA,MAAAC,EACA,GAAIzD,GAAkB,CAAE,eAAAA,CAAA,CAAe,CACxC,CAAA,CACF,EAED,GAAI,CAACoD,EAAS,GAAI,CAChB,MAAMM,EAAY,MAAMN,EAAS,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EACxD,MAAM,IAAI,MAAMM,EAAU,SAAW,2BAA2B,CAClE,CAEA,MAAMC,EAAa,MAAMP,EAAS,KAAA,EAClC,OAAAV,EAAUiB,CAAU,EACbA,CACT,OAASL,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXW,EAAkBT,cAAY,MAAOtC,GAAgB,CACzD,GAAI,CACF,MAAMuC,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAAR,EACA,QAAS5B,CAAA,CACV,CAAA,CACF,EAED,GAAI,CAACuC,EAAS,GAAI,MAAM,IAAI,MAAM,4BAA4B,EAE9D,OAAO,MAAMA,EAAS,KAAA,CACxB,OAASE,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,CACF,EAAG,CAACL,EAAcR,CAAM,CAAC,EAEnBoB,EAAcV,EAAAA,YAAY,IAAM,CACpCT,EAAU,IAAI,EACdI,EAAS,IAAI,CACf,EAAG,CAAA,CAAE,EAEL,MAAO,CACL,OAAAL,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,gBAAAK,EACA,YAAAC,CAAA,CAEJ,ECvFA,SAASC,GAAM,CACb,KAAM,CAAC/D,EAAgBgE,CAAiB,EAAIhD,EAAAA,SAAS,CAAA,CAAE,EACjD,CAACyC,EAAYQ,CAAa,EAAIjD,EAAAA,SAAS,QAAQ,EAC/C,CAACJ,EAAcsD,CAAe,EAAIlD,EAAAA,SAAS,EAAK,EAChD,CAACf,EAAgBkE,CAAiB,EAAInD,EAAAA,SAAS,EAAE,EAEjD,CACJ,OAAA0B,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,YAAAM,CAAA,EACErB,EAAA,EAEJ2B,EAAAA,UAAU,IAAM,CACdjB,EAAA,CACF,EAAG,CAACA,CAAW,CAAC,EAEhB,MAAMkB,EAAuB,SAAY,CACvC,GAAIrE,EAAe,SAAW,EAAG,CAC/B,MAAM,kCAAkC,EACxC,MACF,CAEA,MAAMwD,EAAexD,EAAgByD,EAAY,GAAOxD,CAAc,CACxE,EAEMqE,EAAsBtB,GAAW,CACrCgB,EAAkBhB,CAAM,CAC1B,EAEMuB,EAAwBC,GAAa,CACzCL,EAAkBK,CAAQ,CAC5B,EAGMC,EAAc,IAAM,CACxBX,EAAA,EACAE,EAAkB,CAAA,CAAE,EACpBE,EAAgB,EAAK,EACrBD,EAAc,QAAQ,EACtBE,EAAkB,EAAE,CACtB,EAEMO,EAAuB,IAAM,CACjCR,EAAgB,EAAI,CACtB,EAEA,OACE3D,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CAAO,UAAU,aAChB,SAAA,CAAAC,EAAAA,IAAC,KAAA,CAAG,UAAU,YAAY,SAAA,6BAA0B,EACpDA,EAAAA,IAAC,KAAE,SAAA,0DAAA,CAAwD,CAAA,EAC7D,EAEAA,EAAAA,IAACX,EAAA,CACC,eAAgByE,EAChB,gBAAiBtB,EACjB,eAAAhD,EACA,eAAAC,EACA,iBAAkBsE,CAAA,CAAA,EAGpBhE,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CACC,MAAOkD,EACP,SAAWjE,GAAMyE,EAAczE,EAAE,OAAO,KAAK,EAC7C,UAAU,cAEV,SAAA,CAAAgB,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,OAAI,EACzBA,EAAAA,IAAC,SAAA,CAAO,MAAM,SAAS,SAAA,SAAM,EAC7BA,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,MAAA,CAAI,CAAA,CAAA,CAAA,EAG3BA,EAAAA,IAAC,SAAA,CACC,QAAS6D,EACT,SAAUzB,GAAW5C,EAAe,SAAW,EAC/C,UAAU,2BAET,WAAU,gBAAkB,iBAAA,CAAA,EAG/BQ,EAAAA,IAAC,SAAA,CACC,QAASiE,EACT,UAAU,wBACX,SAAA,OAAA,CAAA,EAIA/B,GAAU,CAAC9B,GACVJ,EAAAA,IAAC,SAAA,CACC,QAASkE,EACT,UAAU,yBACX,SAAA,iBAAA,CAAA,CAED,EAEJ,EAEC5B,GACCvC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBAAgB,SAAA,CAAA,UACrBuC,CAAA,EACV,EAGDF,SAAYL,EAAA,EAAe,EAE3BG,GAAU,CAACE,GACVrC,EAAAA,KAAAoE,EAAAA,SAAA,CACE,SAAA,CAAAnE,EAAAA,IAAC,OAAI,UAAU,cACb,SAAAD,EAAAA,KAAC,OAAA,CAAK,UAAU,eACb,SAAA,CAAAmC,EAAO,SAAS,UAAU,YAAUA,EAAO,SAAS,KAAK,IAAEA,EAAO,SAAS,KAAK,OAAA,CAAA,CACnF,CAAA,CACF,EACAnC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAMiC,EAAO,KACb,MAAOA,EAAO,MACd,aAAA9B,CAAA,CAAA,EAEFJ,EAAAA,IAACyB,EAAA,CAAS,MAAOS,EAAO,KAAA,CAAO,CAAA,CAAA,CACjC,CAAA,EACF,EAGD,CAACA,GAAU,CAACE,GAAW,CAACE,GACvBtC,EAAAA,IAAC,MAAA,CAAI,MAAO,CAAE,UAAW,SAAU,QAAS,OAAQ,MAAO,SAAA,EAAa,SAAA,qDAAA,CAExE,CAAA,EAEJ,CAEJ,CC1IAoE,EAAS,WAAW,SAAS,eAAe,MAAM,CAAC,EAAE,aAClDC,EAAM,WAAN,CACC,SAAArE,MAACuD,IAAI,CAAA,CACP,CACF","x_google_ignoreList":[0,1,2]}
         | 
    	
        crossword-app/backend-py/public/assets/index-CWqdoNhy.css
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            .crossword-app{max-width:1200px;margin:0 auto;padding:20px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.app-header{text-align:center;margin-bottom:30px}.app-title{color:#2c3e50;font-size:2.5rem;margin-bottom:10px}.topic-selector{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.topic-selector h3{margin-top:0;color:#2c3e50}.topic-buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:15px}.topic-btn{padding:8px 16px;border:2px solid #3498db;background:#fff;color:#3498db;border-radius:20px;cursor:pointer;transition:all .3s ease;font-weight:500}.topic-btn:hover,.topic-btn.selected{background:#3498db;color:#fff}.selected-count{color:#7f8c8d;font-size:.9rem;margin:0}.sentence-input-container{margin-top:20px;margin-bottom:15px}.sentence-label{display:block;margin-bottom:8px;color:#2c3e50;font-weight:500;font-size:.95rem}.sentence-input{width:100%;padding:12px;border:2px solid #e1e8ed;border-radius:8px;font-family:inherit;font-size:.9rem;line-height:1.4;resize:vertical;min-height:80px;background:#fff;transition:border-color .3s ease,box-shadow .3s ease;box-sizing:border-box}.sentence-input:focus{outline:none;border-color:#3498db;box-shadow:0 0 0 3px #3498db1a}.sentence-input::placeholder{color:#95a5a6;font-style:italic}.sentence-info{display:flex;justify-content:space-between;align-items:center;margin-top:6px;font-size:.8rem}.char-count{color:#7f8c8d}.clear-sentence-btn{background:#e74c3c;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:.75rem;transition:background-color .2s ease}.clear-sentence-btn:hover{background:#c0392b}.clear-sentence-btn:active{background:#a93226}.ai-toggle-container{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px;border:2px solid #e9ecef;transition:all .3s ease}.ai-toggle-container:has(.ai-checkbox:checked){background:linear-gradient(135deg,#e3f2fd,#f3e5f5);border-color:#3498db}.ai-toggle{display:flex;align-items:center;cursor:pointer;font-weight:500;margin-bottom:8px}.ai-checkbox{width:20px;height:20px;margin-right:12px;cursor:pointer;accent-color:#3498db}.ai-label{font-size:1rem;color:#2c3e50;-webkit-user-select:none;user-select:none}.ai-status{color:#27ae60;font-weight:600;font-size:.9rem}.ai-description{margin:0;font-size:.85rem;color:#6c757d;line-height:1.4;padding-left:32px}.puzzle-controls{display:flex;gap:15px;margin-bottom:20px;justify-content:center}.control-btn{padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-weight:600;transition:background-color .3s ease}.control-btn:disabled{background:#bdc3c7!important;color:#7f8c8d!important;cursor:not-allowed;opacity:.7}.generate-btn{background:#27ae60;color:#fff}.generate-btn:hover{background:#229954}.generate-btn:disabled{background:#bdc3c7;cursor:not-allowed}.reset-btn{background:#e74c3c;color:#fff}.reset-btn:hover{background:#c0392b}.reveal-btn{background:#f39c12;color:#fff}.reveal-btn:hover{background:#e67e22}.loading-spinner{display:flex;flex-direction:column;align-items:center;padding:40px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:15px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-message{color:#7f8c8d;font-size:1.1rem}.puzzle-info{display:flex;justify-content:space-between;align-items:center;margin:20px 0 10px;padding:10px 15px;background:#f8f9fa;border-radius:6px;border-left:4px solid #3498db}.puzzle-stats{font-size:.9rem;color:#6c757d;font-weight:500}.ai-generated-badge{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:4px 12px;border-radius:15px;font-size:.8rem;font-weight:600;text-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 2px 4px #0000001a}.puzzle-layout{display:grid;grid-template-columns:1fr 300px;gap:30px;margin-top:20px}@media (max-width: 768px){.puzzle-layout{grid-template-columns:1fr;gap:20px}.puzzle-info{flex-direction:column;gap:8px;text-align:center}.ai-toggle-container{padding:12px}.ai-description{padding-left:0;text-align:center}}.puzzle-container{display:flex;justify-content:center}.puzzle-grid{display:grid;gap:0;margin:0 auto;width:fit-content;height:fit-content}.grid-cell{width:35px;height:35px;position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:#fff}.grid-cell:before{content:"";position:absolute;top:0;left:0;right:-1px;bottom:-1px;border:1px solid #2c3e50;pointer-events:none;z-index:10}.black-cell{background:#f0f0f0}.black-cell:before{background:#f0f0f0;border:1px solid #2c3e50}.white-cell{background:#fff}.empty-cell{background:transparent;border:none;visibility:hidden}.empty-cell:before{display:none}.cell-input{width:100%;height:100%;border:none!important;text-align:center;font-size:16px;font-weight:700;background:transparent;outline:none;text-transform:uppercase;position:relative;z-index:5}.cell-input:focus{background:#e8f4fd;box-shadow:inset 0 0 0 2px #3498db}.cell-number{position:absolute;top:1px;left:2px;font-size:10px;font-weight:700;color:#2c3e50;line-height:1;z-index:15;pointer-events:none}.solution-text{color:#2c3e50!important;font-weight:700!important;background:#fff!important}.solution-text:disabled{opacity:1!important;cursor:default}.grid-cell .solution-text{border:none!important;background:#fff!important}.clue-list{background:#f8f9fa;padding:20px;border-radius:8px;max-height:600px;overflow-y:auto}.clue-section{margin-bottom:25px}.clue-section h4{color:#2c3e50;margin-bottom:15px;font-size:1.2rem;border-bottom:2px solid #3498db;padding-bottom:5px}.clue-section ol{padding-left:0;list-style:none}.clue-item{display:flex;margin-bottom:8px;padding:8px;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.clue-item:hover{background:#e9ecef}.clue-number{font-weight:700;color:#3498db;margin-right:10px;min-width:25px}.clue-text{flex:1;color:#2c3e50}.error-message{background:#f8d7da;color:#721c24;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #f5c6cb}.success-message{background:#d4edda;color:#155724;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #c3e6cb;text-align:center;font-weight:600}
         | 
    	
        crossword-app/backend-py/public/assets/index-DyT-gQda.css
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            .crossword-app{max-width:1200px;margin:0 auto;padding:20px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.app-header{text-align:center;margin-bottom:30px}.app-title{color:#2c3e50;font-size:2.5rem;margin-bottom:10px}.topic-selector{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.topic-selector h3{margin-top:0;color:#2c3e50}.topic-buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:15px}.topic-btn{padding:8px 16px;border:2px solid #3498db;background:#fff;color:#3498db;border-radius:20px;cursor:pointer;transition:all .3s ease;font-weight:500}.topic-btn:hover,.topic-btn.selected{background:#3498db;color:#fff}.selected-count{color:#7f8c8d;font-size:.9rem;margin:0}.sentence-input-container{margin-top:20px;margin-bottom:15px}.sentence-label{display:block;margin-bottom:8px;color:#2c3e50;font-weight:500;font-size:.95rem}.sentence-input{width:100%;padding:12px;border:2px solid #e1e8ed;border-radius:8px;font-family:inherit;font-size:.9rem;line-height:1.4;resize:vertical;min-height:80px;background:#fff;transition:border-color .3s ease,box-shadow .3s ease;box-sizing:border-box}.sentence-input:focus{outline:none;border-color:#3498db;box-shadow:0 0 0 3px #3498db1a}.sentence-input::placeholder{color:#95a5a6;font-style:italic}.sentence-info{display:flex;justify-content:space-between;align-items:center;margin-top:6px;font-size:.8rem}.char-count{color:#7f8c8d}.clear-sentence-btn{background:#e74c3c;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:.75rem;transition:background-color .2s ease}.clear-sentence-btn:hover{background:#c0392b}.clear-sentence-btn:active{background:#a93226}.multi-theme-toggle-container{margin-top:20px;margin-bottom:15px;padding:15px;background:#f0f4f8;border:1px solid #e1e8ed;border-radius:8px}.multi-theme-toggle{display:flex;align-items:center;cursor:pointer;margin-bottom:8px}.multi-theme-checkbox{width:18px;height:18px;margin-right:10px;cursor:pointer;accent-color:#3498db}.multi-theme-label{font-weight:500;color:#2c3e50;font-size:.95rem;-webkit-user-select:none;user-select:none}.multi-theme-description{margin:0;font-size:.85rem;color:#5a6c7d;line-height:1.4;font-style:italic;padding-left:28px}.ai-toggle-container{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px;border:2px solid #e9ecef;transition:all .3s ease}.ai-toggle-container:has(.ai-checkbox:checked){background:linear-gradient(135deg,#e3f2fd,#f3e5f5);border-color:#3498db}.ai-toggle{display:flex;align-items:center;cursor:pointer;font-weight:500;margin-bottom:8px}.ai-checkbox{width:20px;height:20px;margin-right:12px;cursor:pointer;accent-color:#3498db}.ai-label{font-size:1rem;color:#2c3e50;-webkit-user-select:none;user-select:none}.ai-status{color:#27ae60;font-weight:600;font-size:.9rem}.ai-description{margin:0;font-size:.85rem;color:#6c757d;line-height:1.4;padding-left:32px}.puzzle-controls{display:flex;gap:15px;margin-bottom:20px;justify-content:center}.control-btn{padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-weight:600;transition:background-color .3s ease}.control-btn:disabled{background:#bdc3c7!important;color:#7f8c8d!important;cursor:not-allowed;opacity:.7}.generate-btn{background:#27ae60;color:#fff}.generate-btn:hover{background:#229954}.generate-btn:disabled{background:#bdc3c7;cursor:not-allowed}.reset-btn{background:#e74c3c;color:#fff}.reset-btn:hover{background:#c0392b}.reveal-btn{background:#f39c12;color:#fff}.reveal-btn:hover{background:#e67e22}.loading-spinner{display:flex;flex-direction:column;align-items:center;padding:40px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:15px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-message{color:#7f8c8d;font-size:1.1rem}.puzzle-info{display:flex;justify-content:space-between;align-items:center;margin:20px 0 10px;padding:10px 15px;background:#f8f9fa;border-radius:6px;border-left:4px solid #3498db}.puzzle-stats{font-size:.9rem;color:#6c757d;font-weight:500}.ai-generated-badge{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:4px 12px;border-radius:15px;font-size:.8rem;font-weight:600;text-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 2px 4px #0000001a}.puzzle-layout{display:grid;grid-template-columns:1fr 300px;gap:30px;margin-top:20px}@media (max-width: 768px){.puzzle-layout{grid-template-columns:1fr;gap:20px}.puzzle-info{flex-direction:column;gap:8px;text-align:center}.ai-toggle-container{padding:12px}.ai-description{padding-left:0;text-align:center}}.puzzle-container{display:flex;justify-content:center}.puzzle-grid{display:grid;gap:0;margin:0 auto;width:fit-content;height:fit-content}.grid-cell{width:35px;height:35px;position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:#fff}.grid-cell:before{content:"";position:absolute;top:0;left:0;right:-1px;bottom:-1px;border:1px solid #2c3e50;pointer-events:none;z-index:10}.black-cell{background:#f0f0f0}.black-cell:before{background:#f0f0f0;border:1px solid #2c3e50}.white-cell{background:#fff}.empty-cell{background:transparent;border:none;visibility:hidden}.empty-cell:before{display:none}.cell-input{width:100%;height:100%;border:none!important;text-align:center;font-size:16px;font-weight:700;background:transparent;outline:none;text-transform:uppercase;position:relative;z-index:5}.cell-input:focus{background:#e8f4fd;box-shadow:inset 0 0 0 2px #3498db}.cell-number{position:absolute;top:1px;left:2px;font-size:10px;font-weight:700;color:#2c3e50;line-height:1;z-index:15;pointer-events:none}.solution-text{color:#2c3e50!important;font-weight:700!important;background:#fff!important}.solution-text:disabled{opacity:1!important;cursor:default}.grid-cell .solution-text{border:none!important;background:#fff!important}.clue-list{background:#f8f9fa;padding:20px;border-radius:8px;max-height:600px;overflow-y:auto}.clue-section{margin-bottom:25px}.clue-section h4{color:#2c3e50;margin-bottom:15px;font-size:1.2rem;border-bottom:2px solid #3498db;padding-bottom:5px}.clue-section ol{padding-left:0;list-style:none}.clue-item{display:flex;margin-bottom:8px;padding:8px;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.clue-item:hover{background:#e9ecef}.clue-number{font-weight:700;color:#3498db;margin-right:10px;min-width:25px}.clue-text{flex:1;color:#2c3e50}.error-message{background:#f8d7da;color:#721c24;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #f5c6cb}.success-message{background:#d4edda;color:#155724;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #c3e6cb;text-align:center;font-weight:600}
         | 
    	
        crossword-app/backend-py/public/assets/index-V4v18wFW.css
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            .crossword-app{max-width:1200px;margin:0 auto;padding:20px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.app-header{text-align:center;margin-bottom:30px}.app-title{color:#2c3e50;font-size:2.5rem;margin-bottom:10px}.topic-selector{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.topic-selector h3{margin-top:0;color:#2c3e50}.topic-buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:15px}.topic-btn{padding:8px 16px;border:2px solid #3498db;background:#fff;color:#3498db;border-radius:20px;cursor:pointer;transition:all .3s ease;font-weight:500}.topic-btn:hover,.topic-btn.selected{background:#3498db;color:#fff}.selected-count{color:#7f8c8d;font-size:.9rem;margin:0}.ai-toggle-container{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px;border:2px solid #e9ecef;transition:all .3s ease}.ai-toggle-container:has(.ai-checkbox:checked){background:linear-gradient(135deg,#e3f2fd,#f3e5f5);border-color:#3498db}.ai-toggle{display:flex;align-items:center;cursor:pointer;font-weight:500;margin-bottom:8px}.ai-checkbox{width:20px;height:20px;margin-right:12px;cursor:pointer;accent-color:#3498db}.ai-label{font-size:1rem;color:#2c3e50;-webkit-user-select:none;user-select:none}.ai-status{color:#27ae60;font-weight:600;font-size:.9rem}.ai-description{margin:0;font-size:.85rem;color:#6c757d;line-height:1.4;padding-left:32px}.puzzle-controls{display:flex;gap:15px;margin-bottom:20px;justify-content:center}.control-btn{padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-weight:600;transition:background-color .3s ease}.control-btn:disabled{background:#bdc3c7!important;color:#7f8c8d!important;cursor:not-allowed;opacity:.7}.generate-btn{background:#27ae60;color:#fff}.generate-btn:hover{background:#229954}.generate-btn:disabled{background:#bdc3c7;cursor:not-allowed}.reset-btn{background:#e74c3c;color:#fff}.reset-btn:hover{background:#c0392b}.reveal-btn{background:#f39c12;color:#fff}.reveal-btn:hover{background:#e67e22}.loading-spinner{display:flex;flex-direction:column;align-items:center;padding:40px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:15px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-message{color:#7f8c8d;font-size:1.1rem}.puzzle-info{display:flex;justify-content:space-between;align-items:center;margin:20px 0 10px;padding:10px 15px;background:#f8f9fa;border-radius:6px;border-left:4px solid #3498db}.puzzle-stats{font-size:.9rem;color:#6c757d;font-weight:500}.ai-generated-badge{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:4px 12px;border-radius:15px;font-size:.8rem;font-weight:600;text-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 2px 4px #0000001a}.puzzle-layout{display:grid;grid-template-columns:1fr 300px;gap:30px;margin-top:20px}@media (max-width: 768px){.puzzle-layout{grid-template-columns:1fr;gap:20px}.puzzle-info{flex-direction:column;gap:8px;text-align:center}.ai-toggle-container{padding:12px}.ai-description{padding-left:0;text-align:center}}.puzzle-container{display:flex;justify-content:center}.puzzle-grid{display:grid;gap:0;margin:0 auto;width:fit-content;height:fit-content}.grid-cell{width:35px;height:35px;position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:#fff}.grid-cell:before{content:"";position:absolute;top:0;left:0;right:-1px;bottom:-1px;border:1px solid #2c3e50;pointer-events:none;z-index:10}.black-cell{background:#f0f0f0}.black-cell:before{background:#f0f0f0;border:1px solid #2c3e50}.white-cell{background:#fff}.empty-cell{background:transparent;border:none;visibility:hidden}.empty-cell:before{display:none}.cell-input{width:100%;height:100%;border:none!important;text-align:center;font-size:16px;font-weight:700;background:transparent;outline:none;text-transform:uppercase;position:relative;z-index:5}.cell-input:focus{background:#e8f4fd;box-shadow:inset 0 0 0 2px #3498db}.cell-number{position:absolute;top:1px;left:2px;font-size:10px;font-weight:700;color:#2c3e50;line-height:1;z-index:15;pointer-events:none}.solution-text{color:#2c3e50!important;font-weight:700!important;background:#fff!important}.solution-text:disabled{opacity:1!important;cursor:default}.grid-cell .solution-text{border:none!important;background:#fff!important}.clue-list{background:#f8f9fa;padding:20px;border-radius:8px;max-height:600px;overflow-y:auto}.clue-section{margin-bottom:25px}.clue-section h4{color:#2c3e50;margin-bottom:15px;font-size:1.2rem;border-bottom:2px solid #3498db;padding-bottom:5px}.clue-section ol{padding-left:0;list-style:none}.clue-item{display:flex;margin-bottom:8px;padding:8px;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.clue-item:hover{background:#e9ecef}.clue-number{font-weight:700;color:#3498db;margin-right:10px;min-width:25px}.clue-text{flex:1;color:#2c3e50}.error-message{background:#f8d7da;color:#721c24;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #f5c6cb}.success-message{background:#d4edda;color:#155724;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #c3e6cb;text-align:center;font-weight:600}
         | 
    	
        crossword-app/backend-py/public/assets/index-uK3VdD5a.js
    ADDED
    
    | @@ -0,0 +1,10 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import{r as m,a as T,R as _}from"./vendor-nf7bT_Uh.js";(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))l(t);new MutationObserver(t=>{for(const s of t)if(s.type==="childList")for(const i of s.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&l(i)}).observe(document,{childList:!0,subtree:!0});function o(t){const s={};return t.integrity&&(s.integrity=t.integrity),t.referrerPolicy&&(s.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?s.credentials="include":t.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function l(t){if(t.ep)return;t.ep=!0;const s=o(t);fetch(t.href,s)}})();var P={exports:{}},b={};/**
         | 
| 2 | 
            +
             * @license React
         | 
| 3 | 
            +
             * react-jsx-runtime.production.min.js
         | 
| 4 | 
            +
             *
         | 
| 5 | 
            +
             * Copyright (c) Facebook, Inc. and its affiliates.
         | 
| 6 | 
            +
             *
         | 
| 7 | 
            +
             * This source code is licensed under the MIT license found in the
         | 
| 8 | 
            +
             * LICENSE file in the root directory of this source tree.
         | 
| 9 | 
            +
             */var $=m,O=Symbol.for("react.element"),L=Symbol.for("react.fragment"),A=Object.prototype.hasOwnProperty,F=$.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,D={key:!0,ref:!0,__self:!0,__source:!0};function k(n,a,o){var l,t={},s=null,i=null;o!==void 0&&(s=""+o),a.key!==void 0&&(s=""+a.key),a.ref!==void 0&&(i=a.ref);for(l in a)A.call(a,l)&&!D.hasOwnProperty(l)&&(t[l]=a[l]);if(n&&n.defaultProps)for(l in a=n.defaultProps,a)t[l]===void 0&&(t[l]=a[l]);return{$$typeof:O,type:n,key:s,ref:i,props:t,_owner:F.current}}b.Fragment=L;b.jsx=k;b.jsxs=k;P.exports=b;var e=P.exports,C={},S=T;C.createRoot=S.createRoot,C.hydrateRoot=S.hydrateRoot;const G=({onTopicsChange:n,availableTopics:a=[],selectedTopics:o=[],customSentence:l="",onSentenceChange:t,multiTheme:s=!0,onMultiThemeChange:i})=>{const g=c=>{const y=o.includes(c)?o.filter(p=>p!==c):[...o,c];n(y)};return e.jsxs("div",{className:"topic-selector",children:[e.jsx("h3",{children:"Select Topics"}),e.jsx("div",{className:"topic-buttons",children:a.map(c=>e.jsx("button",{className:`topic-btn ${o.includes(c.name)?"selected":""}`,onClick:()=>g(c.name),children:c.name},c.id))}),e.jsxs("div",{className:"sentence-input-container",children:[e.jsx("label",{htmlFor:"custom-sentence",className:"sentence-label",children:"Custom Sentence (optional)"}),e.jsx("textarea",{id:"custom-sentence",className:"sentence-input",value:l,onChange:c=>t&&t(c.target.value),placeholder:"Enter a sentence to influence word selection...",rows:"3",maxLength:"200"}),e.jsxs("div",{className:"sentence-info",children:[e.jsxs("span",{className:"char-count",children:[l.length,"/200 characters"]}),l&&e.jsx("button",{type:"button",className:"clear-sentence-btn",onClick:()=>t&&t(""),title:"Clear sentence",children:"Clear"})]})]}),e.jsxs("div",{className:"multi-theme-toggle-container",children:[e.jsxs("label",{className:"multi-theme-toggle",children:[e.jsx("input",{type:"checkbox",checked:s,onChange:c=>i&&i(c.target.checked),className:"multi-theme-checkbox"}),e.jsx("span",{className:"multi-theme-label",children:"🎯 Use Multi-Theme Processing"})]}),e.jsx("p",{className:"multi-theme-description",children:s?"AI will process each theme separately and balance results":"AI will blend all themes into a single concept"})]}),e.jsxs("p",{className:"selected-count",children:[o.length," topic",o.length!==1?"s":""," selected"]})]})},B=({grid:n,clues:a,showSolution:o,onCellChange:l})=>{const[t,s]=m.useState({}),i=(d,r,u)=>{const h=`${d}-${r}`,x={...t,[h]:u.toUpperCase()};s(x),l&&l(d,r,u)},g=(d,r)=>{if(o&&!c(d,r))return n[d][r];const u=`${d}-${r}`;return t[u]||""},c=(d,r)=>n[d][r]===".",y=(d,r)=>{if(!a)return null;const u=a.find(h=>h.position.row===d&&h.position.col===r);return u?u.number:null};if(!n||n.length===0)return e.jsx("div",{className:"puzzle-grid",children:"No puzzle loaded"});const p=n.length,f=n[0]?n[0].length:0;return e.jsx("div",{className:"puzzle-container",children:e.jsx("div",{className:"puzzle-grid",style:{gridTemplateColumns:`repeat(${f}, 35px)`,gridTemplateRows:`repeat(${p}, 35px)`},children:n.map((d,r)=>d.map((u,h)=>{const x=y(r,h);return c(r,h)?e.jsx("div",{className:"grid-cell empty-cell",style:{visibility:"hidden"}},`${r}-${h}`):e.jsxs("div",{className:"grid-cell white-cell",children:[x&&e.jsx("span",{className:"cell-number",children:x}),e.jsx("input",{type:"text",maxLength:"1",value:g(r,h),onChange:j=>i(r,h,j.target.value),className:`cell-input ${o?"solution-text":""}`,disabled:o})]},`${r}-${h}`)}))})})},M=({clues:n=[]})=>{const a=n.filter(t=>t.direction==="across"),o=n.filter(t=>t.direction==="down"),l=({title:t,clueList:s})=>e.jsxs("div",{className:"clue-section",children:[e.jsx("h4",{children:t}),e.jsx("ol",{children:s.map(i=>e.jsxs("li",{className:"clue-item",children:[e.jsx("span",{className:"clue-number",children:i.number}),e.jsx("span",{className:"clue-text",children:i.text})]},`${i.number}-${i.direction}`))})]});return e.jsxs("div",{className:"clue-list",children:[e.jsx(l,{title:"Across",clueList:a}),e.jsx(l,{title:"Down",clueList:o})]})},U=({message:n="Generating puzzle..."})=>e.jsxs("div",{className:"loading-spinner",children:[e.jsx("div",{className:"spinner"}),e.jsx("p",{className:"loading-message",children:n})]}),J=()=>{const[n,a]=m.useState(null),[o,l]=m.useState(!1),[t,s]=m.useState(null),[i,g]=m.useState([]),c="",y=m.useCallback(async()=>{try{l(!0);const r=await fetch(`${c}/api/topics`);if(!r.ok)throw new Error("Failed to fetch topics");const u=await r.json();g(u)}catch(r){s(r.message)}finally{l(!1)}},[c]),p=m.useCallback(async(r,u="medium",h=!1,x="",z=!0)=>{try{l(!0),s(null);const j=await fetch(`${c}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({topics:r,difficulty:u,useAI:h,...x&&{customSentence:x},multiTheme:z})});if(!j.ok){const w=await j.json().catch(()=>({}));throw new Error(w.message||"Failed to generate puzzle")}const v=await j.json();return a(v),v}catch(j){return s(j.message),null}finally{l(!1)}},[c]),f=m.useCallback(async r=>{try{const u=await fetch(`${c}/api/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({puzzle:n,answers:r})});if(!u.ok)throw new Error("Failed to validate answers");return await u.json()}catch(u){return s(u.message),null}},[c,n]),d=m.useCallback(()=>{a(null),s(null)},[]);return{puzzle:n,loading:o,error:t,topics:i,fetchTopics:y,generatePuzzle:p,validateAnswers:f,resetPuzzle:d}};function q(){const[n,a]=m.useState([]),[o,l]=m.useState("medium"),[t,s]=m.useState(!1),[i,g]=m.useState(""),[c,y]=m.useState(!0),{puzzle:p,loading:f,error:d,topics:r,fetchTopics:u,generatePuzzle:h,resetPuzzle:x}=J();m.useEffect(()=>{u()},[u]);const z=async()=>{if(n.length===0){alert("Please select at least one topic");return}await h(n,o,!1,i,c)},j=N=>{a(N)},v=N=>{g(N)},w=N=>{y(N)},R=()=>{x(),a([]),s(!1),l("medium"),g(""),y(!0)},E=()=>{s(!0)};return e.jsxs("div",{className:"crossword-app",children:[e.jsxs("header",{className:"app-header",children:[e.jsx("h1",{className:"app-title",children:"Crossword Puzzle Generator"}),e.jsx("p",{children:"Select topics and generate your custom crossword puzzle!"})]}),e.jsx(G,{onTopicsChange:j,availableTopics:r,selectedTopics:n,customSentence:i,onSentenceChange:v,multiTheme:c,onMultiThemeChange:w}),e.jsxs("div",{className:"puzzle-controls",children:[e.jsxs("select",{value:o,onChange:N=>l(N.target.value),className:"control-btn",children:[e.jsx("option",{value:"easy",children:"Easy"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"hard",children:"Hard"})]}),e.jsx("button",{onClick:z,disabled:f||n.length===0,className:"control-btn generate-btn",children:f?"Generating...":"Generate Puzzle"}),e.jsx("button",{onClick:R,className:"control-btn reset-btn",children:"Reset"}),p&&!t&&e.jsx("button",{onClick:E,className:"control-btn reveal-btn",children:"Reveal Solution"})]}),d&&e.jsxs("div",{className:"error-message",children:["Error: ",d]}),f&&e.jsx(U,{}),p&&!f&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"puzzle-info",children:e.jsxs("span",{className:"puzzle-stats",children:[p.metadata.wordCount," words • ",p.metadata.size,"×",p.metadata.size," grid"]})}),e.jsxs("div",{className:"puzzle-layout",children:[e.jsx(B,{grid:p.grid,clues:p.clues,showSolution:t}),e.jsx(M,{clues:p.clues})]})]}),!p&&!f&&!d&&e.jsx("div",{style:{textAlign:"center",padding:"40px",color:"#7f8c8d"},children:'Select topics and click "Generate Puzzle" to start!'})]})}C.createRoot(document.getElementById("root")).render(e.jsx(_.StrictMode,{children:e.jsx(q,{})}));
         | 
| 10 | 
            +
            //# sourceMappingURL=index-uK3VdD5a.js.map
         | 
    	
        crossword-app/backend-py/public/assets/index-uK3VdD5a.js.map
    ADDED
    
    | @@ -0,0 +1 @@ | |
|  | 
|  | |
| 1 | 
            +
            {"version":3,"file":"index-uK3VdD5a.js","sources":["../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/react-dom/client.js","../../src/components/TopicSelector.jsx","../../src/components/PuzzleGrid.jsx","../../src/components/ClueList.jsx","../../src/components/LoadingSpinner.jsx","../../src/hooks/useCrossword.js","../../src/App.jsx","../../src/main.jsx"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n  module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n  module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n  exports.createRoot = m.createRoot;\n  exports.hydrateRoot = m.hydrateRoot;\n} else {\n  var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n  exports.createRoot = function(c, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.createRoot(c, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n  exports.hydrateRoot = function(c, h, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.hydrateRoot(c, h, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n}\n","import React from 'react';\n\nconst TopicSelector = ({ \n  onTopicsChange, \n  availableTopics = [], \n  selectedTopics = [],\n  customSentence = '',\n  onSentenceChange,\n  multiTheme = true,\n  onMultiThemeChange\n}) => {\n  const handleTopicToggle = (topic) => {\n    const newSelectedTopics = selectedTopics.includes(topic)\n      ? selectedTopics.filter(t => t !== topic)\n      : [...selectedTopics, topic];\n    \n    onTopicsChange(newSelectedTopics);\n  };\n\n  return (\n    <div className=\"topic-selector\">\n      <h3>Select Topics</h3>\n      <div className=\"topic-buttons\">\n        {availableTopics.map(topic => (\n          <button\n            key={topic.id}\n            className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}\n            onClick={() => handleTopicToggle(topic.name)}\n          >\n            {topic.name}\n          </button>\n        ))}\n      </div>\n      \n      <div className=\"sentence-input-container\">\n        <label htmlFor=\"custom-sentence\" className=\"sentence-label\">\n          Custom Sentence (optional)\n        </label>\n        <textarea\n          id=\"custom-sentence\"\n          className=\"sentence-input\"\n          value={customSentence}\n          onChange={(e) => onSentenceChange && onSentenceChange(e.target.value)}\n          placeholder=\"Enter a sentence to influence word selection...\"\n          rows=\"3\"\n          maxLength=\"200\"\n        />\n        <div className=\"sentence-info\">\n          <span className=\"char-count\">{customSentence.length}/200 characters</span>\n          {customSentence && (\n            <button \n              type=\"button\"\n              className=\"clear-sentence-btn\"\n              onClick={() => onSentenceChange && onSentenceChange('')}\n              title=\"Clear sentence\"\n            >\n              Clear\n            </button>\n          )}\n        </div>\n      </div>\n      \n      <div className=\"multi-theme-toggle-container\">\n        <label className=\"multi-theme-toggle\">\n          <input\n            type=\"checkbox\"\n            checked={multiTheme}\n            onChange={(e) => onMultiThemeChange && onMultiThemeChange(e.target.checked)}\n            className=\"multi-theme-checkbox\"\n          />\n          <span className=\"multi-theme-label\">\n            🎯 Use Multi-Theme Processing\n          </span>\n        </label>\n        <p className=\"multi-theme-description\">\n          {multiTheme \n            ? \"AI will process each theme separately and balance results\" \n            : \"AI will blend all themes into a single concept\"\n          }\n        </p>\n      </div>\n      \n      <p className=\"selected-count\">\n        {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected\n      </p>\n    </div>\n  );\n};\n\nexport default TopicSelector;","import React, { useState } from 'react';\n\nconst PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {\n  const [userAnswers, setUserAnswers] = useState({});\n\n  const handleCellInput = (row, col, value) => {\n    const key = `${row}-${col}`;\n    const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };\n    setUserAnswers(newAnswers);\n    onCellChange && onCellChange(row, col, value);\n  };\n\n  const getCellValue = (row, col) => {\n    if (showSolution && !isBlackCell(row, col)) {\n      return grid[row][col];\n    }\n    const key = `${row}-${col}`;\n    return userAnswers[key] || '';\n  };\n\n  const isBlackCell = (row, col) => {\n    return grid[row][col] === '.';\n  };\n\n  const getCellNumber = (row, col) => {\n    if (!clues) return null;\n    const clue = clues.find(c => c.position.row === row && c.position.col === col);\n    return clue ? clue.number : null;\n  };\n\n  if (!grid || grid.length === 0) {\n    return <div className=\"puzzle-grid\">No puzzle loaded</div>;\n  }\n\n  const gridRows = grid.length;\n  const gridCols = grid[0] ? grid[0].length : 0;\n\n  return (\n    <div className=\"puzzle-container\">\n      <div \n        className=\"puzzle-grid\"\n        style={{\n          gridTemplateColumns: `repeat(${gridCols}, 35px)`,\n          gridTemplateRows: `repeat(${gridRows}, 35px)`\n        }}\n      >\n        {grid.map((row, rowIndex) =>\n          row.map((cell, colIndex) => {\n            const cellNumber = getCellNumber(rowIndex, colIndex);\n            const isBlack = isBlackCell(rowIndex, colIndex);\n            \n            // Only render cells that contain letters (not black/unused cells)\n            if (isBlack) {\n              return (\n                <div\n                  key={`${rowIndex}-${colIndex}`}\n                  className=\"grid-cell empty-cell\"\n                  style={{ visibility: 'hidden' }}\n                >\n                </div>\n              );\n            }\n            \n            return (\n              <div\n                key={`${rowIndex}-${colIndex}`}\n                className=\"grid-cell white-cell\"\n              >\n                {cellNumber && <span className=\"cell-number\">{cellNumber}</span>}\n                <input\n                  type=\"text\"\n                  maxLength=\"1\"\n                  value={getCellValue(rowIndex, colIndex)}\n                  onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}\n                  className={`cell-input ${showSolution ? 'solution-text' : ''}`}\n                  disabled={showSolution}\n                />\n              </div>\n            );\n          })\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default PuzzleGrid;","import React from 'react';\n\nconst ClueList = ({ clues = [] }) => {\n  const acrossClues = clues.filter(clue => clue.direction === 'across');\n  const downClues = clues.filter(clue => clue.direction === 'down');\n\n  const ClueSection = ({ title, clueList }) => (\n    <div className=\"clue-section\">\n      <h4>{title}</h4>\n      <ol>\n        {clueList.map(clue => (\n          <li key={`${clue.number}-${clue.direction}`} className=\"clue-item\">\n            <span className=\"clue-number\">{clue.number}</span>\n            <span className=\"clue-text\">{clue.text}</span>\n          </li>\n        ))}\n      </ol>\n    </div>\n  );\n\n  return (\n    <div className=\"clue-list\">\n      <ClueSection title=\"Across\" clueList={acrossClues} />\n      <ClueSection title=\"Down\" clueList={downClues} />\n    </div>\n  );\n};\n\nexport default ClueList;","import React from 'react';\n\nconst LoadingSpinner = ({ message = \"Generating puzzle...\" }) => {\n  return (\n    <div className=\"loading-spinner\">\n      <div className=\"spinner\"></div>\n      <p className=\"loading-message\">{message}</p>\n    </div>\n  );\n};\n\nexport default LoadingSpinner;","import { useState, useCallback } from 'react';\n\nconst useCrossword = () => {\n  const [puzzle, setPuzzle] = useState(null);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [topics, setTopics] = useState([]);\n\n  const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || (import.meta.env.PROD ? '' : 'http://localhost:3000');\n\n  const fetchTopics = useCallback(async () => {\n    try {\n      setLoading(true);\n      const response = await fetch(`${API_BASE_URL}/api/topics`);\n      if (!response.ok) throw new Error('Failed to fetch topics');\n      const data = await response.json();\n      setTopics(data);\n    } catch (err) {\n      setError(err.message);\n    } finally {\n      setLoading(false);\n    }\n  }, [API_BASE_URL]);\n\n  const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false, customSentence = '', multiTheme = true) => {\n    try {\n      setLoading(true);\n      setError(null);\n      \n      const response = await fetch(`${API_BASE_URL}/api/generate`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          topics: selectedTopics,\n          difficulty,\n          useAI,\n          ...(customSentence && { customSentence }),\n          multiTheme\n        })\n      });\n\n      if (!response.ok) {\n        const errorData = await response.json().catch(() => ({}));\n        throw new Error(errorData.message || 'Failed to generate puzzle');\n      }\n      \n      const puzzleData = await response.json();\n      setPuzzle(puzzleData);\n      return puzzleData;\n    } catch (err) {\n      setError(err.message);\n      return null;\n    } finally {\n      setLoading(false);\n    }\n  }, [API_BASE_URL]);\n\n  const validateAnswers = useCallback(async (userAnswers) => {\n    try {\n      const response = await fetch(`${API_BASE_URL}/api/validate`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          puzzle: puzzle,\n          answers: userAnswers\n        })\n      });\n\n      if (!response.ok) throw new Error('Failed to validate answers');\n      \n      return await response.json();\n    } catch (err) {\n      setError(err.message);\n      return null;\n    }\n  }, [API_BASE_URL, puzzle]);\n\n  const resetPuzzle = useCallback(() => {\n    setPuzzle(null);\n    setError(null);\n  }, []);\n\n  return {\n    puzzle,\n    loading,\n    error,\n    topics,\n    fetchTopics,\n    generatePuzzle,\n    validateAnswers,\n    resetPuzzle\n  };\n};\n\nexport default useCrossword;","import React, { useState, useEffect } from 'react';\nimport TopicSelector from './components/TopicSelector';\nimport PuzzleGrid from './components/PuzzleGrid';\nimport ClueList from './components/ClueList';\nimport LoadingSpinner from './components/LoadingSpinner';\nimport useCrossword from './hooks/useCrossword';\nimport './styles/puzzle.css';\n\nfunction App() {\n  const [selectedTopics, setSelectedTopics] = useState([]);\n  const [difficulty, setDifficulty] = useState('medium');\n  const [showSolution, setShowSolution] = useState(false);\n  const [customSentence, setCustomSentence] = useState('');\n  const [multiTheme, setMultiTheme] = useState(true);\n  \n  const {\n    puzzle,\n    loading,\n    error,\n    topics,\n    fetchTopics,\n    generatePuzzle,\n    resetPuzzle\n  } = useCrossword();\n\n  useEffect(() => {\n    fetchTopics();\n  }, [fetchTopics]);\n\n  const handleGeneratePuzzle = async () => {\n    if (selectedTopics.length === 0) {\n      alert('Please select at least one topic');\n      return;\n    }\n    \n    await generatePuzzle(selectedTopics, difficulty, false, customSentence, multiTheme);\n  };\n\n  const handleTopicsChange = (topics) => {\n    setSelectedTopics(topics);\n  };\n\n  const handleSentenceChange = (sentence) => {\n    setCustomSentence(sentence);\n  };\n\n  const handleMultiThemeChange = (enabled) => {\n    setMultiTheme(enabled);\n  };\n\n\n  const handleReset = () => {\n    resetPuzzle();\n    setSelectedTopics([]);\n    setShowSolution(false);\n    setDifficulty('medium');\n    setCustomSentence('');\n    setMultiTheme(true);\n  };\n\n  const handleRevealSolution = () => {\n    setShowSolution(true);\n  };\n\n  return (\n    <div className=\"crossword-app\">\n      <header className=\"app-header\">\n        <h1 className=\"app-title\">Crossword Puzzle Generator</h1>\n        <p>Select topics and generate your custom crossword puzzle!</p>\n      </header>\n\n      <TopicSelector \n        onTopicsChange={handleTopicsChange}\n        availableTopics={topics}\n        selectedTopics={selectedTopics}\n        customSentence={customSentence}\n        onSentenceChange={handleSentenceChange}\n        multiTheme={multiTheme}\n        onMultiThemeChange={handleMultiThemeChange}\n      />\n\n      <div className=\"puzzle-controls\">\n        <select \n          value={difficulty} \n          onChange={(e) => setDifficulty(e.target.value)}\n          className=\"control-btn\"\n        >\n          <option value=\"easy\">Easy</option>\n          <option value=\"medium\">Medium</option>\n          <option value=\"hard\">Hard</option>\n        </select>\n        \n        <button\n          onClick={handleGeneratePuzzle}\n          disabled={loading || selectedTopics.length === 0}\n          className=\"control-btn generate-btn\"\n        >\n          {loading ? 'Generating...' : 'Generate Puzzle'}\n        </button>\n        \n        <button\n          onClick={handleReset}\n          className=\"control-btn reset-btn\"\n        >\n          Reset\n        </button>\n        \n        {puzzle && !showSolution && (\n          <button\n            onClick={handleRevealSolution}\n            className=\"control-btn reveal-btn\"\n          >\n            Reveal Solution\n          </button>\n        )}\n      </div>\n\n      {error && (\n        <div className=\"error-message\">\n          Error: {error}\n        </div>\n      )}\n\n      {loading && <LoadingSpinner />}\n\n      {puzzle && !loading && (\n        <>\n          <div className=\"puzzle-info\">\n            <span className=\"puzzle-stats\">\n              {puzzle.metadata.wordCount} words • {puzzle.metadata.size}×{puzzle.metadata.size} grid\n            </span>\n          </div>\n          <div className=\"puzzle-layout\">\n            <PuzzleGrid \n              grid={puzzle.grid} \n              clues={puzzle.clues}\n              showSolution={showSolution}\n            />\n            <ClueList clues={puzzle.clues} />\n          </div>\n        </>\n      )}\n\n      {!puzzle && !loading && !error && (\n        <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>\n          Select topics and click \"Generate Puzzle\" to start!\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default App;","import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App.jsx'\n\nReactDOM.createRoot(document.getElementById('root')).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n)"],"names":["f","require$$0","k","l","m","n","p","q","c","g","b","d","e","h","reactJsxRuntime_production_min","jsxRuntimeModule","client","TopicSelector","onTopicsChange","availableTopics","selectedTopics","customSentence","onSentenceChange","multiTheme","onMultiThemeChange","handleTopicToggle","topic","newSelectedTopics","t","jsxs","jsx","PuzzleGrid","grid","clues","showSolution","onCellChange","userAnswers","setUserAnswers","useState","handleCellInput","row","col","value","key","newAnswers","getCellValue","isBlackCell","getCellNumber","clue","gridRows","gridCols","rowIndex","cell","colIndex","cellNumber","ClueList","acrossClues","downClues","ClueSection","title","clueList","LoadingSpinner","message","useCrossword","puzzle","setPuzzle","loading","setLoading","error","setError","topics","setTopics","API_BASE_URL","fetchTopics","useCallback","response","data","err","generatePuzzle","difficulty","useAI","errorData","puzzleData","validateAnswers","resetPuzzle","App","setSelectedTopics","setDifficulty","setShowSolution","setCustomSentence","setMultiTheme","useEffect","handleGeneratePuzzle","handleTopicsChange","handleSentenceChange","sentence","handleMultiThemeChange","enabled","handleReset","handleRevealSolution","Fragment","ReactDOM","React"],"mappings":";;;;;;;;GASa,IAAIA,EAAEC,EAAiBC,EAAE,OAAO,IAAI,eAAe,EAAEC,EAAE,OAAO,IAAI,gBAAgB,EAAEC,EAAE,OAAO,UAAU,eAAeC,EAAEL,EAAE,mDAAmD,kBAAkBM,EAAE,CAAC,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,EAAE,EAClP,SAASC,EAAEC,EAAE,EAAEC,EAAE,CAAC,IAAIC,EAAEC,EAAE,GAAGC,EAAE,KAAKC,EAAE,KAAcJ,IAAT,SAAaG,EAAE,GAAGH,GAAY,EAAE,MAAX,SAAiBG,EAAE,GAAG,EAAE,KAAc,EAAE,MAAX,SAAiBC,EAAE,EAAE,KAAK,IAAIH,KAAK,EAAEN,EAAE,KAAK,EAAEM,CAAC,GAAG,CAACJ,EAAE,eAAeI,CAAC,IAAIC,EAAED,CAAC,EAAE,EAAEA,CAAC,GAAG,GAAGF,GAAGA,EAAE,aAAa,IAAIE,KAAK,EAAEF,EAAE,aAAa,EAAWG,EAAED,CAAC,aAAIC,EAAED,CAAC,EAAE,EAAEA,CAAC,GAAG,MAAM,CAAC,SAASR,EAAE,KAAKM,EAAE,IAAII,EAAE,IAAIC,EAAE,MAAMF,EAAE,OAAON,EAAE,OAAO,CAAC,YAAkBF,EAAEW,EAAA,IAAYP,EAAEO,EAAA,KAAaP,ECPxWQ,EAAA,QAAiBd,uBCDfG,EAAIH,EAENe,EAAA,WAAqBZ,EAAE,WACvBY,EAAA,YAAsBZ,EAAE,YCH1B,MAAMa,EAAgB,CAAC,CACrB,eAAAC,EACA,gBAAAC,EAAkB,CAAA,EAClB,eAAAC,EAAiB,CAAA,EACjB,eAAAC,EAAiB,GACjB,iBAAAC,EACA,WAAAC,EAAa,GACb,mBAAAC,CACF,IAAM,CACJ,MAAMC,EAAqBC,GAAU,CACnC,MAAMC,EAAoBP,EAAe,SAASM,CAAK,EACnDN,EAAe,OAAOQ,GAAKA,IAAMF,CAAK,EACtC,CAAC,GAAGN,EAAgBM,CAAK,EAE7BR,EAAeS,CAAiB,CAClC,EAEA,OACEE,EAAAA,KAAC,MAAA,CAAI,UAAU,iBACb,SAAA,CAAAC,EAAAA,IAAC,MAAG,SAAA,eAAA,CAAa,QAChB,MAAA,CAAI,UAAU,gBACZ,SAAAX,EAAgB,IAAIO,GACnBI,EAAAA,IAAC,SAAA,CAEC,UAAW,aAAaV,EAAe,SAASM,EAAM,IAAI,EAAI,WAAa,EAAE,GAC7E,QAAS,IAAMD,EAAkBC,EAAM,IAAI,EAE1C,SAAAA,EAAM,IAAA,EAJFA,EAAM,EAAA,CAMd,EACH,EAEAG,EAAAA,KAAC,MAAA,CAAI,UAAU,2BACb,SAAA,CAAAC,MAAC,QAAA,CAAM,QAAQ,kBAAkB,UAAU,iBAAiB,SAAA,6BAE5D,EACAA,EAAAA,IAAC,WAAA,CACC,GAAG,kBACH,UAAU,iBACV,MAAOT,EACP,SAAWT,GAAMU,GAAoBA,EAAiBV,EAAE,OAAO,KAAK,EACpE,YAAY,kDACZ,KAAK,IACL,UAAU,KAAA,CAAA,EAEZiB,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAc,SAAA,CAAAR,EAAe,OAAO,iBAAA,EAAe,EAClEA,GACCS,EAAAA,IAAC,SAAA,CACC,KAAK,SACL,UAAU,qBACV,QAAS,IAAMR,GAAoBA,EAAiB,EAAE,EACtD,MAAM,iBACP,SAAA,OAAA,CAAA,CAED,CAAA,CAEJ,CAAA,EACF,EAEAO,EAAAA,KAAC,MAAA,CAAI,UAAU,+BACb,SAAA,CAAAA,EAAAA,KAAC,QAAA,CAAM,UAAU,qBACf,SAAA,CAAAC,EAAAA,IAAC,QAAA,CACC,KAAK,WACL,QAASP,EACT,SAAWX,GAAMY,GAAsBA,EAAmBZ,EAAE,OAAO,OAAO,EAC1E,UAAU,sBAAA,CAAA,EAEZkB,EAAAA,IAAC,OAAA,CAAK,UAAU,oBAAoB,SAAA,+BAAA,CAEpC,CAAA,EACF,QACC,IAAA,CAAE,UAAU,0BACV,SAAAP,EACG,4DACA,gDAAA,CAEN,CAAA,EACF,EAEAM,EAAAA,KAAC,IAAA,CAAE,UAAU,iBACV,SAAA,CAAAT,EAAe,OAAO,SAAOA,EAAe,SAAW,EAAI,IAAM,GAAG,WAAA,CAAA,CACvE,CAAA,EACF,CAEJ,ECrFMW,EAAa,CAAC,CAAE,KAAAC,EAAM,MAAAC,EAAO,aAAAC,EAAc,aAAAC,KAAmB,CAClE,KAAM,CAACC,EAAaC,CAAc,EAAIC,EAAAA,SAAS,CAAA,CAAE,EAE3CC,EAAkB,CAACC,EAAKC,EAAKC,IAAU,CAC3C,MAAMC,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACnBG,EAAa,CAAE,GAAGR,EAAa,CAACO,CAAG,EAAGD,EAAM,aAAY,EAC9DL,EAAeO,CAAU,EACzBT,GAAgBA,EAAaK,EAAKC,EAAKC,CAAK,CAC9C,EAEMG,EAAe,CAACL,EAAKC,IAAQ,CACjC,GAAIP,GAAgB,CAACY,EAAYN,EAAKC,CAAG,EACvC,OAAOT,EAAKQ,CAAG,EAAEC,CAAG,EAEtB,MAAME,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACzB,OAAOL,EAAYO,CAAG,GAAK,EAC7B,EAEMG,EAAc,CAACN,EAAKC,IACjBT,EAAKQ,CAAG,EAAEC,CAAG,IAAM,IAGtBM,EAAgB,CAACP,EAAKC,IAAQ,CAClC,GAAI,CAACR,EAAO,OAAO,KACnB,MAAMe,EAAOf,EAAM,KAAKzB,GAAKA,EAAE,SAAS,MAAQgC,GAAOhC,EAAE,SAAS,MAAQiC,CAAG,EAC7E,OAAOO,EAAOA,EAAK,OAAS,IAC9B,EAEA,GAAI,CAAChB,GAAQA,EAAK,SAAW,EAC3B,OAAOF,EAAAA,IAAC,MAAA,CAAI,UAAU,cAAc,SAAA,mBAAgB,EAGtD,MAAMmB,EAAWjB,EAAK,OAChBkB,EAAWlB,EAAK,CAAC,EAAIA,EAAK,CAAC,EAAE,OAAS,EAE5C,OACEF,EAAAA,IAAC,MAAA,CAAI,UAAU,mBACb,SAAAA,EAAAA,IAAC,MAAA,CACC,UAAU,cACV,MAAO,CACL,oBAAqB,UAAUoB,CAAQ,UACvC,iBAAkB,UAAUD,CAAQ,SAAA,EAGrC,SAAAjB,EAAK,IAAI,CAACQ,EAAKW,IACdX,EAAI,IAAI,CAACY,EAAMC,IAAa,CAC1B,MAAMC,EAAaP,EAAcI,EAAUE,CAAQ,EAInD,OAHgBP,EAAYK,EAAUE,CAAQ,EAK1CvB,EAAAA,IAAC,MAAA,CAEC,UAAU,uBACV,MAAO,CAAE,WAAY,QAAA,CAAS,EAFzB,GAAGqB,CAAQ,IAAIE,CAAQ,EAAA,EAShCxB,EAAAA,KAAC,MAAA,CAEC,UAAU,uBAET,SAAA,CAAAyB,GAAcxB,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAwB,EAAW,EACzDxB,EAAAA,IAAC,QAAA,CACC,KAAK,OACL,UAAU,IACV,MAAOe,EAAaM,EAAUE,CAAQ,EACtC,SAAWzC,GAAM2B,EAAgBY,EAAUE,EAAUzC,EAAE,OAAO,KAAK,EACnE,UAAW,cAAcsB,EAAe,gBAAkB,EAAE,GAC5D,SAAUA,CAAA,CAAA,CACZ,CAAA,EAXK,GAAGiB,CAAQ,IAAIE,CAAQ,EAAA,CAclC,CAAC,CAAA,CACH,CAAA,EAEJ,CAEJ,EClFME,EAAW,CAAC,CAAE,MAAAtB,EAAQ,CAAA,KAAS,CACnC,MAAMuB,EAAcvB,EAAM,OAAOe,GAAQA,EAAK,YAAc,QAAQ,EAC9DS,EAAYxB,EAAM,OAAOe,GAAQA,EAAK,YAAc,MAAM,EAE1DU,EAAc,CAAC,CAAE,MAAAC,EAAO,SAAAC,KAC5B/B,OAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAC,EAAAA,IAAC,MAAI,SAAA6B,CAAA,CAAM,EACX7B,EAAAA,IAAC,MACE,SAAA8B,EAAS,OACR/B,EAAAA,KAAC,KAAA,CAA4C,UAAU,YACrD,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAkB,EAAK,OAAO,EAC3ClB,EAAAA,IAAC,OAAA,CAAK,UAAU,YAAa,WAAK,IAAA,CAAK,CAAA,GAFhC,GAAGkB,EAAK,MAAM,IAAIA,EAAK,SAAS,EAGzC,CACD,CAAA,CACH,CAAA,EACF,EAGF,OACEnB,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAC,EAAAA,IAAC4B,EAAA,CAAY,MAAM,SAAS,SAAUF,EAAa,EACnD1B,EAAAA,IAAC4B,EAAA,CAAY,MAAM,OAAO,SAAUD,CAAA,CAAW,CAAA,EACjD,CAEJ,ECxBMI,EAAiB,CAAC,CAAE,QAAAC,EAAU,0BAEhCjC,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAC,EAAAA,IAAC,MAAA,CAAI,UAAU,SAAA,CAAU,EACzBA,EAAAA,IAAC,IAAA,CAAE,UAAU,kBAAmB,SAAAgC,CAAA,CAAQ,CAAA,EAC1C,ECLEC,EAAe,IAAM,CACzB,KAAM,CAACC,EAAQC,CAAS,EAAI3B,EAAAA,SAAS,IAAI,EACnC,CAAC4B,EAASC,CAAU,EAAI7B,EAAAA,SAAS,EAAK,EACtC,CAAC8B,EAAOC,CAAQ,EAAI/B,EAAAA,SAAS,IAAI,EACjC,CAACgC,EAAQC,CAAS,EAAIjC,EAAAA,SAAS,CAAA,CAAE,EAEjCkC,EAA4E,GAE5EC,EAAcC,EAAAA,YAAY,SAAY,CAC1C,GAAI,CACFP,EAAW,EAAI,EACf,MAAMQ,EAAW,MAAM,MAAM,GAAGH,CAAY,aAAa,EACzD,GAAI,CAACG,EAAS,GAAI,MAAM,IAAI,MAAM,wBAAwB,EAC1D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5BJ,EAAUK,CAAI,CAChB,OAASC,EAAK,CACZR,EAASQ,EAAI,OAAO,CACtB,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXM,EAAiBJ,EAAAA,YAAY,MAAOtD,EAAgB2D,EAAa,SAAUC,EAAQ,GAAO3D,EAAiB,GAAIE,EAAa,KAAS,CACzI,GAAI,CACF4C,EAAW,EAAI,EACfE,EAAS,IAAI,EAEb,MAAMM,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAQpD,EACR,WAAA2D,EACA,MAAAC,EACA,GAAI3D,GAAkB,CAAE,eAAAA,CAAA,EACxB,WAAAE,CAAA,CACD,CAAA,CACF,EAED,GAAI,CAACoD,EAAS,GAAI,CAChB,MAAMM,EAAY,MAAMN,EAAS,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EACxD,MAAM,IAAI,MAAMM,EAAU,SAAW,2BAA2B,CAClE,CAEA,MAAMC,EAAa,MAAMP,EAAS,KAAA,EAClC,OAAAV,EAAUiB,CAAU,EACbA,CACT,OAASL,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXW,EAAkBT,cAAY,MAAOtC,GAAgB,CACzD,GAAI,CACF,MAAMuC,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAAR,EACA,QAAS5B,CAAA,CACV,CAAA,CACF,EAED,GAAI,CAACuC,EAAS,GAAI,MAAM,IAAI,MAAM,4BAA4B,EAE9D,OAAO,MAAMA,EAAS,KAAA,CACxB,OAASE,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,CACF,EAAG,CAACL,EAAcR,CAAM,CAAC,EAEnBoB,EAAcV,EAAAA,YAAY,IAAM,CACpCT,EAAU,IAAI,EACdI,EAAS,IAAI,CACf,EAAG,CAAA,CAAE,EAEL,MAAO,CACL,OAAAL,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,gBAAAK,EACA,YAAAC,CAAA,CAEJ,ECxFA,SAASC,GAAM,CACb,KAAM,CAACjE,EAAgBkE,CAAiB,EAAIhD,EAAAA,SAAS,CAAA,CAAE,EACjD,CAACyC,EAAYQ,CAAa,EAAIjD,EAAAA,SAAS,QAAQ,EAC/C,CAACJ,EAAcsD,CAAe,EAAIlD,EAAAA,SAAS,EAAK,EAChD,CAACjB,EAAgBoE,CAAiB,EAAInD,EAAAA,SAAS,EAAE,EACjD,CAACf,EAAYmE,CAAa,EAAIpD,EAAAA,SAAS,EAAI,EAE3C,CACJ,OAAA0B,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,YAAAM,CAAA,EACErB,EAAA,EAEJ4B,EAAAA,UAAU,IAAM,CACdlB,EAAA,CACF,EAAG,CAACA,CAAW,CAAC,EAEhB,MAAMmB,EAAuB,SAAY,CACvC,GAAIxE,EAAe,SAAW,EAAG,CAC/B,MAAM,kCAAkC,EACxC,MACF,CAEA,MAAM0D,EAAe1D,EAAgB2D,EAAY,GAAO1D,EAAgBE,CAAU,CACpF,EAEMsE,EAAsBvB,GAAW,CACrCgB,EAAkBhB,CAAM,CAC1B,EAEMwB,EAAwBC,GAAa,CACzCN,EAAkBM,CAAQ,CAC5B,EAEMC,EAA0BC,GAAY,CAC1CP,EAAcO,CAAO,CACvB,EAGMC,EAAc,IAAM,CACxBd,EAAA,EACAE,EAAkB,CAAA,CAAE,EACpBE,EAAgB,EAAK,EACrBD,EAAc,QAAQ,EACtBE,EAAkB,EAAE,EACpBC,EAAc,EAAI,CACpB,EAEMS,EAAuB,IAAM,CACjCX,EAAgB,EAAI,CACtB,EAEA,OACE3D,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CAAO,UAAU,aAChB,SAAA,CAAAC,EAAAA,IAAC,KAAA,CAAG,UAAU,YAAY,SAAA,6BAA0B,EACpDA,EAAAA,IAAC,KAAE,SAAA,0DAAA,CAAwD,CAAA,EAC7D,EAEAA,EAAAA,IAACb,EAAA,CACC,eAAgB4E,EAChB,gBAAiBvB,EACjB,eAAAlD,EACA,eAAAC,EACA,iBAAkByE,EAClB,WAAAvE,EACA,mBAAoByE,CAAA,CAAA,EAGtBnE,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CACC,MAAOkD,EACP,SAAWnE,GAAM2E,EAAc3E,EAAE,OAAO,KAAK,EAC7C,UAAU,cAEV,SAAA,CAAAkB,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,OAAI,EACzBA,EAAAA,IAAC,SAAA,CAAO,MAAM,SAAS,SAAA,SAAM,EAC7BA,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,MAAA,CAAI,CAAA,CAAA,CAAA,EAG3BA,EAAAA,IAAC,SAAA,CACC,QAAS8D,EACT,SAAU1B,GAAW9C,EAAe,SAAW,EAC/C,UAAU,2BAET,WAAU,gBAAkB,iBAAA,CAAA,EAG/BU,EAAAA,IAAC,SAAA,CACC,QAASoE,EACT,UAAU,wBACX,SAAA,OAAA,CAAA,EAIAlC,GAAU,CAAC9B,GACVJ,EAAAA,IAAC,SAAA,CACC,QAASqE,EACT,UAAU,yBACX,SAAA,iBAAA,CAAA,CAED,EAEJ,EAEC/B,GACCvC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBAAgB,SAAA,CAAA,UACrBuC,CAAA,EACV,EAGDF,SAAYL,EAAA,EAAe,EAE3BG,GAAU,CAACE,GACVrC,EAAAA,KAAAuE,EAAAA,SAAA,CACE,SAAA,CAAAtE,EAAAA,IAAC,OAAI,UAAU,cACb,SAAAD,EAAAA,KAAC,OAAA,CAAK,UAAU,eACb,SAAA,CAAAmC,EAAO,SAAS,UAAU,YAAUA,EAAO,SAAS,KAAK,IAAEA,EAAO,SAAS,KAAK,OAAA,CAAA,CACnF,CAAA,CACF,EACAnC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAMiC,EAAO,KACb,MAAOA,EAAO,MACd,aAAA9B,CAAA,CAAA,EAEFJ,EAAAA,IAACyB,EAAA,CAAS,MAAOS,EAAO,KAAA,CAAO,CAAA,CAAA,CACjC,CAAA,EACF,EAGD,CAACA,GAAU,CAACE,GAAW,CAACE,GACvBtC,EAAAA,IAAC,MAAA,CAAI,MAAO,CAAE,UAAW,SAAU,QAAS,OAAQ,MAAO,SAAA,EAAa,SAAA,qDAAA,CAExE,CAAA,EAEJ,CAEJ,CClJAuE,EAAS,WAAW,SAAS,eAAe,MAAM,CAAC,EAAE,aAClDC,EAAM,WAAN,CACC,SAAAxE,MAACuD,IAAI,CAAA,CACP,CACF","x_google_ignoreList":[0,1,2]}
         | 
    	
        crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js
    ADDED
    
    | The diff for this file is too large to render. 
		See raw diff | 
|  | 
    	
        crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js.map
    ADDED
    
    | The diff for this file is too large to render. 
		See raw diff | 
|  | 
    	
        crossword-app/backend-py/public/index.html
    ADDED
    
    | @@ -0,0 +1,16 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            <!DOCTYPE html>
         | 
| 2 | 
            +
            <html lang="en">
         | 
| 3 | 
            +
              <head>
         | 
| 4 | 
            +
                <meta charset="UTF-8" />
         | 
| 5 | 
            +
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
         | 
| 6 | 
            +
                <meta name="description" content="Generate custom crossword puzzles by selecting topics" />
         | 
| 7 | 
            +
                <meta name="keywords" content="crossword, puzzle, word game, brain teaser" />
         | 
| 8 | 
            +
                <title>Crossword Puzzle Generator</title>
         | 
| 9 | 
            +
                <script type="module" crossorigin src="/assets/index-uK3VdD5a.js"></script>
         | 
| 10 | 
            +
                <link rel="modulepreload" crossorigin href="/assets/vendor-nf7bT_Uh.js">
         | 
| 11 | 
            +
                <link rel="stylesheet" crossorigin href="/assets/index-DyT-gQda.css">
         | 
| 12 | 
            +
              </head>
         | 
| 13 | 
            +
              <body>
         | 
| 14 | 
            +
                <div id="root"></div>
         | 
| 15 | 
            +
              </body>
         | 
| 16 | 
            +
            </html>
         | 
    	
        crossword-app/backend-py/requirements.txt
    CHANGED
    
    | @@ -24,7 +24,8 @@ idna==3.10 | |
| 24 | 
             
            numpy==2.3.2
         | 
| 25 |  | 
| 26 | 
             
            # Logging and monitoring
         | 
| 27 | 
            -
             | 
|  | |
| 28 |  | 
| 29 | 
             
            # Development and testing dependencies
         | 
| 30 | 
             
            pytest==8.4.1
         | 
| @@ -34,15 +35,17 @@ packaging==25.0 | |
| 34 | 
             
            pluggy==1.6.0
         | 
| 35 | 
             
            pygments==2.19.2
         | 
| 36 |  | 
| 37 | 
            -
            # AI/ML dependencies  | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
|  | |
|  | |
| 45 |  | 
| 46 | 
             
            # Additional utility dependencies
         | 
| 47 | 
             
            annotated-types==0.7.0
         | 
| 48 | 
            -
            sniffio==1.3.1
         | 
|  | |
| 24 | 
             
            numpy==2.3.2
         | 
| 25 |  | 
| 26 | 
             
            # Logging and monitoring
         | 
| 27 | 
            +
            # (Using standard Python logging with enhanced format)
         | 
| 28 | 
            +
             | 
| 29 |  | 
| 30 | 
             
            # Development and testing dependencies
         | 
| 31 | 
             
            pytest==8.4.1
         | 
|  | |
| 35 | 
             
            pluggy==1.6.0
         | 
| 36 | 
             
            pygments==2.19.2
         | 
| 37 |  | 
| 38 | 
            +
            # AI/ML dependencies for thematic word generation
         | 
| 39 | 
            +
            sentence-transformers==3.3.0
         | 
| 40 | 
            +
            torch==2.5.1
         | 
| 41 | 
            +
            transformers==4.47.1
         | 
| 42 | 
            +
            scikit-learn==1.5.2
         | 
| 43 | 
            +
            huggingface-hub==0.26.2
         | 
| 44 | 
            +
            wordfreq==3.1.0
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            # NLTK dependencies for WordNet clue generation
         | 
| 47 | 
            +
            nltk==3.8.1
         | 
| 48 |  | 
| 49 | 
             
            # Additional utility dependencies
         | 
| 50 | 
             
            annotated-types==0.7.0
         | 
| 51 | 
            +
            sniffio==1.3.1
         | 
    	
        crossword-app/backend-py/src/routes/api.py
    CHANGED
    
    | @@ -20,7 +20,9 @@ router = APIRouter() | |
| 20 | 
             
            class GeneratePuzzleRequest(BaseModel):
         | 
| 21 | 
             
                topics: List[str] = Field(..., description="List of topics for the puzzle")
         | 
| 22 | 
             
                difficulty: str = Field(default="medium", description="Difficulty level: easy, medium, hard")
         | 
| 23 | 
            -
                 | 
|  | |
|  | |
| 24 |  | 
| 25 | 
             
            class WordInfo(BaseModel):
         | 
| 26 | 
             
                word: str
         | 
| @@ -55,22 +57,30 @@ class TopicInfo(BaseModel): | |
| 55 | 
             
            generator = None
         | 
| 56 |  | 
| 57 | 
             
            def get_crossword_generator(request: Request) -> CrosswordGenerator:
         | 
| 58 | 
            -
                """Dependency to get the crossword generator with  | 
| 59 | 
             
                global generator
         | 
| 60 | 
             
                if generator is None:
         | 
| 61 | 
            -
                     | 
| 62 | 
            -
                    generator = CrosswordGenerator( | 
| 63 | 
             
                return generator
         | 
| 64 |  | 
| 65 | 
             
            @router.get("/topics", response_model=List[TopicInfo])
         | 
| 66 | 
             
            async def get_topics():
         | 
| 67 | 
             
                """Get available topics for puzzle generation."""
         | 
| 68 | 
            -
                # Return  | 
| 69 | 
             
                topics = [
         | 
| 70 | 
             
                    {"id": "animals", "name": "Animals"},
         | 
| 71 | 
             
                    {"id": "geography", "name": "Geography"},
         | 
| 72 | 
             
                    {"id": "science", "name": "Science"},
         | 
| 73 | 
            -
                    {"id": "technology", "name": "Technology"}
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 74 | 
             
                ]
         | 
| 75 | 
             
                return topics
         | 
| 76 |  | 
| @@ -80,16 +90,18 @@ async def generate_puzzle( | |
| 80 | 
             
                crossword_gen: CrosswordGenerator = Depends(get_crossword_generator)
         | 
| 81 | 
             
            ):
         | 
| 82 | 
             
                """
         | 
| 83 | 
            -
                Generate a crossword puzzle with  | 
| 84 |  | 
| 85 | 
             
                This endpoint matches the JavaScript API exactly for frontend compatibility.
         | 
| 86 | 
             
                """
         | 
| 87 | 
             
                try:
         | 
| 88 | 
            -
                     | 
|  | |
|  | |
| 89 |  | 
| 90 | 
            -
                    # Validate topics
         | 
| 91 | 
            -
                    if not request.topics:
         | 
| 92 | 
            -
                        raise HTTPException(status_code=400, detail="At least one topic is required")
         | 
| 93 |  | 
| 94 | 
             
                    valid_difficulties = ["easy", "medium", "hard"]
         | 
| 95 | 
             
                    if request.difficulty not in valid_difficulties:
         | 
| @@ -98,11 +110,20 @@ async def generate_puzzle( | |
| 98 | 
             
                            detail=f"Invalid difficulty. Must be one of: {valid_difficulties}"
         | 
| 99 | 
             
                        )
         | 
| 100 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 101 | 
             
                    # Generate puzzle
         | 
| 102 | 
             
                    puzzle_data = await crossword_gen.generate_puzzle(
         | 
| 103 | 
             
                        topics=request.topics,
         | 
| 104 | 
             
                        difficulty=request.difficulty,
         | 
| 105 | 
            -
                         | 
|  | |
|  | |
| 106 | 
             
                    )
         | 
| 107 |  | 
| 108 | 
             
                    if not puzzle_data:
         | 
| @@ -130,14 +151,12 @@ async def generate_words( | |
| 130 | 
             
                try:
         | 
| 131 | 
             
                    words = await crossword_gen.generate_words_for_topics(
         | 
| 132 | 
             
                        topics=request.topics,
         | 
| 133 | 
            -
                        difficulty=request.difficulty | 
| 134 | 
            -
                        use_ai=request.useAI
         | 
| 135 | 
             
                    )
         | 
| 136 |  | 
| 137 | 
             
                    return {
         | 
| 138 | 
             
                        "topics": request.topics,
         | 
| 139 | 
             
                        "difficulty": request.difficulty,
         | 
| 140 | 
            -
                        "useAI": request.useAI,
         | 
| 141 | 
             
                        "wordCount": len(words),
         | 
| 142 | 
             
                        "words": words
         | 
| 143 | 
             
                    }
         | 
| @@ -147,31 +166,129 @@ async def generate_words( | |
| 147 | 
             
                    raise HTTPException(status_code=500, detail=str(e))
         | 
| 148 |  | 
| 149 | 
             
            @router.get("/health")
         | 
| 150 | 
            -
            async def api_health():
         | 
| 151 | 
            -
                """API health check."""
         | 
| 152 | 
            -
                 | 
|  | |
|  | |
| 153 | 
             
                    "status": "healthy",
         | 
| 154 | 
             
                    "timestamp": datetime.utcnow().isoformat(),
         | 
| 155 | 
             
                    "backend": "python",
         | 
| 156 | 
            -
                    "version": "2.0.0"
         | 
|  | |
|  | |
|  | |
|  | |
| 157 | 
             
                }
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 158 |  | 
| 159 | 
            -
            @router.get("/debug/ | 
| 160 | 
            -
            async def  | 
| 161 | 
             
                topic: str,
         | 
| 162 | 
             
                difficulty: str = "medium",
         | 
| 163 | 
             
                max_words: int = 10,
         | 
| 164 | 
             
                request: Request = None
         | 
| 165 | 
             
            ):
         | 
| 166 | 
             
                """
         | 
| 167 | 
            -
                Debug endpoint to test  | 
| 168 | 
             
                """
         | 
| 169 | 
             
                try:
         | 
| 170 | 
            -
                     | 
| 171 | 
            -
                    if not  | 
| 172 | 
            -
                        raise HTTPException(status_code=503, detail=" | 
| 173 |  | 
| 174 | 
            -
                    words = await  | 
| 175 |  | 
| 176 | 
             
                    return {
         | 
| 177 | 
             
                        "topic": topic,
         | 
| @@ -182,5 +299,5 @@ async def debug_vector_search( | |
| 182 | 
             
                    }
         | 
| 183 |  | 
| 184 | 
             
                except Exception as e:
         | 
| 185 | 
            -
                    logger.error(f"❌  | 
| 186 | 
             
                    raise HTTPException(status_code=500, detail=str(e))
         | 
|  | |
| 20 | 
             
            class GeneratePuzzleRequest(BaseModel):
         | 
| 21 | 
             
                topics: List[str] = Field(..., description="List of topics for the puzzle")
         | 
| 22 | 
             
                difficulty: str = Field(default="medium", description="Difficulty level: easy, medium, hard")
         | 
| 23 | 
            +
                customSentence: Optional[str] = Field(default=None, description="Optional custom sentence to influence word selection")
         | 
| 24 | 
            +
                multiTheme: bool = Field(default=True, description="Whether to use multi-theme processing or single-theme blending")
         | 
| 25 | 
            +
                wordCount: Optional[int] = Field(default=10, description="Number of words to include in the crossword (8-15)")
         | 
| 26 |  | 
| 27 | 
             
            class WordInfo(BaseModel):
         | 
| 28 | 
             
                word: str
         | 
|  | |
| 57 | 
             
            generator = None
         | 
| 58 |  | 
| 59 | 
             
            def get_crossword_generator(request: Request) -> CrosswordGenerator:
         | 
| 60 | 
            +
                """Dependency to get the crossword generator with thematic service."""
         | 
| 61 | 
             
                global generator
         | 
| 62 | 
             
                if generator is None:
         | 
| 63 | 
            +
                    thematic_service = getattr(request.app.state, 'thematic_service', None)
         | 
| 64 | 
            +
                    generator = CrosswordGenerator(thematic_service)
         | 
| 65 | 
             
                return generator
         | 
| 66 |  | 
| 67 | 
             
            @router.get("/topics", response_model=List[TopicInfo])
         | 
| 68 | 
             
            async def get_topics():
         | 
| 69 | 
             
                """Get available topics for puzzle generation."""
         | 
| 70 | 
            +
                # Return expanded topic list for better user variety
         | 
| 71 | 
             
                topics = [
         | 
| 72 | 
             
                    {"id": "animals", "name": "Animals"},
         | 
| 73 | 
             
                    {"id": "geography", "name": "Geography"},
         | 
| 74 | 
             
                    {"id": "science", "name": "Science"},
         | 
| 75 | 
            +
                    {"id": "technology", "name": "Technology"},
         | 
| 76 | 
            +
                    {"id": "sports", "name": "Sports"},
         | 
| 77 | 
            +
                    {"id": "history", "name": "History"},
         | 
| 78 | 
            +
                    {"id": "food", "name": "Food"},
         | 
| 79 | 
            +
                    {"id": "entertainment", "name": "Entertainment"},
         | 
| 80 | 
            +
                    {"id": "nature", "name": "Nature"},
         | 
| 81 | 
            +
                    {"id": "transportation", "name": "Transportation"},
         | 
| 82 | 
            +
                    {"id": "art", "name": "Art"},
         | 
| 83 | 
            +
                    {"id": "medicine", "name": "Medicine"}
         | 
| 84 | 
             
                ]
         | 
| 85 | 
             
                return topics
         | 
| 86 |  | 
|  | |
| 90 | 
             
                crossword_gen: CrosswordGenerator = Depends(get_crossword_generator)
         | 
| 91 | 
             
            ):
         | 
| 92 | 
             
                """
         | 
| 93 | 
            +
                Generate a crossword puzzle with AI thematic word generation.
         | 
| 94 |  | 
| 95 | 
             
                This endpoint matches the JavaScript API exactly for frontend compatibility.
         | 
| 96 | 
             
                """
         | 
| 97 | 
             
                try:
         | 
| 98 | 
            +
                    sentence_info = f", custom sentence: '{request.customSentence}'" if request.customSentence else ""
         | 
| 99 | 
            +
                    theme_mode = "multi-theme" if request.multiTheme else "single-theme"
         | 
| 100 | 
            +
                    logger.info(f"🎯 Generating puzzle for topics: {request.topics}, difficulty: {request.difficulty}{sentence_info}, mode: {theme_mode}")
         | 
| 101 |  | 
| 102 | 
            +
                    # Validate topics - require either topics or custom sentence
         | 
| 103 | 
            +
                    if not request.topics and not (request.customSentence and request.customSentence.strip()):
         | 
| 104 | 
            +
                        raise HTTPException(status_code=400, detail="At least one topic or a custom sentence is required")
         | 
| 105 |  | 
| 106 | 
             
                    valid_difficulties = ["easy", "medium", "hard"]
         | 
| 107 | 
             
                    if request.difficulty not in valid_difficulties:
         | 
|  | |
| 110 | 
             
                            detail=f"Invalid difficulty. Must be one of: {valid_difficulties}"
         | 
| 111 | 
             
                        )
         | 
| 112 |  | 
| 113 | 
            +
                    # Validate word count
         | 
| 114 | 
            +
                    if request.wordCount and (request.wordCount < 8 or request.wordCount > 15):
         | 
| 115 | 
            +
                        raise HTTPException(
         | 
| 116 | 
            +
                            status_code=400,
         | 
| 117 | 
            +
                            detail="Word count must be between 8 and 15"
         | 
| 118 | 
            +
                        )
         | 
| 119 | 
            +
                    
         | 
| 120 | 
             
                    # Generate puzzle
         | 
| 121 | 
             
                    puzzle_data = await crossword_gen.generate_puzzle(
         | 
| 122 | 
             
                        topics=request.topics,
         | 
| 123 | 
             
                        difficulty=request.difficulty,
         | 
| 124 | 
            +
                        custom_sentence=request.customSentence,
         | 
| 125 | 
            +
                        multi_theme=request.multiTheme,
         | 
| 126 | 
            +
                        requested_words=request.wordCount
         | 
| 127 | 
             
                    )
         | 
| 128 |  | 
| 129 | 
             
                    if not puzzle_data:
         | 
|  | |
| 151 | 
             
                try:
         | 
| 152 | 
             
                    words = await crossword_gen.generate_words_for_topics(
         | 
| 153 | 
             
                        topics=request.topics,
         | 
| 154 | 
            +
                        difficulty=request.difficulty
         | 
|  | |
| 155 | 
             
                    )
         | 
| 156 |  | 
| 157 | 
             
                    return {
         | 
| 158 | 
             
                        "topics": request.topics,
         | 
| 159 | 
             
                        "difficulty": request.difficulty,
         | 
|  | |
| 160 | 
             
                        "wordCount": len(words),
         | 
| 161 | 
             
                        "words": words
         | 
| 162 | 
             
                    }
         | 
|  | |
| 166 | 
             
                    raise HTTPException(status_code=500, detail=str(e))
         | 
| 167 |  | 
| 168 | 
             
            @router.get("/health")
         | 
| 169 | 
            +
            async def api_health(request: Request):
         | 
| 170 | 
            +
                """API health check with cache status."""
         | 
| 171 | 
            +
                thematic_service = getattr(request.app.state, 'thematic_service', None)
         | 
| 172 | 
            +
                
         | 
| 173 | 
            +
                health_info = {
         | 
| 174 | 
             
                    "status": "healthy",
         | 
| 175 | 
             
                    "timestamp": datetime.utcnow().isoformat(),
         | 
| 176 | 
             
                    "backend": "python",
         | 
| 177 | 
            +
                    "version": "2.0.0",
         | 
| 178 | 
            +
                    "thematic_service": {
         | 
| 179 | 
            +
                        "available": thematic_service is not None,
         | 
| 180 | 
            +
                        "initialized": thematic_service.is_initialized if thematic_service else False
         | 
| 181 | 
            +
                    }
         | 
| 182 | 
             
                }
         | 
| 183 | 
            +
                
         | 
| 184 | 
            +
                # Add cache status if service is available
         | 
| 185 | 
            +
                if thematic_service:
         | 
| 186 | 
            +
                    try:
         | 
| 187 | 
            +
                        cache_status = thematic_service.get_cache_status()
         | 
| 188 | 
            +
                        health_info["cache"] = cache_status
         | 
| 189 | 
            +
                    except Exception as e:
         | 
| 190 | 
            +
                        health_info["cache"] = {"error": str(e)}
         | 
| 191 | 
            +
                
         | 
| 192 | 
            +
                return health_info
         | 
| 193 | 
            +
             | 
| 194 | 
            +
            @router.get("/health/cache")
         | 
| 195 | 
            +
            async def cache_health(request: Request):
         | 
| 196 | 
            +
                """Detailed cache health check and status."""
         | 
| 197 | 
            +
                thematic_service = getattr(request.app.state, 'thematic_service', None)
         | 
| 198 | 
            +
                
         | 
| 199 | 
            +
                if not thematic_service:
         | 
| 200 | 
            +
                    return {"error": "Thematic service not available"}
         | 
| 201 | 
            +
                
         | 
| 202 | 
            +
                try:
         | 
| 203 | 
            +
                    cache_status = thematic_service.get_cache_status()
         | 
| 204 | 
            +
                    
         | 
| 205 | 
            +
                    # Add additional diagnostic information
         | 
| 206 | 
            +
                    import os
         | 
| 207 | 
            +
                    cache_dir = cache_status['cache_directory']
         | 
| 208 | 
            +
                    
         | 
| 209 | 
            +
                    diagnostics = {
         | 
| 210 | 
            +
                        "cache_status": cache_status,
         | 
| 211 | 
            +
                        "diagnostics": {
         | 
| 212 | 
            +
                            "cache_dir_exists": os.path.exists(cache_dir),
         | 
| 213 | 
            +
                            "cache_dir_readable": os.access(cache_dir, os.R_OK) if os.path.exists(cache_dir) else False,
         | 
| 214 | 
            +
                            "cache_dir_writable": os.access(cache_dir, os.W_OK) if os.path.exists(cache_dir) else False,
         | 
| 215 | 
            +
                            "service_initialized": thematic_service.is_initialized,
         | 
| 216 | 
            +
                            "vocab_size_limit": thematic_service.vocab_size_limit,
         | 
| 217 | 
            +
                            "model_name": thematic_service.model_name
         | 
| 218 | 
            +
                        }
         | 
| 219 | 
            +
                    }
         | 
| 220 | 
            +
                    
         | 
| 221 | 
            +
                    # Add file listing if directory exists
         | 
| 222 | 
            +
                    if os.path.exists(cache_dir):
         | 
| 223 | 
            +
                        try:
         | 
| 224 | 
            +
                            cache_files = []
         | 
| 225 | 
            +
                            for file in os.listdir(cache_dir):
         | 
| 226 | 
            +
                                file_path = os.path.join(cache_dir, file)
         | 
| 227 | 
            +
                                if os.path.isfile(file_path):
         | 
| 228 | 
            +
                                    stat = os.stat(file_path)
         | 
| 229 | 
            +
                                    cache_files.append({
         | 
| 230 | 
            +
                                        "name": file,
         | 
| 231 | 
            +
                                        "size_bytes": stat.st_size,
         | 
| 232 | 
            +
                                        "size_mb": round(stat.st_size / (1024 * 1024), 2),
         | 
| 233 | 
            +
                                        "modified": datetime.fromtimestamp(stat.st_mtime).isoformat()
         | 
| 234 | 
            +
                                    })
         | 
| 235 | 
            +
                            diagnostics["cache_files"] = cache_files
         | 
| 236 | 
            +
                        except Exception as e:
         | 
| 237 | 
            +
                            diagnostics["cache_files_error"] = str(e)
         | 
| 238 | 
            +
                    
         | 
| 239 | 
            +
                    return diagnostics
         | 
| 240 | 
            +
                    
         | 
| 241 | 
            +
                except Exception as e:
         | 
| 242 | 
            +
                    return {"error": f"Failed to get cache status: {e}"}
         | 
| 243 | 
            +
             | 
| 244 | 
            +
            @router.post("/health/cache/reinitialize")
         | 
| 245 | 
            +
            async def reinitialize_cache(request: Request):
         | 
| 246 | 
            +
                """Force re-initialization of the thematic service and cache creation."""
         | 
| 247 | 
            +
                thematic_service = getattr(request.app.state, 'thematic_service', None)
         | 
| 248 | 
            +
                
         | 
| 249 | 
            +
                if not thematic_service:
         | 
| 250 | 
            +
                    return {"error": "Thematic service not available"}
         | 
| 251 | 
            +
                
         | 
| 252 | 
            +
                try:
         | 
| 253 | 
            +
                    # Reset initialization flag to force re-initialization
         | 
| 254 | 
            +
                    thematic_service.is_initialized = False
         | 
| 255 | 
            +
                    
         | 
| 256 | 
            +
                    # Force re-initialization
         | 
| 257 | 
            +
                    await thematic_service.initialize_async()
         | 
| 258 | 
            +
                    
         | 
| 259 | 
            +
                    # Get updated cache status
         | 
| 260 | 
            +
                    cache_status = thematic_service.get_cache_status()
         | 
| 261 | 
            +
                    
         | 
| 262 | 
            +
                    return {
         | 
| 263 | 
            +
                        "message": "Cache re-initialization completed",
         | 
| 264 | 
            +
                        "cache_status": cache_status,
         | 
| 265 | 
            +
                        "timestamp": datetime.utcnow().isoformat()
         | 
| 266 | 
            +
                    }
         | 
| 267 | 
            +
                    
         | 
| 268 | 
            +
                except Exception as e:
         | 
| 269 | 
            +
                    import traceback
         | 
| 270 | 
            +
                    return {
         | 
| 271 | 
            +
                        "error": f"Failed to reinitialize cache: {e}",
         | 
| 272 | 
            +
                        "traceback": traceback.format_exc(),
         | 
| 273 | 
            +
                        "timestamp": datetime.utcnow().isoformat()
         | 
| 274 | 
            +
                    }
         | 
| 275 |  | 
| 276 | 
            +
            @router.get("/debug/thematic-search")
         | 
| 277 | 
            +
            async def debug_thematic_search(
         | 
| 278 | 
             
                topic: str,
         | 
| 279 | 
             
                difficulty: str = "medium",
         | 
| 280 | 
             
                max_words: int = 10,
         | 
| 281 | 
             
                request: Request = None
         | 
| 282 | 
             
            ):
         | 
| 283 | 
             
                """
         | 
| 284 | 
            +
                Debug endpoint to test thematic word generation directly.
         | 
| 285 | 
             
                """
         | 
| 286 | 
             
                try:
         | 
| 287 | 
            +
                    thematic_service = getattr(request.app.state, 'thematic_service', None)
         | 
| 288 | 
            +
                    if not thematic_service or not thematic_service.is_initialized:
         | 
| 289 | 
            +
                        raise HTTPException(status_code=503, detail="Thematic service not available")
         | 
| 290 |  | 
| 291 | 
            +
                    words = await thematic_service.find_words_for_crossword([topic], difficulty, max_words)
         | 
| 292 |  | 
| 293 | 
             
                    return {
         | 
| 294 | 
             
                        "topic": topic,
         | 
|  | |
| 299 | 
             
                    }
         | 
| 300 |  | 
| 301 | 
             
                except Exception as e:
         | 
| 302 | 
            +
                    logger.error(f"❌ Thematic search debug failed: {e}")
         | 
| 303 | 
             
                    raise HTTPException(status_code=500, detail=str(e))
         | 
    	
        crossword-app/backend-py/src/services///
    ADDED
    
    | @@ -0,0 +1,12 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            # cross-words
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            - [x] tell claude it is stupid
         | 
| 4 | 
            +
            - [x] remove use_ai flag. always use AI. that was the plan
         | 
| 5 | 
            +
            - [ ] in the initialize function of vector search service log all config parameters. some config items are missing
         | 
| 6 | 
            +
            - [x] add filename line number to logs
         | 
| 7 | 
            +
            - [ ] make difficulty to tier mapping separate, configurable
         | 
| 8 | 
            +
            - [x] remove use_ai from frontend
         | 
| 9 | 
            +
            - [x] add more topics to choose
         | 
| 10 | 
            +
            - [ ] how to dynamically generate topics
         | 
| 11 | 
            +
            - [x] enable the difficulty chooser in frontend
         | 
| 12 | 
            +
            - [ ] let backend return multiple set of words and frontend use it one by one till crossword generation is success
         | 
    	
        crossword-app/backend-py/src/services/__pycache__/__init__.cpython-310.pyc
    DELETED
    
    | Binary file (184 Bytes) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/__init__.cpython-313.pyc
    DELETED
    
    | Binary file (188 Bytes) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-310.pyc
    DELETED
    
    | Binary file (20 kB) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-313.pyc
    DELETED
    
    | Binary file (33.3 kB) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/crossword_generator_fixed.cpython-313.pyc
    DELETED
    
    | Binary file (33.4 kB) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/crossword_generator_wrapper.cpython-313.pyc
    DELETED
    
    | Binary file (2.91 kB) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/vector_search.cpython-313.pyc
    DELETED
    
    | Binary file (68.4 kB) | 
|  | 
    	
        crossword-app/backend-py/src/services/__pycache__/word_cache.cpython-313.pyc
    DELETED
    
    | Binary file (17.3 kB) | 
|  | 
    	
        crossword-app/backend-py/src/services/clue_generator.py
    ADDED
    
    | @@ -0,0 +1,35 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            """
         | 
| 4 | 
            +
            WordNet-Based Clue Generator for Crossword Puzzles
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Uses NLTK WordNet to generate crossword clues by analyzing word definitions,
         | 
| 7 | 
            +
            synonyms, hypernyms, and semantic relationships. Integrated with the thematic
         | 
| 8 | 
            +
            word generator for complete crossword creation without API dependencies.
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            Features:
         | 
| 11 | 
            +
            - WordNet-based clue generation using definitions and relationships
         | 
| 12 | 
            +
            - Integration with UnifiedThematicWordGenerator for word discovery
         | 
| 13 | 
            +
            - Interactive mode with topic-based generation
         | 
| 14 | 
            +
            - Multiple clue styles (definition, synonym, category, descriptive)
         | 
| 15 | 
            +
            - Difficulty-based clue complexity
         | 
| 16 | 
            +
            - Caching for improved performance
         | 
| 17 | 
            +
            """
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            import os
         | 
| 20 | 
            +
            import sys
         | 
| 21 | 
            +
            import re
         | 
| 22 | 
            +
            import time
         | 
| 23 | 
            +
            import logging
         | 
| 24 | 
            +
            from typing import List, Dict, Optional, Tuple, Set, Any
         | 
| 25 | 
            +
            from pathlib import Path
         | 
| 26 | 
            +
            from dataclasses import dataclass
         | 
| 27 | 
            +
            from collections import defaultdict
         | 
| 28 | 
            +
            import random
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            # Set up logging
         | 
| 31 | 
            +
            logging.basicConfig(
         | 
| 32 | 
            +
                level=logging.INFO,
         | 
| 33 | 
            +
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
         | 
| 34 | 
            +
            )
         | 
| 35 | 
            +
            logger = logging.getLogger(__name__)
         | 
    	
        crossword-app/backend-py/src/services/crossword_generator.py
    CHANGED
    
    | @@ -4,37 +4,30 @@ Fixed Crossword Generator - Ported from working JavaScript implementation. | |
| 4 |  | 
| 5 | 
             
            import asyncio
         | 
| 6 | 
             
            import json
         | 
|  | |
| 7 | 
             
            import random
         | 
| 8 | 
             
            import time
         | 
| 9 | 
             
            from pathlib import Path
         | 
| 10 | 
             
            from typing import Dict, List, Optional, Any, Tuple
         | 
| 11 | 
            -
            import structlog
         | 
| 12 |  | 
| 13 | 
            -
            logger =  | 
| 14 |  | 
| 15 | 
             
            class CrosswordGenerator:
         | 
| 16 | 
            -
                def __init__(self,  | 
| 17 | 
             
                    self.max_attempts = 100
         | 
| 18 | 
             
                    self.min_words = 6
         | 
| 19 | 
            -
                    self. | 
| 20 | 
            -
                    self.vector_service = vector_service
         | 
| 21 |  | 
| 22 | 
            -
                async def generate_puzzle(self, topics: List[str], difficulty: str = "medium",  | 
| 23 | 
             
                    """
         | 
| 24 | 
             
                    Generate a complete crossword puzzle.
         | 
| 25 | 
             
                    """
         | 
| 26 | 
             
                    try:
         | 
| 27 | 
            -
                         | 
| 28 | 
            -
                         | 
| 29 | 
            -
                            from .vector_search import VectorSearchService
         | 
| 30 | 
            -
                        except ImportError as import_error:
         | 
| 31 | 
            -
                            logger.warning(f"⚠️ Could not import VectorSearchService: {import_error}. Using static words only.")
         | 
| 32 | 
            -
                            # Continue without vector service
         | 
| 33 | 
            -
                        
         | 
| 34 | 
            -
                        logger.info(f"🎯 Generating puzzle for topics: {topics}, difficulty: {difficulty}, AI: {use_ai}")
         | 
| 35 |  | 
| 36 | 
            -
                        # Get words  | 
| 37 | 
            -
                        words = await self._select_words(topics, difficulty,  | 
| 38 |  | 
| 39 | 
             
                        if len(words) < self.min_words:
         | 
| 40 | 
             
                            logger.error(f"❌ Not enough words: {len(words)} < {self.min_words}")
         | 
| @@ -57,7 +50,7 @@ class CrosswordGenerator: | |
| 57 | 
             
                                "difficulty": difficulty,
         | 
| 58 | 
             
                                "wordCount": len(grid_result["placed_words"]),
         | 
| 59 | 
             
                                "size": len(grid_result["grid"]),
         | 
| 60 | 
            -
                                "aiGenerated":  | 
| 61 | 
             
                            }
         | 
| 62 | 
             
                        }
         | 
| 63 |  | 
| @@ -65,66 +58,22 @@ class CrosswordGenerator: | |
| 65 | 
             
                        logger.error(f"❌ Error generating puzzle: {e}")
         | 
| 66 | 
             
                        raise
         | 
| 67 |  | 
| 68 | 
            -
                async def _select_words(self, topics: List[str], difficulty: str,  | 
| 69 | 
            -
                    """Select words for the crossword."""
         | 
| 70 | 
            -
                     | 
| 71 | 
            -
             | 
| 72 | 
            -
                    if use_ai and self.vector_service:
         | 
| 73 | 
            -
                        # Use the initialized vector service
         | 
| 74 | 
            -
                        logger.info(f"🤖 Using initialized vector service for AI word generation")
         | 
| 75 | 
            -
                        for topic in topics:
         | 
| 76 | 
            -
                            ai_words = await self.vector_service.find_similar_words(topic, difficulty, self.max_words // len(topics))
         | 
| 77 | 
            -
                            all_words.extend(ai_words)
         | 
| 78 | 
            -
                        
         | 
| 79 | 
            -
                        if len(all_words) >= self.min_words:
         | 
| 80 | 
            -
                            logger.info(f"✅ AI generated {len(all_words)} words")
         | 
| 81 | 
            -
                            return self._sort_words_for_crossword(all_words[:self.max_words])
         | 
| 82 | 
            -
                        else:
         | 
| 83 | 
            -
                            logger.warning(f"⚠️ AI only generated {len(all_words)} words, falling back to static")
         | 
| 84 | 
            -
                    
         | 
| 85 | 
            -
                    # Fallback to cached words
         | 
| 86 | 
            -
                    if self.vector_service:
         | 
| 87 | 
            -
                        # Use the cached words from the initialized service
         | 
| 88 | 
            -
                        logger.info(f"📦 Using cached words from initialized vector service")
         | 
| 89 | 
            -
                        for topic in topics:
         | 
| 90 | 
            -
                            cached_words = await self.vector_service._get_cached_fallback(topic, difficulty, self.max_words // len(topics))
         | 
| 91 | 
            -
                            all_words.extend(cached_words)
         | 
| 92 | 
            -
                    else:
         | 
| 93 | 
            -
                        # Last resort: load static words directly
         | 
| 94 | 
            -
                        logger.warning(f"⚠️ No vector service available, loading static words directly")
         | 
| 95 | 
            -
                        all_words = await self._get_static_words(topics, difficulty)
         | 
| 96 |  | 
| 97 | 
            -
                     | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
                     | 
| 101 | 
            -
                    all_words = []
         | 
| 102 | 
            -
                    
         | 
| 103 | 
            -
                    for topic in topics:
         | 
| 104 | 
            -
                        # Try multiple case variations
         | 
| 105 | 
            -
                        for topic_variation in [topic, topic.capitalize(), topic.lower()]:
         | 
| 106 | 
            -
                            word_file = Path(__file__).parent.parent.parent / "data" / "word-lists" / f"{topic_variation.lower()}.json"
         | 
| 107 | 
            -
                            
         | 
| 108 | 
            -
                            if word_file.exists():
         | 
| 109 | 
            -
                                with open(word_file, 'r') as f:
         | 
| 110 | 
            -
                                    words = json.load(f)
         | 
| 111 | 
            -
                                    # Filter by difficulty
         | 
| 112 | 
            -
                                    filtered = self._filter_by_difficulty(words, difficulty)
         | 
| 113 | 
            -
                                    all_words.extend(filtered)
         | 
| 114 | 
            -
                                break
         | 
| 115 | 
            -
                    
         | 
| 116 | 
            -
                    return all_words
         | 
| 117 | 
            -
                
         | 
| 118 | 
            -
                def _filter_by_difficulty(self, words: List[Dict[str, Any]], difficulty: str) -> List[Dict[str, Any]]:
         | 
| 119 | 
            -
                    """Filter words by difficulty (length)."""
         | 
| 120 | 
            -
                    difficulty_map = {
         | 
| 121 | 
            -
                        "easy": {"min_len": 3, "max_len": 8},
         | 
| 122 | 
            -
                        "medium": {"min_len": 4, "max_len": 10},
         | 
| 123 | 
            -
                        "hard": {"min_len": 5, "max_len": 15}
         | 
| 124 | 
            -
                    }
         | 
| 125 |  | 
| 126 | 
            -
                     | 
| 127 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
| 128 |  | 
| 129 | 
             
                def _sort_words_for_crossword(self, words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
         | 
| 130 | 
             
                    """Sort words by crossword suitability."""
         | 
| @@ -271,11 +220,18 @@ class CrosswordGenerator: | |
| 271 | 
             
                            logger.info(f"🔧 Backtrack successful, trimming grid...")
         | 
| 272 | 
             
                            trimmed = self._trim_grid(grid, placed_words)
         | 
| 273 | 
             
                            logger.info(f"🔧 Grid trimmed, generating clues...")
         | 
| 274 | 
            -
                             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 275 |  | 
| 276 | 
             
                            return {
         | 
| 277 | 
             
                                "grid": trimmed["grid"],
         | 
| 278 | 
            -
                                "placed_words":  | 
| 279 | 
             
                                "clues": clues
         | 
| 280 | 
             
                            }
         | 
| 281 | 
             
                        else:
         | 
| @@ -634,6 +590,114 @@ class CrosswordGenerator: | |
| 634 |  | 
| 635 | 
             
                    return {"grid": trimmed_grid, "placed_words": updated_words}
         | 
| 636 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 637 | 
             
                def _create_simple_cross(self, word_list: List[str], word_objs: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
         | 
| 638 | 
             
                    """Create simple cross with two words."""
         | 
| 639 | 
             
                    if len(word_list) < 2:
         | 
| @@ -678,31 +742,29 @@ class CrosswordGenerator: | |
| 678 | 
             
                    ]
         | 
| 679 |  | 
| 680 | 
             
                    trimmed = self._trim_grid(grid, placed_words)
         | 
| 681 | 
            -
                     | 
|  | |
|  | |
|  | |
| 682 |  | 
| 683 | 
             
                    return {
         | 
| 684 | 
             
                        "grid": trimmed["grid"],
         | 
| 685 | 
            -
                        "placed_words":  | 
| 686 | 
             
                        "clues": clues
         | 
| 687 | 
             
                    }
         | 
| 688 |  | 
| 689 | 
             
                def _generate_clues(self, word_objs: List[Dict[str, Any]], placed_words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
         | 
| 690 | 
            -
                    """Generate clues for placed words."""
         | 
| 691 | 
            -
                    logger.info(f"🔧 _generate_clues: word_objs={len(word_objs)}, placed_words={len(placed_words)}")
         | 
| 692 | 
             
                    clues = []
         | 
| 693 |  | 
| 694 | 
             
                    try:
         | 
| 695 | 
            -
                        for  | 
| 696 | 
            -
                            logger.info(f"🔧 Processing placed word {i}: {placed_word.get('word', 'UNKNOWN')}")
         | 
| 697 | 
            -
                            
         | 
| 698 | 
             
                            # Find matching word object
         | 
| 699 | 
             
                            word_obj = next((w for w in word_objs if w["word"].upper() == placed_word["word"]), None)
         | 
| 700 |  | 
| 701 | 
            -
                            if word_obj:
         | 
| 702 | 
            -
                                 | 
| 703 | 
            -
                                clue_text = word_obj["clue"] if "clue" in word_obj else f"Clue for {placed_word['word']}"
         | 
| 704 | 
             
                            else:
         | 
| 705 | 
            -
                                logger.warning(f"⚠️ No matching word_obj found for {placed_word['word']}")
         | 
| 706 | 
             
                                clue_text = f"Clue for {placed_word['word']}"
         | 
| 707 |  | 
| 708 | 
             
                            clues.append({
         | 
| @@ -713,7 +775,6 @@ class CrosswordGenerator: | |
| 713 | 
             
                                "position": {"row": placed_word["row"], "col": placed_word["col"]}
         | 
| 714 | 
             
                            })
         | 
| 715 |  | 
| 716 | 
            -
                        logger.info(f"🔧 Generated {len(clues)} clues")
         | 
| 717 | 
             
                        return clues
         | 
| 718 | 
             
                    except Exception as e:
         | 
| 719 | 
             
                        logger.error(f"❌ Error in _generate_clues: {e}")
         | 
|  | |
| 4 |  | 
| 5 | 
             
            import asyncio
         | 
| 6 | 
             
            import json
         | 
| 7 | 
            +
            import logging
         | 
| 8 | 
             
            import random
         | 
| 9 | 
             
            import time
         | 
| 10 | 
             
            from pathlib import Path
         | 
| 11 | 
             
            from typing import Dict, List, Optional, Any, Tuple
         | 
|  | |
| 12 |  | 
| 13 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 14 |  | 
| 15 | 
             
            class CrosswordGenerator:
         | 
| 16 | 
            +
                def __init__(self, thematic_service=None):
         | 
| 17 | 
             
                    self.max_attempts = 100
         | 
| 18 | 
             
                    self.min_words = 6
         | 
| 19 | 
            +
                    self.thematic_service = thematic_service
         | 
|  | |
| 20 |  | 
| 21 | 
            +
                async def generate_puzzle(self, topics: List[str], difficulty: str = "medium", custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10) -> Optional[Dict[str, Any]]:
         | 
| 22 | 
             
                    """
         | 
| 23 | 
             
                    Generate a complete crossword puzzle.
         | 
| 24 | 
             
                    """
         | 
| 25 | 
             
                    try:
         | 
| 26 | 
            +
                        sentence_info = f", custom sentence: '{custom_sentence}'" if custom_sentence else ""
         | 
| 27 | 
            +
                        logger.info(f"🎯 Generating puzzle for topics: {topics}, difficulty: {difficulty}{sentence_info}, requested words: {requested_words}")
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 28 |  | 
| 29 | 
            +
                        # Get words from thematic AI service
         | 
| 30 | 
            +
                        words = await self._select_words(topics, difficulty, custom_sentence, multi_theme, requested_words)
         | 
| 31 |  | 
| 32 | 
             
                        if len(words) < self.min_words:
         | 
| 33 | 
             
                            logger.error(f"❌ Not enough words: {len(words)} < {self.min_words}")
         | 
|  | |
| 50 | 
             
                                "difficulty": difficulty,
         | 
| 51 | 
             
                                "wordCount": len(grid_result["placed_words"]),
         | 
| 52 | 
             
                                "size": len(grid_result["grid"]),
         | 
| 53 | 
            +
                                "aiGenerated": True
         | 
| 54 | 
             
                            }
         | 
| 55 | 
             
                        }
         | 
| 56 |  | 
|  | |
| 58 | 
             
                        logger.error(f"❌ Error generating puzzle: {e}")
         | 
| 59 | 
             
                        raise
         | 
| 60 |  | 
| 61 | 
            +
                async def _select_words(self, topics: List[str], difficulty: str, custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10) -> List[Dict[str, Any]]:
         | 
| 62 | 
            +
                    """Select words for the crossword using thematic AI service."""
         | 
| 63 | 
            +
                    if not self.thematic_service:
         | 
| 64 | 
            +
                        raise Exception("Thematic service is required for word generation")
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 65 |  | 
| 66 | 
            +
                    logger.info(f"🎯 Using thematic AI service for word generation with {requested_words} requested words")
         | 
| 67 | 
            +
                    
         | 
| 68 | 
            +
                    # Use the dedicated crossword method for better word selection
         | 
| 69 | 
            +
                    words = await self.thematic_service.find_words_for_crossword(topics, difficulty, requested_words, custom_sentence, multi_theme)
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 70 |  | 
| 71 | 
            +
                    if len(words) < self.min_words:
         | 
| 72 | 
            +
                        raise Exception(f"Thematic service generated insufficient words: {len(words)} < {self.min_words}")
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                    logger.info(f"✅ Thematic service generated {len(words)} words")
         | 
| 75 | 
            +
                    return self._sort_words_for_crossword(words)
         | 
| 76 | 
            +
                
         | 
| 77 |  | 
| 78 | 
             
                def _sort_words_for_crossword(self, words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
         | 
| 79 | 
             
                    """Sort words by crossword suitability."""
         | 
|  | |
| 220 | 
             
                            logger.info(f"🔧 Backtrack successful, trimming grid...")
         | 
| 221 | 
             
                            trimmed = self._trim_grid(grid, placed_words)
         | 
| 222 | 
             
                            logger.info(f"🔧 Grid trimmed, generating clues...")
         | 
| 223 | 
            +
                            
         | 
| 224 | 
            +
                            # Generate clues first so we can display them with positions
         | 
| 225 | 
            +
                            clues_data = self._generate_clues_data(word_objs, trimmed["placed_words"])
         | 
| 226 | 
            +
                            
         | 
| 227 | 
            +
                            logger.info(f"🔧 Clues generated, assigning proper crossword numbers...")
         | 
| 228 | 
            +
                            
         | 
| 229 | 
            +
                            # Fix numbering based on grid position (reading order) and log with clues
         | 
| 230 | 
            +
                            numbered_words, clues = self._assign_numbers_and_clues(trimmed["placed_words"], clues_data)
         | 
| 231 |  | 
| 232 | 
             
                            return {
         | 
| 233 | 
             
                                "grid": trimmed["grid"],
         | 
| 234 | 
            +
                                "placed_words": numbered_words,
         | 
| 235 | 
             
                                "clues": clues
         | 
| 236 | 
             
                            }
         | 
| 237 | 
             
                        else:
         | 
|  | |
| 590 |  | 
| 591 | 
             
                    return {"grid": trimmed_grid, "placed_words": updated_words}
         | 
| 592 |  | 
| 593 | 
            +
                def _assign_crossword_numbers(self, placed_words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
         | 
| 594 | 
            +
                    """
         | 
| 595 | 
            +
                    Assign proper crossword numbers based on grid position (reading order).
         | 
| 596 | 
            +
                    
         | 
| 597 | 
            +
                    Crossword numbering rules:
         | 
| 598 | 
            +
                    1. Numbers are assigned to word starting positions
         | 
| 599 | 
            +
                    2. Reading order: top-to-bottom, then left-to-right  
         | 
| 600 | 
            +
                    3. A single number can be shared by both across and down words starting at the same cell
         | 
| 601 | 
            +
                    """
         | 
| 602 | 
            +
                    if not placed_words:
         | 
| 603 | 
            +
                        return placed_words
         | 
| 604 | 
            +
                    
         | 
| 605 | 
            +
                    # Collect all unique starting positions
         | 
| 606 | 
            +
                    starting_positions = {}  # (row, col) -> list of words starting at that position
         | 
| 607 | 
            +
                    
         | 
| 608 | 
            +
                    for word in placed_words:
         | 
| 609 | 
            +
                        pos_key = (word["row"], word["col"])
         | 
| 610 | 
            +
                        if pos_key not in starting_positions:
         | 
| 611 | 
            +
                            starting_positions[pos_key] = []
         | 
| 612 | 
            +
                        starting_positions[pos_key].append(word)
         | 
| 613 | 
            +
                    
         | 
| 614 | 
            +
                    # Sort positions by reading order (top-to-bottom, left-to-right)
         | 
| 615 | 
            +
                    sorted_positions = sorted(starting_positions.keys(), key=lambda pos: (pos[0], pos[1]))
         | 
| 616 | 
            +
                    
         | 
| 617 | 
            +
                    # Assign numbers
         | 
| 618 | 
            +
                    numbered_words = []
         | 
| 619 | 
            +
                    for i, pos in enumerate(sorted_positions):
         | 
| 620 | 
            +
                        number = i + 1  # Crossword numbers start at 1
         | 
| 621 | 
            +
                        
         | 
| 622 | 
            +
                        # Assign this number to all words starting at this position
         | 
| 623 | 
            +
                        for word in starting_positions[pos]:
         | 
| 624 | 
            +
                            numbered_word = word.copy()
         | 
| 625 | 
            +
                            numbered_word["number"] = number
         | 
| 626 | 
            +
                            numbered_words.append(numbered_word)
         | 
| 627 | 
            +
                    
         | 
| 628 | 
            +
                    logger.info(f"🔢 Assigned crossword numbers: {len(sorted_positions)} unique starting positions (legacy function)")
         | 
| 629 | 
            +
                    
         | 
| 630 | 
            +
                    return numbered_words
         | 
| 631 | 
            +
                
         | 
| 632 | 
            +
                def _generate_clues_data(self, word_objs: List[Dict[str, Any]], placed_words: List[Dict[str, Any]]) -> Dict[str, str]:
         | 
| 633 | 
            +
                    """Generate a mapping of words to their clues."""
         | 
| 634 | 
            +
                    clues_map = {}
         | 
| 635 | 
            +
                    
         | 
| 636 | 
            +
                    for placed_word in placed_words:
         | 
| 637 | 
            +
                        # Find matching word object
         | 
| 638 | 
            +
                        word_obj = next((w for w in word_objs if w["word"].upper() == placed_word["word"]), None)
         | 
| 639 | 
            +
                        
         | 
| 640 | 
            +
                        if word_obj and "clue" in word_obj:
         | 
| 641 | 
            +
                            clues_map[placed_word["word"]] = word_obj["clue"]
         | 
| 642 | 
            +
                        else:
         | 
| 643 | 
            +
                            clues_map[placed_word["word"]] = f"Clue for {placed_word['word']}"
         | 
| 644 | 
            +
                    
         | 
| 645 | 
            +
                    return clues_map
         | 
| 646 | 
            +
                
         | 
| 647 | 
            +
                def _assign_numbers_and_clues(self, placed_words: List[Dict[str, Any]], clues_data: Dict[str, str]) -> tuple:
         | 
| 648 | 
            +
                    """
         | 
| 649 | 
            +
                    Assign proper crossword numbers based on grid position and create clues with enhanced logging.
         | 
| 650 | 
            +
                    
         | 
| 651 | 
            +
                    Returns: (numbered_words, clues_list)
         | 
| 652 | 
            +
                    """
         | 
| 653 | 
            +
                    if not placed_words:
         | 
| 654 | 
            +
                        return placed_words, []
         | 
| 655 | 
            +
                    
         | 
| 656 | 
            +
                    # Collect all unique starting positions
         | 
| 657 | 
            +
                    starting_positions = {}  # (row, col) -> list of words starting at that position
         | 
| 658 | 
            +
                    
         | 
| 659 | 
            +
                    for word in placed_words:
         | 
| 660 | 
            +
                        pos_key = (word["row"], word["col"])
         | 
| 661 | 
            +
                        if pos_key not in starting_positions:
         | 
| 662 | 
            +
                            starting_positions[pos_key] = []
         | 
| 663 | 
            +
                        starting_positions[pos_key].append(word)
         | 
| 664 | 
            +
                    
         | 
| 665 | 
            +
                    # Sort positions by reading order (top-to-bottom, left-to-right)
         | 
| 666 | 
            +
                    sorted_positions = sorted(starting_positions.keys(), key=lambda pos: (pos[0], pos[1]))
         | 
| 667 | 
            +
                    
         | 
| 668 | 
            +
                    # Assign numbers and create both numbered words and clues
         | 
| 669 | 
            +
                    numbered_words = []
         | 
| 670 | 
            +
                    clues = []
         | 
| 671 | 
            +
                    
         | 
| 672 | 
            +
                    logger.info(f"🔢 Assigned crossword numbers: {len(sorted_positions)} unique starting positions")
         | 
| 673 | 
            +
                    
         | 
| 674 | 
            +
                    for i, pos in enumerate(sorted_positions):
         | 
| 675 | 
            +
                        number = i + 1  # Crossword numbers start at 1
         | 
| 676 | 
            +
                        
         | 
| 677 | 
            +
                        # Process all words starting at this position
         | 
| 678 | 
            +
                        for word in starting_positions[pos]:
         | 
| 679 | 
            +
                            numbered_word = word.copy()
         | 
| 680 | 
            +
                            numbered_word["number"] = number
         | 
| 681 | 
            +
                            numbered_words.append(numbered_word)
         | 
| 682 | 
            +
                            
         | 
| 683 | 
            +
                            # Create clue object
         | 
| 684 | 
            +
                            clue_text = clues_data.get(word["word"], f"Clue for {word['word']}")
         | 
| 685 | 
            +
                            direction = "across" if word["direction"] == "horizontal" else "down"
         | 
| 686 | 
            +
                            
         | 
| 687 | 
            +
                            clue = {
         | 
| 688 | 
            +
                                "number": number,
         | 
| 689 | 
            +
                                "word": word["word"],
         | 
| 690 | 
            +
                                "text": clue_text,
         | 
| 691 | 
            +
                                "direction": direction,
         | 
| 692 | 
            +
                                "position": {"row": word["row"], "col": word["col"]}
         | 
| 693 | 
            +
                            }
         | 
| 694 | 
            +
                            clues.append(clue)
         | 
| 695 | 
            +
                            
         | 
| 696 | 
            +
                            # Enhanced logging with clues
         | 
| 697 | 
            +
                            logger.info(f"  {number} {direction}: {word['word']} at ({word['row']}, {word['col']}) - \"{clue_text}\"")
         | 
| 698 | 
            +
                    
         | 
| 699 | 
            +
                    return numbered_words, clues
         | 
| 700 | 
            +
                
         | 
| 701 | 
             
                def _create_simple_cross(self, word_list: List[str], word_objs: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
         | 
| 702 | 
             
                    """Create simple cross with two words."""
         | 
| 703 | 
             
                    if len(word_list) < 2:
         | 
|  | |
| 742 | 
             
                    ]
         | 
| 743 |  | 
| 744 | 
             
                    trimmed = self._trim_grid(grid, placed_words)
         | 
| 745 | 
            +
                    
         | 
| 746 | 
            +
                    # Generate clues first, then assign numbers with enhanced logging
         | 
| 747 | 
            +
                    clues_data = self._generate_clues_data(word_objs[:2], trimmed["placed_words"])
         | 
| 748 | 
            +
                    numbered_words, clues = self._assign_numbers_and_clues(trimmed["placed_words"], clues_data)
         | 
| 749 |  | 
| 750 | 
             
                    return {
         | 
| 751 | 
             
                        "grid": trimmed["grid"],
         | 
| 752 | 
            +
                        "placed_words": numbered_words,
         | 
| 753 | 
             
                        "clues": clues
         | 
| 754 | 
             
                    }
         | 
| 755 |  | 
| 756 | 
             
                def _generate_clues(self, word_objs: List[Dict[str, Any]], placed_words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
         | 
| 757 | 
            +
                    """Generate clues for placed words (legacy function - use _assign_numbers_and_clues for better logging)."""
         | 
|  | |
| 758 | 
             
                    clues = []
         | 
| 759 |  | 
| 760 | 
             
                    try:
         | 
| 761 | 
            +
                        for placed_word in placed_words:
         | 
|  | |
|  | |
| 762 | 
             
                            # Find matching word object
         | 
| 763 | 
             
                            word_obj = next((w for w in word_objs if w["word"].upper() == placed_word["word"]), None)
         | 
| 764 |  | 
| 765 | 
            +
                            if word_obj and "clue" in word_obj:
         | 
| 766 | 
            +
                                clue_text = word_obj["clue"]
         | 
|  | |
| 767 | 
             
                            else:
         | 
|  | |
| 768 | 
             
                                clue_text = f"Clue for {placed_word['word']}"
         | 
| 769 |  | 
| 770 | 
             
                            clues.append({
         | 
|  | |
| 775 | 
             
                                "position": {"row": placed_word["row"], "col": placed_word["col"]}
         | 
| 776 | 
             
                            })
         | 
| 777 |  | 
|  | |
| 778 | 
             
                        return clues
         | 
| 779 | 
             
                    except Exception as e:
         | 
| 780 | 
             
                        logger.error(f"❌ Error in _generate_clues: {e}")
         | 
    	
        crossword-app/backend-py/src/services/crossword_generator_wrapper.py
    CHANGED
    
    | @@ -12,16 +12,18 @@ class CrosswordGenerator: | |
| 12 | 
             
                Wrapper that uses the fixed crossword generator implementation.
         | 
| 13 | 
             
                """
         | 
| 14 |  | 
| 15 | 
            -
                def __init__(self,  | 
| 16 | 
            -
                    self. | 
| 17 | 
             
                    self.min_words = 8
         | 
| 18 | 
             
                    self.max_words = 15
         | 
| 19 |  | 
| 20 | 
             
                async def generate_puzzle(
         | 
| 21 | 
             
                    self, 
         | 
| 22 | 
             
                    topics: List[str], 
         | 
| 23 | 
            -
                    difficulty: str = "medium", | 
| 24 | 
            -
                     | 
|  | |
|  | |
| 25 | 
             
                ) -> Dict[str, Any]:
         | 
| 26 | 
             
                    """
         | 
| 27 | 
             
                    Generate a complete crossword puzzle using the fixed generator.
         | 
| @@ -29,19 +31,21 @@ class CrosswordGenerator: | |
| 29 | 
             
                    Args:
         | 
| 30 | 
             
                        topics: List of topic strings
         | 
| 31 | 
             
                        difficulty: "easy", "medium", or "hard"
         | 
| 32 | 
            -
                         | 
|  | |
|  | |
| 33 |  | 
| 34 | 
             
                    Returns:
         | 
| 35 | 
             
                        Dictionary containing grid, clues, and metadata
         | 
| 36 | 
             
                    """
         | 
| 37 | 
             
                    try:
         | 
| 38 | 
            -
                        logger.info(f"🎯 Using fixed crossword generator for topics: {topics}")
         | 
| 39 |  | 
| 40 | 
            -
                        # Use the fixed generator implementation with the initialized  | 
| 41 | 
             
                        from .crossword_generator import CrosswordGenerator as ActualGenerator
         | 
| 42 | 
            -
                        actual_generator = ActualGenerator( | 
| 43 |  | 
| 44 | 
            -
                        puzzle = await actual_generator.generate_puzzle(topics, difficulty,  | 
| 45 |  | 
| 46 | 
             
                        logger.info(f"✅ Generated crossword with fixed algorithm")
         | 
| 47 | 
             
                        return puzzle
         | 
| @@ -50,9 +54,9 @@ class CrosswordGenerator: | |
| 50 | 
             
                        logger.error(f"❌ Failed to generate puzzle: {e}")
         | 
| 51 | 
             
                        raise
         | 
| 52 |  | 
| 53 | 
            -
                async def generate_words_for_topics(self, topics: List[str], difficulty: str,  | 
| 54 | 
             
                    """Backward compatibility method."""
         | 
| 55 | 
             
                    # This method is kept for compatibility but delegates to the fixed generator
         | 
| 56 | 
             
                    from .crossword_generator import CrosswordGenerator as ActualGenerator
         | 
| 57 | 
            -
                    actual_generator = ActualGenerator()
         | 
| 58 | 
            -
                    return await actual_generator._select_words(topics, difficulty,  | 
|  | |
| 12 | 
             
                Wrapper that uses the fixed crossword generator implementation.
         | 
| 13 | 
             
                """
         | 
| 14 |  | 
| 15 | 
            +
                def __init__(self, thematic_service=None):
         | 
| 16 | 
            +
                    self.thematic_service = thematic_service
         | 
| 17 | 
             
                    self.min_words = 8
         | 
| 18 | 
             
                    self.max_words = 15
         | 
| 19 |  | 
| 20 | 
             
                async def generate_puzzle(
         | 
| 21 | 
             
                    self, 
         | 
| 22 | 
             
                    topics: List[str], 
         | 
| 23 | 
            +
                    difficulty: str = "medium",
         | 
| 24 | 
            +
                    custom_sentence: str = None,
         | 
| 25 | 
            +
                    multi_theme: bool = True,
         | 
| 26 | 
            +
                    requested_words: int = 10
         | 
| 27 | 
             
                ) -> Dict[str, Any]:
         | 
| 28 | 
             
                    """
         | 
| 29 | 
             
                    Generate a complete crossword puzzle using the fixed generator.
         | 
|  | |
| 31 | 
             
                    Args:
         | 
| 32 | 
             
                        topics: List of topic strings
         | 
| 33 | 
             
                        difficulty: "easy", "medium", or "hard"
         | 
| 34 | 
            +
                        custom_sentence: Optional custom sentence to influence word selection
         | 
| 35 | 
            +
                        multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
         | 
| 36 | 
            +
                        requested_words: Number of words requested by frontend
         | 
| 37 |  | 
| 38 | 
             
                    Returns:
         | 
| 39 | 
             
                        Dictionary containing grid, clues, and metadata
         | 
| 40 | 
             
                    """
         | 
| 41 | 
             
                    try:
         | 
| 42 | 
            +
                        logger.info(f"🎯 Using fixed crossword generator for topics: {topics}, requested words: {requested_words}")
         | 
| 43 |  | 
| 44 | 
            +
                        # Use the fixed generator implementation with the initialized thematic service
         | 
| 45 | 
             
                        from .crossword_generator import CrosswordGenerator as ActualGenerator
         | 
| 46 | 
            +
                        actual_generator = ActualGenerator(thematic_service=self.thematic_service)
         | 
| 47 |  | 
| 48 | 
            +
                        puzzle = await actual_generator.generate_puzzle(topics, difficulty, custom_sentence, multi_theme, requested_words)
         | 
| 49 |  | 
| 50 | 
             
                        logger.info(f"✅ Generated crossword with fixed algorithm")
         | 
| 51 | 
             
                        return puzzle
         | 
|  | |
| 54 | 
             
                        logger.error(f"❌ Failed to generate puzzle: {e}")
         | 
| 55 | 
             
                        raise
         | 
| 56 |  | 
| 57 | 
            +
                async def generate_words_for_topics(self, topics: List[str], difficulty: str, custom_sentence: str = None) -> List[Dict[str, Any]]:
         | 
| 58 | 
             
                    """Backward compatibility method."""
         | 
| 59 | 
             
                    # This method is kept for compatibility but delegates to the fixed generator
         | 
| 60 | 
             
                    from .crossword_generator import CrosswordGenerator as ActualGenerator
         | 
| 61 | 
            +
                    actual_generator = ActualGenerator(thematic_service=self.thematic_service)
         | 
| 62 | 
            +
                    return await actual_generator._select_words(topics, difficulty, custom_sentence)
         | 
    	
        crossword-app/backend-py/src/services/thematic_word_service.py
    ADDED
    
    | @@ -0,0 +1,1057 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
            """
         | 
| 3 | 
            +
            Unified Thematic Word Generator using WordFreq + SentenceTransformers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Eliminates vocabulary redundancy by using WordFreq as the single vocabulary source
         | 
| 6 | 
            +
            for both word lists and frequency data, with all-mpnet-base-v2 for embeddings.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            Features:
         | 
| 9 | 
            +
            - Single vocabulary source (WordFreq 319K words vs previous 3 separate sources)
         | 
| 10 | 
            +
            - Unified filtering for crossword-suitable words
         | 
| 11 | 
            +
            - 10-tier frequency classification system
         | 
| 12 | 
            +
            - Compatible with crossword backend services
         | 
| 13 | 
            +
            - Comprehensive modern vocabulary with proper frequency data
         | 
| 14 | 
            +
            - Environment variable configuration for cache paths and settings
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            Environment Variables:
         | 
| 17 | 
            +
            - CACHE_DIR: Cache directory for all thematic service files (default: ./model_cache)
         | 
| 18 | 
            +
            - THEMATIC_VOCAB_SIZE_LIMIT: Maximum vocabulary size (default: 100000)
         | 
| 19 | 
            +
            - MAX_VOCABULARY_SIZE: Fallback vocab size limit (used if THEMATIC_VOCAB_SIZE_LIMIT not set)
         | 
| 20 | 
            +
            - THEMATIC_MODEL_NAME: Sentence transformer model to use (default: all-mpnet-base-v2)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            Cache Structure:
         | 
| 23 | 
            +
            - {cache_dir}/vocabulary_{size}.pkl - Processed vocabulary words
         | 
| 24 | 
            +
            - {cache_dir}/frequencies_{size}.pkl - Word frequency data
         | 
| 25 | 
            +
            - {cache_dir}/embeddings_{model}_{size}.npy - Word embeddings
         | 
| 26 | 
            +
            - {cache_dir}/sentence-transformers/ - Hugging Face model cache
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            Usage:
         | 
| 29 | 
            +
              # Use environment variables for production
         | 
| 30 | 
            +
              export CACHE_DIR=/app/cache
         | 
| 31 | 
            +
              export THEMATIC_VOCAB_SIZE_LIMIT=50000
         | 
| 32 | 
            +
              
         | 
| 33 | 
            +
              # Or pass directly to constructor for development
         | 
| 34 | 
            +
              service = ThematicWordService(cache_dir="/custom/path", vocab_size_limit=25000)
         | 
| 35 | 
            +
            """
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            import os
         | 
| 38 | 
            +
            import csv
         | 
| 39 | 
            +
            import pickle
         | 
| 40 | 
            +
            import numpy as np
         | 
| 41 | 
            +
            import logging
         | 
| 42 | 
            +
            import asyncio
         | 
| 43 | 
            +
            import random
         | 
| 44 | 
            +
            from typing import List, Tuple, Optional, Dict, Set, Any
         | 
| 45 | 
            +
            from sentence_transformers import SentenceTransformer
         | 
| 46 | 
            +
            from sklearn.metrics.pairwise import cosine_similarity
         | 
| 47 | 
            +
            from sklearn.cluster import KMeans
         | 
| 48 | 
            +
            from datetime import datetime
         | 
| 49 | 
            +
            import time
         | 
| 50 | 
            +
            from collections import Counter
         | 
| 51 | 
            +
            from pathlib import Path
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            # WordFreq imports (assumed to be available)
         | 
| 54 | 
            +
            from wordfreq import word_frequency, zipf_frequency, top_n_list
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            # Use backend's logging configuration
         | 
| 57 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            def get_timestamp():
         | 
| 60 | 
            +
                return datetime.now().strftime("%H:%M:%S")
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            def get_datetimestamp():
         | 
| 63 | 
            +
                return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
         | 
| 64 | 
            +
             | 
| 65 | 
            +
             | 
| 66 | 
            +
            class VocabularyManager:
         | 
| 67 | 
            +
                """
         | 
| 68 | 
            +
                Centralized vocabulary management using WordFreq as the single source.
         | 
| 69 | 
            +
                Handles loading, filtering, caching, and frequency data generation.
         | 
| 70 | 
            +
                """
         | 
| 71 | 
            +
                
         | 
| 72 | 
            +
                def __init__(self, cache_dir: Optional[str] = None, vocab_size_limit: Optional[int] = None):
         | 
| 73 | 
            +
                    """Initialize vocabulary manager.
         | 
| 74 | 
            +
                    
         | 
| 75 | 
            +
                    Args:
         | 
| 76 | 
            +
                        cache_dir: Directory for caching vocabulary and embeddings
         | 
| 77 | 
            +
                        vocab_size_limit: Maximum vocabulary size (None for full WordFreq vocabulary)
         | 
| 78 | 
            +
                    """
         | 
| 79 | 
            +
                    if cache_dir is None:
         | 
| 80 | 
            +
                        # Check environment variable for cache directory
         | 
| 81 | 
            +
                        cache_dir = os.getenv("CACHE_DIR")
         | 
| 82 | 
            +
                        if cache_dir is None:
         | 
| 83 | 
            +
                            cache_dir = os.path.join(os.path.dirname(__file__), 'model_cache')
         | 
| 84 | 
            +
                    
         | 
| 85 | 
            +
                    self.cache_dir = Path(cache_dir)
         | 
| 86 | 
            +
                    self.cache_dir.mkdir(parents=True, exist_ok=True)
         | 
| 87 | 
            +
                    
         | 
| 88 | 
            +
                    # Vocabulary size configuration
         | 
| 89 | 
            +
                    self.vocab_size_limit = vocab_size_limit or int(os.getenv("THEMATIC_VOCAB_SIZE_LIMIT", 
         | 
| 90 | 
            +
                                                                             os.getenv("MAX_VOCABULARY_SIZE", "100000")))
         | 
| 91 | 
            +
                    
         | 
| 92 | 
            +
                    # Cache paths
         | 
| 93 | 
            +
                    self.vocab_cache_path = self.cache_dir / f"vocabulary_{self.vocab_size_limit}.pkl"
         | 
| 94 | 
            +
                    self.frequency_cache_path = self.cache_dir / f"frequencies_{self.vocab_size_limit}.pkl"
         | 
| 95 | 
            +
                    
         | 
| 96 | 
            +
                    # Loaded data
         | 
| 97 | 
            +
                    self.vocabulary: List[str] = []
         | 
| 98 | 
            +
                    self.word_frequencies: Counter = Counter()
         | 
| 99 | 
            +
                    self.is_loaded = False
         | 
| 100 | 
            +
                    
         | 
| 101 | 
            +
                def load_vocabulary(self) -> Tuple[List[str], Counter]:
         | 
| 102 | 
            +
                    """Load vocabulary and frequency data, with caching."""
         | 
| 103 | 
            +
                    if self.is_loaded:
         | 
| 104 | 
            +
                        return self.vocabulary, self.word_frequencies
         | 
| 105 | 
            +
                        
         | 
| 106 | 
            +
                    # Try loading from cache
         | 
| 107 | 
            +
                    if self._load_from_cache():
         | 
| 108 | 
            +
                        logger.info(f"✅ Loaded vocabulary from cache: {len(self.vocabulary):,} words")
         | 
| 109 | 
            +
                        self.is_loaded = True
         | 
| 110 | 
            +
                        return self.vocabulary, self.word_frequencies
         | 
| 111 | 
            +
                    
         | 
| 112 | 
            +
                    # Generate from WordFreq
         | 
| 113 | 
            +
                    logger.info("🔄 Generating vocabulary from WordFreq...")
         | 
| 114 | 
            +
                    self._generate_vocabulary_from_wordfreq()
         | 
| 115 | 
            +
                    
         | 
| 116 | 
            +
                    # Save to cache
         | 
| 117 | 
            +
                    self._save_to_cache()
         | 
| 118 | 
            +
                    
         | 
| 119 | 
            +
                    self.is_loaded = True
         | 
| 120 | 
            +
                    return self.vocabulary, self.word_frequencies
         | 
| 121 | 
            +
                
         | 
| 122 | 
            +
                def _load_from_cache(self) -> bool:
         | 
| 123 | 
            +
                    """Load vocabulary and frequencies from cache."""
         | 
| 124 | 
            +
                    try:
         | 
| 125 | 
            +
                        if self.vocab_cache_path.exists() and self.frequency_cache_path.exists():
         | 
| 126 | 
            +
                            logger.info(f"📦 Loading vocabulary from cache...")
         | 
| 127 | 
            +
                            logger.info(f"  Vocab cache: {self.vocab_cache_path}")
         | 
| 128 | 
            +
                            logger.info(f"  Freq cache: {self.frequency_cache_path}")
         | 
| 129 | 
            +
                            
         | 
| 130 | 
            +
                            # Validate cache files are readable
         | 
| 131 | 
            +
                            if not os.access(self.vocab_cache_path, os.R_OK):
         | 
| 132 | 
            +
                                logger.warning(f"⚠️ Vocabulary cache file not readable: {self.vocab_cache_path}")
         | 
| 133 | 
            +
                                return False
         | 
| 134 | 
            +
                                
         | 
| 135 | 
            +
                            if not os.access(self.frequency_cache_path, os.R_OK):
         | 
| 136 | 
            +
                                logger.warning(f"⚠️ Frequency cache file not readable: {self.frequency_cache_path}")
         | 
| 137 | 
            +
                                return False
         | 
| 138 | 
            +
                            
         | 
| 139 | 
            +
                            with open(self.vocab_cache_path, 'rb') as f:
         | 
| 140 | 
            +
                                self.vocabulary = pickle.load(f)
         | 
| 141 | 
            +
                                
         | 
| 142 | 
            +
                            with open(self.frequency_cache_path, 'rb') as f:
         | 
| 143 | 
            +
                                self.word_frequencies = pickle.load(f)
         | 
| 144 | 
            +
                            
         | 
| 145 | 
            +
                            # Validate loaded data
         | 
| 146 | 
            +
                            if not self.vocabulary or not self.word_frequencies:
         | 
| 147 | 
            +
                                logger.warning("⚠️ Cache files contain empty data")
         | 
| 148 | 
            +
                                return False
         | 
| 149 | 
            +
                                
         | 
| 150 | 
            +
                            logger.info(f"✅ Loaded {len(self.vocabulary):,} words and {len(self.word_frequencies):,} frequencies from cache")
         | 
| 151 | 
            +
                            return True
         | 
| 152 | 
            +
                        else:
         | 
| 153 | 
            +
                            missing = []
         | 
| 154 | 
            +
                            if not self.vocab_cache_path.exists():
         | 
| 155 | 
            +
                                missing.append(f"vocabulary ({self.vocab_cache_path})")
         | 
| 156 | 
            +
                            if not self.frequency_cache_path.exists():
         | 
| 157 | 
            +
                                missing.append(f"frequency ({self.frequency_cache_path})")
         | 
| 158 | 
            +
                            logger.info(f"📂 Cache files missing: {', '.join(missing)}")
         | 
| 159 | 
            +
                            return False
         | 
| 160 | 
            +
                    except Exception as e:
         | 
| 161 | 
            +
                        logger.warning(f"⚠️ Cache loading failed: {e}")
         | 
| 162 | 
            +
                        
         | 
| 163 | 
            +
                    return False
         | 
| 164 | 
            +
                
         | 
| 165 | 
            +
                def _save_to_cache(self):
         | 
| 166 | 
            +
                    """Save vocabulary and frequencies to cache."""
         | 
| 167 | 
            +
                    try:
         | 
| 168 | 
            +
                        logger.info("💾 Saving vocabulary to cache...")
         | 
| 169 | 
            +
                        
         | 
| 170 | 
            +
                        with open(self.vocab_cache_path, 'wb') as f:
         | 
| 171 | 
            +
                            pickle.dump(self.vocabulary, f)
         | 
| 172 | 
            +
                            
         | 
| 173 | 
            +
                        with open(self.frequency_cache_path, 'wb') as f:
         | 
| 174 | 
            +
                            pickle.dump(self.word_frequencies, f)
         | 
| 175 | 
            +
                            
         | 
| 176 | 
            +
                        logger.info("✅ Vocabulary cached successfully")
         | 
| 177 | 
            +
                    except Exception as e:
         | 
| 178 | 
            +
                        logger.warning(f"⚠️ Cache saving failed: {e}")
         | 
| 179 | 
            +
                
         | 
| 180 | 
            +
                def _generate_vocabulary_from_wordfreq(self):
         | 
| 181 | 
            +
                    """Generate filtered vocabulary from WordFreq database."""
         | 
| 182 | 
            +
                    logger.info(f"📚 Fetching top {self.vocab_size_limit:,} words from WordFreq...")
         | 
| 183 | 
            +
                    
         | 
| 184 | 
            +
                    # Get comprehensive word list from WordFreq
         | 
| 185 | 
            +
                    raw_words = top_n_list('en', self.vocab_size_limit * 2, wordlist='large')  # Get extra for filtering
         | 
| 186 | 
            +
                    logger.info(f"📥 Retrieved {len(raw_words):,} raw words from WordFreq")
         | 
| 187 | 
            +
                    
         | 
| 188 | 
            +
                    # Apply crossword-suitable filtering
         | 
| 189 | 
            +
                    filtered_words = []
         | 
| 190 | 
            +
                    frequency_data = Counter()
         | 
| 191 | 
            +
                    
         | 
| 192 | 
            +
                    logger.info("🔍 Applying crossword filtering...")
         | 
| 193 | 
            +
                    for word in raw_words:
         | 
| 194 | 
            +
                        if self._is_crossword_suitable(word):
         | 
| 195 | 
            +
                            filtered_words.append(word.lower())
         | 
| 196 | 
            +
                            
         | 
| 197 | 
            +
                            # Get frequency data
         | 
| 198 | 
            +
                            try:
         | 
| 199 | 
            +
                                freq = word_frequency(word, 'en', wordlist='large')
         | 
| 200 | 
            +
                                if freq > 0:
         | 
| 201 | 
            +
                                    # Scale frequency to preserve precision
         | 
| 202 | 
            +
                                    frequency_data[word.lower()] = int(freq * 1e9)
         | 
| 203 | 
            +
                            except:
         | 
| 204 | 
            +
                                frequency_data[word.lower()] = 1  # Minimal frequency for unknown words
         | 
| 205 | 
            +
                            
         | 
| 206 | 
            +
                            if len(filtered_words) >= self.vocab_size_limit:
         | 
| 207 | 
            +
                                break
         | 
| 208 | 
            +
                    
         | 
| 209 | 
            +
                    # Remove duplicates and sort
         | 
| 210 | 
            +
                    self.vocabulary = sorted(list(set(filtered_words)))
         | 
| 211 | 
            +
                    self.word_frequencies = frequency_data
         | 
| 212 | 
            +
                    
         | 
| 213 | 
            +
                    logger.info(f"✅ Generated filtered vocabulary: {len(self.vocabulary):,} words")
         | 
| 214 | 
            +
                    logger.info(f"📊 Frequency data coverage: {len(self.word_frequencies):,} words")
         | 
| 215 | 
            +
                
         | 
| 216 | 
            +
                def _is_crossword_suitable(self, word: str) -> bool:
         | 
| 217 | 
            +
                    """Check if word is suitable for crosswords."""
         | 
| 218 | 
            +
                    word = word.lower().strip()
         | 
| 219 | 
            +
                    
         | 
| 220 | 
            +
                    # Length check (3-12 characters for crosswords)
         | 
| 221 | 
            +
                    if len(word) < 3 or len(word) > 12:
         | 
| 222 | 
            +
                        return False
         | 
| 223 | 
            +
                        
         | 
| 224 | 
            +
                    # Must be alphabetic only
         | 
| 225 | 
            +
                    if not word.isalpha():
         | 
| 226 | 
            +
                        return False
         | 
| 227 | 
            +
                        
         | 
| 228 | 
            +
                    # Skip boring/common words
         | 
| 229 | 
            +
                    boring_words = {
         | 
| 230 | 
            +
                        'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'this', 'that',
         | 
| 231 | 
            +
                        'with', 'from', 'they', 'were', 'been', 'have', 'their', 'said', 'each',
         | 
| 232 | 
            +
                        'which', 'what', 'there', 'will', 'more', 'when', 'some', 'like', 'into',
         | 
| 233 | 
            +
                        'time', 'very', 'only', 'has', 'had', 'who', 'its', 'now', 'find', 'long',
         | 
| 234 | 
            +
                        'down', 'day', 'did', 'get', 'come', 'made', 'may', 'part'
         | 
| 235 | 
            +
                    }
         | 
| 236 | 
            +
                    
         | 
| 237 | 
            +
                    if word in boring_words:
         | 
| 238 | 
            +
                        return False
         | 
| 239 | 
            +
                        
         | 
| 240 | 
            +
                    # Skip obvious plurals (simple heuristic)
         | 
| 241 | 
            +
                    if len(word) > 4 and word.endswith('s') and not word.endswith(('ss', 'us', 'is')):
         | 
| 242 | 
            +
                        return False
         | 
| 243 | 
            +
                        
         | 
| 244 | 
            +
                    # Skip words with repeated characters (often not real words)
         | 
| 245 | 
            +
                    if len(set(word)) < len(word) * 0.6:  # Less than 60% unique characters
         | 
| 246 | 
            +
                        return False
         | 
| 247 | 
            +
                        
         | 
| 248 | 
            +
                    return True
         | 
| 249 | 
            +
             | 
| 250 | 
            +
             | 
| 251 | 
            +
            class ThematicWordService:
         | 
| 252 | 
            +
                """
         | 
| 253 | 
            +
                Unified thematic word generator using WordFreq vocabulary and all-mpnet-base-v2 embeddings.
         | 
| 254 | 
            +
                
         | 
| 255 | 
            +
                Compatible with both hack tools and crossword backend services.
         | 
| 256 | 
            +
                Eliminates vocabulary redundancy by using single source for everything.
         | 
| 257 | 
            +
                """
         | 
| 258 | 
            +
                
         | 
| 259 | 
            +
                def __init__(self, cache_dir: Optional[str] = None, model_name: str = 'all-mpnet-base-v2', 
         | 
| 260 | 
            +
                             vocab_size_limit: Optional[int] = None):
         | 
| 261 | 
            +
                    """Initialize the unified thematic word generator.
         | 
| 262 | 
            +
                    
         | 
| 263 | 
            +
                    Args:
         | 
| 264 | 
            +
                        cache_dir: Directory to cache model and embeddings
         | 
| 265 | 
            +
                        model_name: Sentence transformer model to use
         | 
| 266 | 
            +
                        vocab_size_limit: Maximum vocabulary size (None for 100K default)
         | 
| 267 | 
            +
                    """
         | 
| 268 | 
            +
                    if cache_dir is None:
         | 
| 269 | 
            +
                        # Check environment variable for cache directory
         | 
| 270 | 
            +
                        cache_dir = os.getenv("CACHE_DIR")
         | 
| 271 | 
            +
                        if cache_dir is None:
         | 
| 272 | 
            +
                            cache_dir = os.path.join(os.path.dirname(__file__), 'model_cache')
         | 
| 273 | 
            +
                    
         | 
| 274 | 
            +
                    self.cache_dir = Path(cache_dir)
         | 
| 275 | 
            +
                    self.cache_dir.mkdir(parents=True, exist_ok=True)
         | 
| 276 | 
            +
                    
         | 
| 277 | 
            +
                    # Get model name from environment if not specified
         | 
| 278 | 
            +
                    self.model_name = os.getenv("THEMATIC_MODEL_NAME", model_name)
         | 
| 279 | 
            +
                    
         | 
| 280 | 
            +
                    # Get vocabulary size limit from environment if not specified
         | 
| 281 | 
            +
                    self.vocab_size_limit = (vocab_size_limit or 
         | 
| 282 | 
            +
                                            int(os.getenv("THEMATIC_VOCAB_SIZE_LIMIT", 
         | 
| 283 | 
            +
                                                         os.getenv("MAX_VOCABULARY_SIZE", "100000"))))
         | 
| 284 | 
            +
                    
         | 
| 285 | 
            +
                    # Core components
         | 
| 286 | 
            +
                    self.vocab_manager = VocabularyManager(str(self.cache_dir), self.vocab_size_limit)
         | 
| 287 | 
            +
                    self.model: Optional[SentenceTransformer] = None
         | 
| 288 | 
            +
                    
         | 
| 289 | 
            +
                    # Loaded data
         | 
| 290 | 
            +
                    self.vocabulary: List[str] = []
         | 
| 291 | 
            +
                    self.word_frequencies: Counter = Counter()
         | 
| 292 | 
            +
                    self.vocab_embeddings: Optional[np.ndarray] = None
         | 
| 293 | 
            +
                    self.frequency_tiers: Dict[str, str] = {}
         | 
| 294 | 
            +
                    self.tier_descriptions: Dict[str, str] = {}
         | 
| 295 | 
            +
                    
         | 
| 296 | 
            +
                    # Cache paths for embeddings
         | 
| 297 | 
            +
                    vocab_hash = f"{self.model_name.replace('/', '_')}_{self.vocab_size_limit}"
         | 
| 298 | 
            +
                    self.embeddings_cache_path = self.cache_dir / f"embeddings_{vocab_hash}.npy"
         | 
| 299 | 
            +
                    
         | 
| 300 | 
            +
                    self.is_initialized = False
         | 
| 301 | 
            +
                
         | 
| 302 | 
            +
                def initialize(self):
         | 
| 303 | 
            +
                    """Initialize the generator (synchronous version)."""
         | 
| 304 | 
            +
                    if self.is_initialized:
         | 
| 305 | 
            +
                        return
         | 
| 306 | 
            +
                        
         | 
| 307 | 
            +
                    start_time = time.time()
         | 
| 308 | 
            +
                    logger.info(f"🚀 Initializing Thematic Word Service...")
         | 
| 309 | 
            +
                    logger.info(f"📁 Cache directory: {self.cache_dir}")
         | 
| 310 | 
            +
                    logger.info(f"🤖 Model: {self.model_name}")
         | 
| 311 | 
            +
                    logger.info(f"📊 Vocabulary size limit: {self.vocab_size_limit:,}")
         | 
| 312 | 
            +
                    
         | 
| 313 | 
            +
                    # Check if cache directory exists and is accessible
         | 
| 314 | 
            +
                    if not self.cache_dir.exists():
         | 
| 315 | 
            +
                        logger.warning(f"⚠️ Cache directory does not exist, creating: {self.cache_dir}")
         | 
| 316 | 
            +
                        try:
         | 
| 317 | 
            +
                            self.cache_dir.mkdir(parents=True, exist_ok=True)
         | 
| 318 | 
            +
                        except Exception as e:
         | 
| 319 | 
            +
                            logger.error(f"❌ Failed to create cache directory: {e}")
         | 
| 320 | 
            +
                            raise
         | 
| 321 | 
            +
                    
         | 
| 322 | 
            +
                    # Load vocabulary and frequency data
         | 
| 323 | 
            +
                    vocab_start = time.time()
         | 
| 324 | 
            +
                    self.vocabulary, self.word_frequencies = self.vocab_manager.load_vocabulary()
         | 
| 325 | 
            +
                    vocab_time = time.time() - vocab_start
         | 
| 326 | 
            +
                    logger.info(f"✅ Vocabulary loaded in {vocab_time:.2f}s: {len(self.vocabulary):,} words")
         | 
| 327 | 
            +
                    
         | 
| 328 | 
            +
                    # Load or create frequency tiers
         | 
| 329 | 
            +
                    self.frequency_tiers = self._create_frequency_tiers()
         | 
| 330 | 
            +
                    
         | 
| 331 | 
            +
                    # Load model
         | 
| 332 | 
            +
                    logger.info(f"🤖 Loading embedding model: {self.model_name}")
         | 
| 333 | 
            +
                    model_start = time.time()
         | 
| 334 | 
            +
                    self.model = SentenceTransformer(
         | 
| 335 | 
            +
                        f'sentence-transformers/{self.model_name}',
         | 
| 336 | 
            +
                        cache_folder=str(self.cache_dir)
         | 
| 337 | 
            +
                    )
         | 
| 338 | 
            +
                    model_time = time.time() - model_start
         | 
| 339 | 
            +
                    logger.info(f"✅ Model loaded in {model_time:.2f}s")
         | 
| 340 | 
            +
                    
         | 
| 341 | 
            +
                    # Load or create embeddings
         | 
| 342 | 
            +
                    self.vocab_embeddings = self._load_or_create_embeddings()
         | 
| 343 | 
            +
                    
         | 
| 344 | 
            +
                    self.is_initialized = True
         | 
| 345 | 
            +
                    total_time = time.time() - start_time
         | 
| 346 | 
            +
                    logger.info(f"🎉 Unified generator initialized in {total_time:.2f}s")
         | 
| 347 | 
            +
                    logger.info(f"📊 Vocabulary: {len(self.vocabulary):,} words")
         | 
| 348 | 
            +
                    logger.info(f"📈 Frequency data: {len(self.word_frequencies):,} words")
         | 
| 349 | 
            +
                
         | 
| 350 | 
            +
                async def initialize_async(self):
         | 
| 351 | 
            +
                    """Initialize the generator (async version for backend compatibility)."""
         | 
| 352 | 
            +
                    return self.initialize()  # For now, same as sync version
         | 
| 353 | 
            +
                
         | 
| 354 | 
            +
                def _load_or_create_embeddings(self) -> np.ndarray:
         | 
| 355 | 
            +
                    """Load embeddings from cache or create them."""
         | 
| 356 | 
            +
                    # Try loading from cache
         | 
| 357 | 
            +
                    if self.embeddings_cache_path.exists():
         | 
| 358 | 
            +
                        try:
         | 
| 359 | 
            +
                            logger.info(f"📦 Loading embeddings from cache: {self.embeddings_cache_path}")
         | 
| 360 | 
            +
                            
         | 
| 361 | 
            +
                            # Validate cache file is readable
         | 
| 362 | 
            +
                            if not os.access(self.embeddings_cache_path, os.R_OK):
         | 
| 363 | 
            +
                                logger.warning(f"⚠️ Embeddings cache file not readable: {self.embeddings_cache_path}")
         | 
| 364 | 
            +
                                return self._create_embeddings_from_scratch()
         | 
| 365 | 
            +
                            
         | 
| 366 | 
            +
                            embeddings = np.load(self.embeddings_cache_path)
         | 
| 367 | 
            +
                            
         | 
| 368 | 
            +
                            # Validate embeddings shape matches vocabulary size
         | 
| 369 | 
            +
                            expected_shape = (len(self.vocabulary), None)  # Second dimension varies by model
         | 
| 370 | 
            +
                            if embeddings.shape[0] != len(self.vocabulary):
         | 
| 371 | 
            +
                                logger.warning(f"⚠️ Embeddings shape mismatch: cache={embeddings.shape[0]}, vocab={len(self.vocabulary)}")
         | 
| 372 | 
            +
                                logger.warning("🔄 Vocabulary size changed, recreating embeddings...")
         | 
| 373 | 
            +
                                return self._create_embeddings_from_scratch()
         | 
| 374 | 
            +
                            
         | 
| 375 | 
            +
                            logger.info(f"✅ Loaded embeddings from cache: {embeddings.shape}")
         | 
| 376 | 
            +
                            return embeddings
         | 
| 377 | 
            +
                        except Exception as e:
         | 
| 378 | 
            +
                            logger.warning(f"⚠️ Embeddings cache loading failed: {e}")
         | 
| 379 | 
            +
                            return self._create_embeddings_from_scratch()
         | 
| 380 | 
            +
                    else:
         | 
| 381 | 
            +
                        logger.info(f"📂 Embeddings cache not found: {self.embeddings_cache_path}")
         | 
| 382 | 
            +
                        return self._create_embeddings_from_scratch()
         | 
| 383 | 
            +
             | 
| 384 | 
            +
                def _create_embeddings_from_scratch(self) -> np.ndarray:
         | 
| 385 | 
            +
                    
         | 
| 386 | 
            +
                    # Create embeddings
         | 
| 387 | 
            +
                    logger.info("🔄 Creating embeddings for vocabulary...")
         | 
| 388 | 
            +
                    start_time = time.time()
         | 
| 389 | 
            +
                    
         | 
| 390 | 
            +
                    # Create embeddings in batches for memory efficiency
         | 
| 391 | 
            +
                    batch_size = 512
         | 
| 392 | 
            +
                    all_embeddings = []
         | 
| 393 | 
            +
                    
         | 
| 394 | 
            +
                    for i in range(0, len(self.vocabulary), batch_size):
         | 
| 395 | 
            +
                        batch_words = self.vocabulary[i:i + batch_size]
         | 
| 396 | 
            +
                        batch_embeddings = self.model.encode(
         | 
| 397 | 
            +
                            batch_words,
         | 
| 398 | 
            +
                            convert_to_tensor=False,
         | 
| 399 | 
            +
                            show_progress_bar=i == 0  # Only show progress for first batch
         | 
| 400 | 
            +
                        )
         | 
| 401 | 
            +
                        all_embeddings.append(batch_embeddings)
         | 
| 402 | 
            +
                        
         | 
| 403 | 
            +
                        if i % (batch_size * 10) == 0:
         | 
| 404 | 
            +
                            logger.info(f"📊 Embeddings progress: {i:,}/{len(self.vocabulary):,}")
         | 
| 405 | 
            +
                    
         | 
| 406 | 
            +
                    embeddings = np.vstack(all_embeddings)
         | 
| 407 | 
            +
                    embedding_time = time.time() - start_time
         | 
| 408 | 
            +
                    logger.info(f"✅ Created embeddings in {embedding_time:.2f}s: {embeddings.shape}")
         | 
| 409 | 
            +
                    
         | 
| 410 | 
            +
                    # Save to cache
         | 
| 411 | 
            +
                    try:
         | 
| 412 | 
            +
                        np.save(self.embeddings_cache_path, embeddings)
         | 
| 413 | 
            +
                        logger.info("💾 Embeddings cached successfully")
         | 
| 414 | 
            +
                    except Exception as e:
         | 
| 415 | 
            +
                        logger.warning(f"⚠️ Embeddings cache saving failed: {e}")
         | 
| 416 | 
            +
                    
         | 
| 417 | 
            +
                    return embeddings
         | 
| 418 | 
            +
                
         | 
| 419 | 
            +
                def _create_frequency_tiers(self) -> Dict[str, str]:
         | 
| 420 | 
            +
                    """Create 10-tier frequency classification system."""
         | 
| 421 | 
            +
                    if not self.word_frequencies:
         | 
| 422 | 
            +
                        return {}
         | 
| 423 | 
            +
                    
         | 
| 424 | 
            +
                    logger.info("📊 Creating frequency tiers...")
         | 
| 425 | 
            +
                    
         | 
| 426 | 
            +
                    tiers = {}
         | 
| 427 | 
            +
                    
         | 
| 428 | 
            +
                    # Calculate percentile-based thresholds for even distribution
         | 
| 429 | 
            +
                    all_counts = list(self.word_frequencies.values())
         | 
| 430 | 
            +
                    all_counts.sort(reverse=True)
         | 
| 431 | 
            +
                    
         | 
| 432 | 
            +
                    # Define 10 tiers with percentile-based thresholds
         | 
| 433 | 
            +
                    tier_definitions = [
         | 
| 434 | 
            +
                        ("tier_1_ultra_common", 0.999, "Ultra Common (Top 0.1%)"),
         | 
| 435 | 
            +
                        ("tier_2_extremely_common", 0.995, "Extremely Common (Top 0.5%)"), 
         | 
| 436 | 
            +
                        ("tier_3_very_common", 0.99, "Very Common (Top 1%)"),
         | 
| 437 | 
            +
                        ("tier_4_highly_common", 0.97, "Highly Common (Top 3%)"),
         | 
| 438 | 
            +
                        ("tier_5_common", 0.92, "Common (Top 8%)"),
         | 
| 439 | 
            +
                        ("tier_6_moderately_common", 0.85, "Moderately Common (Top 15%)"),
         | 
| 440 | 
            +
                        ("tier_7_somewhat_uncommon", 0.70, "Somewhat Uncommon (Top 30%)"),
         | 
| 441 | 
            +
                        ("tier_8_uncommon", 0.50, "Uncommon (Top 50%)"),
         | 
| 442 | 
            +
                        ("tier_9_rare", 0.25, "Rare (Top 75%)"),
         | 
| 443 | 
            +
                        ("tier_10_very_rare", 0.0, "Very Rare (Bottom 25%)")
         | 
| 444 | 
            +
                    ]
         | 
| 445 | 
            +
                    
         | 
| 446 | 
            +
                    # Calculate actual thresholds
         | 
| 447 | 
            +
                    thresholds = []
         | 
| 448 | 
            +
                    for tier_name, percentile, description in tier_definitions:
         | 
| 449 | 
            +
                        if percentile > 0:
         | 
| 450 | 
            +
                            idx = int((1 - percentile) * len(all_counts))
         | 
| 451 | 
            +
                            threshold = all_counts[min(idx, len(all_counts) - 1)]
         | 
| 452 | 
            +
                        else:
         | 
| 453 | 
            +
                            threshold = 0
         | 
| 454 | 
            +
                        thresholds.append((tier_name, threshold, description))
         | 
| 455 | 
            +
                    
         | 
| 456 | 
            +
                    # Store descriptions
         | 
| 457 | 
            +
                    self.tier_descriptions = {name: desc for name, _, desc in thresholds}
         | 
| 458 | 
            +
                    
         | 
| 459 | 
            +
                    # Assign tiers
         | 
| 460 | 
            +
                    for word, count in self.word_frequencies.items():
         | 
| 461 | 
            +
                        assigned = False
         | 
| 462 | 
            +
                        for tier_name, threshold, description in thresholds:
         | 
| 463 | 
            +
                            if count >= threshold:
         | 
| 464 | 
            +
                                tiers[word] = tier_name
         | 
| 465 | 
            +
                                assigned = True
         | 
| 466 | 
            +
                                break
         | 
| 467 | 
            +
                        
         | 
| 468 | 
            +
                        if not assigned:
         | 
| 469 | 
            +
                            tiers[word] = "tier_10_very_rare"
         | 
| 470 | 
            +
                    
         | 
| 471 | 
            +
                    # Words not in frequency data are very rare
         | 
| 472 | 
            +
                    for word in self.vocabulary:
         | 
| 473 | 
            +
                        if word not in tiers:
         | 
| 474 | 
            +
                            tiers[word] = "tier_10_very_rare"
         | 
| 475 | 
            +
                    
         | 
| 476 | 
            +
                    # Log tier distribution
         | 
| 477 | 
            +
                    tier_counts = Counter(tiers.values())
         | 
| 478 | 
            +
                    logger.info(f"✅ Created frequency tiers:")
         | 
| 479 | 
            +
                    for tier_name, count in sorted(tier_counts.items()):
         | 
| 480 | 
            +
                        desc = self.tier_descriptions.get(tier_name, tier_name)
         | 
| 481 | 
            +
                        logger.info(f"   {desc}: {count:,} words")
         | 
| 482 | 
            +
                    
         | 
| 483 | 
            +
                    return tiers
         | 
| 484 | 
            +
                
         | 
| 485 | 
            +
                def generate_thematic_words(self, 
         | 
| 486 | 
            +
                                          inputs, 
         | 
| 487 | 
            +
                                          num_words: int = 100, 
         | 
| 488 | 
            +
                                          min_similarity: float = 0.3,
         | 
| 489 | 
            +
                                          multi_theme: bool = False,
         | 
| 490 | 
            +
                                          difficulty_tier: Optional[str] = None) -> List[Tuple[str, float, str]]:
         | 
| 491 | 
            +
                    """Generate thematically related words from input seeds.
         | 
| 492 | 
            +
                    
         | 
| 493 | 
            +
                    Args:
         | 
| 494 | 
            +
                        inputs: Single string, or list of words/sentences as theme seeds
         | 
| 495 | 
            +
                        num_words: Number of words to return
         | 
| 496 | 
            +
                        min_similarity: Minimum similarity threshold
         | 
| 497 | 
            +
                        multi_theme: Whether to detect and use multiple themes
         | 
| 498 | 
            +
                        difficulty_tier: Specific tier to filter by (e.g., "tier_5_common")
         | 
| 499 | 
            +
                        
         | 
| 500 | 
            +
                    Returns:
         | 
| 501 | 
            +
                        List of (word, similarity_score, frequency_tier) tuples
         | 
| 502 | 
            +
                    """
         | 
| 503 | 
            +
                    if not self.is_initialized:
         | 
| 504 | 
            +
                        self.initialize()
         | 
| 505 | 
            +
                    
         | 
| 506 | 
            +
                    logger.info(f"🎯 Generating {num_words} thematic words")
         | 
| 507 | 
            +
                    
         | 
| 508 | 
            +
                    # Handle single string input (convert to list for compatibility)
         | 
| 509 | 
            +
                    if isinstance(inputs, str):
         | 
| 510 | 
            +
                        inputs = [inputs]
         | 
| 511 | 
            +
                    
         | 
| 512 | 
            +
                    if not inputs:
         | 
| 513 | 
            +
                        return []
         | 
| 514 | 
            +
                    
         | 
| 515 | 
            +
                    # Clean inputs
         | 
| 516 | 
            +
                    clean_inputs = [inp.strip().lower() for inp in inputs if inp.strip()]
         | 
| 517 | 
            +
                    if not clean_inputs:
         | 
| 518 | 
            +
                        return []
         | 
| 519 | 
            +
                    
         | 
| 520 | 
            +
                    logger.info(f"📝 Input themes: {clean_inputs}")
         | 
| 521 | 
            +
                    if difficulty_tier:
         | 
| 522 | 
            +
                        logger.info(f"📊 Filtering to tier: {self.tier_descriptions.get(difficulty_tier, difficulty_tier)}")
         | 
| 523 | 
            +
                    
         | 
| 524 | 
            +
                    # Get theme vector(s) using original logic
         | 
| 525 | 
            +
                    # Auto-enable multi-theme for 3+ inputs (matching original behavior)
         | 
| 526 | 
            +
                    auto_multi_theme = len(clean_inputs) > 2
         | 
| 527 | 
            +
                    final_multi_theme = multi_theme or auto_multi_theme
         | 
| 528 | 
            +
                    
         | 
| 529 | 
            +
                    logger.info(f"🔍 Multi-theme detection: {final_multi_theme} (auto: {auto_multi_theme}, manual: {multi_theme})")
         | 
| 530 | 
            +
                    
         | 
| 531 | 
            +
                    if final_multi_theme:
         | 
| 532 | 
            +
                        theme_vectors = self._detect_multiple_themes(clean_inputs)
         | 
| 533 | 
            +
                        logger.info(f"📊 Detected {len(theme_vectors)} themes")
         | 
| 534 | 
            +
                    else:
         | 
| 535 | 
            +
                        theme_vectors = [self._compute_theme_vector(clean_inputs)]
         | 
| 536 | 
            +
                        logger.info("📊 Using single theme vector")
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                    # Collect similarities from all themes
         | 
| 539 | 
            +
                    all_similarities = np.zeros(len(self.vocabulary))
         | 
| 540 | 
            +
                    
         | 
| 541 | 
            +
                    for theme_vector in theme_vectors:
         | 
| 542 | 
            +
                        # Compute similarities with vocabulary
         | 
| 543 | 
            +
                        similarities = cosine_similarity(theme_vector, self.vocab_embeddings)[0]
         | 
| 544 | 
            +
                        all_similarities += similarities / len(theme_vectors)  # Average across themes
         | 
| 545 | 
            +
                    
         | 
| 546 | 
            +
                    logger.info("✅ Computed semantic similarities")
         | 
| 547 | 
            +
                    
         | 
| 548 | 
            +
                    # Get top candidates sorted by similarity
         | 
| 549 | 
            +
                    # np.argsort() returns indices that would sort array in ascending order
         | 
| 550 | 
            +
                    # [::-1] reverses to get descending order (highest similarity first)
         | 
| 551 | 
            +
                    # top_indices[0] contains the vocabulary index of the word most similar to theme vector
         | 
| 552 | 
            +
                    top_indices = np.argsort(all_similarities)[::-1]
         | 
| 553 | 
            +
                    
         | 
| 554 | 
            +
                    # Filter and format results
         | 
| 555 | 
            +
                    results = []
         | 
| 556 | 
            +
                    input_words_set = set(clean_inputs)
         | 
| 557 | 
            +
                    logger.info(f"{clean_inputs=}")
         | 
| 558 | 
            +
                    
         | 
| 559 | 
            +
                    # Traverse top_indices from beginning to get most similar words first
         | 
| 560 | 
            +
                    # Each idx is used to lookup the actual word in self.vocabulary[idx]
         | 
| 561 | 
            +
                    for idx in top_indices:
         | 
| 562 | 
            +
                        if len(results) >= num_words * 3:  # Get extra candidates for filtering
         | 
| 563 | 
            +
                            break
         | 
| 564 | 
            +
                            
         | 
| 565 | 
            +
                        similarity_score = all_similarities[idx]
         | 
| 566 | 
            +
                        word = self.vocabulary[idx]  # Get actual word using vocabulary index
         | 
| 567 | 
            +
                        
         | 
| 568 | 
            +
                        # Apply filters - use early termination since top_indices is sorted by similarity
         | 
| 569 | 
            +
                        if similarity_score < min_similarity:
         | 
| 570 | 
            +
                            break  # All remaining words will also be below threshold since array is sorted
         | 
| 571 | 
            +
                            
         | 
| 572 | 
            +
                        # Skip input words themselves
         | 
| 573 | 
            +
                        if word.lower() in input_words_set:
         | 
| 574 | 
            +
                            continue
         | 
| 575 | 
            +
                        
         | 
| 576 | 
            +
                        # Get pre-assigned tier for this word
         | 
| 577 | 
            +
                        # Tiers are computed during initialization using WordFreq data
         | 
| 578 | 
            +
                        # Based on percentile thresholds: tier_1 (top 0.1%), tier_5 (top 8%), etc.
         | 
| 579 | 
            +
                        word_tier = self.frequency_tiers.get(word, "tier_10_very_rare")
         | 
| 580 | 
            +
                        
         | 
| 581 | 
            +
                        # Filter by difficulty tier if specified
         | 
| 582 | 
            +
                        # If difficulty_tier is specified, only include words from that exact tier
         | 
| 583 | 
            +
                        # If no difficulty_tier specified, include all words (subject to similarity threshold)
         | 
| 584 | 
            +
                        if difficulty_tier and word_tier != difficulty_tier:
         | 
| 585 | 
            +
                            continue
         | 
| 586 | 
            +
                        
         | 
| 587 | 
            +
                        results.append((word, similarity_score, word_tier))
         | 
| 588 | 
            +
                    
         | 
| 589 | 
            +
                    # Sort by similarity and return top results
         | 
| 590 | 
            +
                    results.sort(key=lambda x: x[1], reverse=True)
         | 
| 591 | 
            +
                    final_results = results[:num_words]
         | 
| 592 | 
            +
                    
         | 
| 593 | 
            +
                    logger.info(f"✅ Generated {len(final_results)} thematic words")
         | 
| 594 | 
            +
                    return final_results
         | 
| 595 | 
            +
                
         | 
| 596 | 
            +
                def _compute_theme_vector(self, inputs: List[str]) -> np.ndarray:
         | 
| 597 | 
            +
                    """Compute semantic centroid from input words/sentences."""
         | 
| 598 | 
            +
                    logger.info(f"🎯 Computing theme vector for {len(inputs)} inputs")
         | 
| 599 | 
            +
                    
         | 
| 600 | 
            +
                    # Encode all inputs
         | 
| 601 | 
            +
                    input_embeddings = self.model.encode(inputs, convert_to_tensor=False, show_progress_bar=False)
         | 
| 602 | 
            +
                    logger.info(f"✅ Encoded {len(inputs)} inputs")
         | 
| 603 | 
            +
                    
         | 
| 604 | 
            +
                    # Simple approach: average all input embeddings
         | 
| 605 | 
            +
                    theme_vector = np.mean(input_embeddings, axis=0)
         | 
| 606 | 
            +
                    
         | 
| 607 | 
            +
                    return theme_vector.reshape(1, -1)
         | 
| 608 | 
            +
                
         | 
| 609 | 
            +
                def _detect_multiple_themes(self, inputs: List[str], max_themes: int = 3) -> List[np.ndarray]:
         | 
| 610 | 
            +
                    """Detect multiple themes using clustering."""
         | 
| 611 | 
            +
                    if len(inputs) < 2:
         | 
| 612 | 
            +
                        return [self._compute_theme_vector(inputs)]
         | 
| 613 | 
            +
                    
         | 
| 614 | 
            +
                    logger.info(f"🔍 Detecting multiple themes from {len(inputs)} inputs")
         | 
| 615 | 
            +
                    
         | 
| 616 | 
            +
                    # Encode inputs
         | 
| 617 | 
            +
                    input_embeddings = self.model.encode(inputs, convert_to_tensor=False, show_progress_bar=False)
         | 
| 618 | 
            +
                    logger.info("✅ Encoded inputs for clustering")
         | 
| 619 | 
            +
                    
         | 
| 620 | 
            +
                    # Determine optimal number of clusters
         | 
| 621 | 
            +
                    n_clusters = min(max_themes, len(inputs), 3)
         | 
| 622 | 
            +
                    logger.info(f"📊 Using {n_clusters} clusters for theme detection")
         | 
| 623 | 
            +
                    
         | 
| 624 | 
            +
                    if n_clusters == 1:
         | 
| 625 | 
            +
                        return [np.mean(input_embeddings, axis=0).reshape(1, -1)]
         | 
| 626 | 
            +
                    
         | 
| 627 | 
            +
                    # Perform clustering
         | 
| 628 | 
            +
                    kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
         | 
| 629 | 
            +
                    kmeans.fit(input_embeddings)
         | 
| 630 | 
            +
                    
         | 
| 631 | 
            +
                    logger.info(f"✅ Clustered inputs into {n_clusters} themes")
         | 
| 632 | 
            +
                    
         | 
| 633 | 
            +
                    # Return cluster centers as theme vectors
         | 
| 634 | 
            +
                    return [center.reshape(1, -1) for center in kmeans.cluster_centers_]
         | 
| 635 | 
            +
                
         | 
| 636 | 
            +
                def get_tier_words(self, tier: str, limit: int = 1000) -> List[str]:
         | 
| 637 | 
            +
                    """Get all words from a specific frequency tier.
         | 
| 638 | 
            +
                    
         | 
| 639 | 
            +
                    Args:
         | 
| 640 | 
            +
                        tier: Frequency tier name (e.g., "tier_5_common")
         | 
| 641 | 
            +
                        limit: Maximum number of words to return
         | 
| 642 | 
            +
                        
         | 
| 643 | 
            +
                    Returns:
         | 
| 644 | 
            +
                        List of words in the specified tier
         | 
| 645 | 
            +
                    """
         | 
| 646 | 
            +
                    if not self.is_initialized:
         | 
| 647 | 
            +
                        self.initialize()
         | 
| 648 | 
            +
                        
         | 
| 649 | 
            +
                    tier_words = [word for word, word_tier in self.frequency_tiers.items() 
         | 
| 650 | 
            +
                                 if word_tier == tier]
         | 
| 651 | 
            +
                    
         | 
| 652 | 
            +
                    return tier_words[:limit]
         | 
| 653 | 
            +
                
         | 
| 654 | 
            +
                def get_word_info(self, word: str) -> Dict[str, Any]:
         | 
| 655 | 
            +
                    """Get comprehensive information about a word.
         | 
| 656 | 
            +
                    
         | 
| 657 | 
            +
                    Args:
         | 
| 658 | 
            +
                        word: Word to get information for
         | 
| 659 | 
            +
                        
         | 
| 660 | 
            +
                    Returns:
         | 
| 661 | 
            +
                        Dictionary with word info including frequency, tier, etc.
         | 
| 662 | 
            +
                    """
         | 
| 663 | 
            +
                    if not self.is_initialized:
         | 
| 664 | 
            +
                        self.initialize()
         | 
| 665 | 
            +
                        
         | 
| 666 | 
            +
                    word_lower = word.lower()
         | 
| 667 | 
            +
                    
         | 
| 668 | 
            +
                    info = {
         | 
| 669 | 
            +
                        'word': word,
         | 
| 670 | 
            +
                        'in_vocabulary': word_lower in self.vocabulary,
         | 
| 671 | 
            +
                        'frequency': self.word_frequencies.get(word_lower, 0),
         | 
| 672 | 
            +
                        'tier': self.frequency_tiers.get(word_lower, "tier_10_very_rare"),
         | 
| 673 | 
            +
                        'tier_description': self.tier_descriptions.get(
         | 
| 674 | 
            +
                            self.frequency_tiers.get(word_lower, "tier_10_very_rare"),
         | 
| 675 | 
            +
                            "Unknown"
         | 
| 676 | 
            +
                        )
         | 
| 677 | 
            +
                    }
         | 
| 678 | 
            +
                    
         | 
| 679 | 
            +
                    return info
         | 
| 680 | 
            +
                
         | 
| 681 | 
            +
                # Backend compatibility methods
         | 
| 682 | 
            +
                async def find_similar_words(self, topic: str, difficulty: str = "medium", max_words: int = 15) -> List[Dict[str, Any]]:
         | 
| 683 | 
            +
                    """Backend-compatible method for finding similar words.
         | 
| 684 | 
            +
                    
         | 
| 685 | 
            +
                    Returns list of word dictionaries compatible with crossword_generator.py
         | 
| 686 | 
            +
                    Expected format: [{"word": str, "clue": str}, ...]
         | 
| 687 | 
            +
                    """
         | 
| 688 | 
            +
                    # Map difficulty to appropriate tier filtering
         | 
| 689 | 
            +
                    difficulty_tier_map = {
         | 
| 690 | 
            +
                        "easy": [ "tier_2_extremely_common", "tier_3_very_common", "tier_4_highly_common"],
         | 
| 691 | 
            +
                        "medium": ["tier_4_highly_common", "tier_5_common", "tier_6_moderately_common", "tier_7_somewhat_uncommon"],
         | 
| 692 | 
            +
                        "hard": ["tier_7_somewhat_uncommon", "tier_8_uncommon", "tier_9_rare"]
         | 
| 693 | 
            +
                    }
         | 
| 694 | 
            +
                    
         | 
| 695 | 
            +
                    allowed_tiers = difficulty_tier_map.get(difficulty, difficulty_tier_map["medium"])
         | 
| 696 | 
            +
                    
         | 
| 697 | 
            +
                    # Get thematic words
         | 
| 698 | 
            +
                    all_results = self.generate_thematic_words(
         | 
| 699 | 
            +
                        topic, 
         | 
| 700 | 
            +
                        num_words=150,  # Get extra for filtering
         | 
| 701 | 
            +
                        min_similarity=0.3
         | 
| 702 | 
            +
                    )
         | 
| 703 | 
            +
                    
         | 
| 704 | 
            +
                    # Filter by difficulty and format for backend
         | 
| 705 | 
            +
                    backend_words = []
         | 
| 706 | 
            +
                    for word, similarity, tier in all_results:
         | 
| 707 | 
            +
                        # Check difficulty criteria
         | 
| 708 | 
            +
                        if not self._matches_backend_difficulty(word, difficulty):
         | 
| 709 | 
            +
                            continue
         | 
| 710 | 
            +
                            
         | 
| 711 | 
            +
                        # Optional tier filtering for more precise difficulty control
         | 
| 712 | 
            +
                        # (Comment out if tier filtering is too restrictive)
         | 
| 713 | 
            +
                        # if tier not in allowed_tiers:
         | 
| 714 | 
            +
                        #     continue
         | 
| 715 | 
            +
                        
         | 
| 716 | 
            +
                        # Format for backend compatibility
         | 
| 717 | 
            +
                        backend_word = {
         | 
| 718 | 
            +
                            "word": word.upper(),  # Backend expects uppercase
         | 
| 719 | 
            +
                            "clue": self._generate_simple_clue(word, topic),
         | 
| 720 | 
            +
                            "similarity": similarity,
         | 
| 721 | 
            +
                            "tier": tier
         | 
| 722 | 
            +
                        }
         | 
| 723 | 
            +
                        
         | 
| 724 | 
            +
                        backend_words.append(backend_word)
         | 
| 725 | 
            +
                        
         | 
| 726 | 
            +
                        if len(backend_words) >= max_words:
         | 
| 727 | 
            +
                            break
         | 
| 728 | 
            +
                    
         | 
| 729 | 
            +
                    logger.info(f"🎯 Generated {len(backend_words)} words for topic '{topic}' (difficulty: {difficulty})")
         | 
| 730 | 
            +
                    return backend_words
         | 
| 731 | 
            +
                
         | 
| 732 | 
            +
                def _matches_backend_difficulty(self, word: str, difficulty: str) -> bool:
         | 
| 733 | 
            +
                    """Check if word matches backend difficulty criteria."""
         | 
| 734 | 
            +
                    difficulty_map = {
         | 
| 735 | 
            +
                        "easy": {"min_len": 3, "max_len": 8},
         | 
| 736 | 
            +
                        "medium": {"min_len": 4, "max_len": 10},
         | 
| 737 | 
            +
                        "hard": {"min_len": 5, "max_len": 15}
         | 
| 738 | 
            +
                    }
         | 
| 739 | 
            +
                    
         | 
| 740 | 
            +
                    criteria = difficulty_map.get(difficulty, difficulty_map["medium"])
         | 
| 741 | 
            +
                    return criteria["min_len"] <= len(word) <= criteria["max_len"]
         | 
| 742 | 
            +
                
         | 
| 743 | 
            +
                def _generate_simple_clue(self, word: str, topic: str) -> str:
         | 
| 744 | 
            +
                    """Generate a simple clue for the word (backend compatibility)."""
         | 
| 745 | 
            +
                    # Basic clue templates matching backend expectations
         | 
| 746 | 
            +
                    word_lower = word.lower()
         | 
| 747 | 
            +
                    topic_lower = topic.lower()
         | 
| 748 | 
            +
                    
         | 
| 749 | 
            +
                    # Topic-specific clue templates
         | 
| 750 | 
            +
                    if "animal" in topic_lower:
         | 
| 751 | 
            +
                        return f"{word_lower} (animal)"
         | 
| 752 | 
            +
                    elif "tech" in topic_lower or "computer" in topic_lower:
         | 
| 753 | 
            +
                        return f"{word_lower} (technology)"
         | 
| 754 | 
            +
                    elif "science" in topic_lower:
         | 
| 755 | 
            +
                        return f"{word_lower} (science)"
         | 
| 756 | 
            +
                    elif "geo" in topic_lower or "place" in topic_lower:
         | 
| 757 | 
            +
                        return f"{word_lower} (geography)"
         | 
| 758 | 
            +
                    elif "food" in topic_lower:
         | 
| 759 | 
            +
                        return f"{word_lower} (food)"
         | 
| 760 | 
            +
                    else:
         | 
| 761 | 
            +
                        return f"{word_lower} (related to {topic_lower})"
         | 
| 762 | 
            +
                
         | 
| 763 | 
            +
                def get_vocabulary_size(self) -> int:
         | 
| 764 | 
            +
                    """Get the size of the loaded vocabulary."""
         | 
| 765 | 
            +
                    return len(self.vocabulary)
         | 
| 766 | 
            +
                
         | 
| 767 | 
            +
                def get_tier_distribution(self) -> Dict[str, int]:
         | 
| 768 | 
            +
                    """Get distribution of words across frequency tiers."""
         | 
| 769 | 
            +
                    if not self.frequency_tiers:
         | 
| 770 | 
            +
                        return {}
         | 
| 771 | 
            +
                        
         | 
| 772 | 
            +
                    tier_counts = Counter(self.frequency_tiers.values())
         | 
| 773 | 
            +
                    return dict(tier_counts)
         | 
| 774 | 
            +
             | 
| 775 | 
            +
                def get_cache_status(self) -> Dict[str, Any]:
         | 
| 776 | 
            +
                    """Get detailed cache status information."""
         | 
| 777 | 
            +
                    vocab_exists = self.vocab_manager.vocab_cache_path.exists()
         | 
| 778 | 
            +
                    freq_exists = self.vocab_manager.frequency_cache_path.exists()
         | 
| 779 | 
            +
                    embeddings_exists = self.embeddings_cache_path.exists()
         | 
| 780 | 
            +
                    
         | 
| 781 | 
            +
                    status = {
         | 
| 782 | 
            +
                        "cache_directory": str(self.cache_dir),
         | 
| 783 | 
            +
                        "vocabulary_cache": {
         | 
| 784 | 
            +
                            "path": str(self.vocab_manager.vocab_cache_path),
         | 
| 785 | 
            +
                            "exists": vocab_exists,
         | 
| 786 | 
            +
                            "readable": vocab_exists and os.access(self.vocab_manager.vocab_cache_path, os.R_OK)
         | 
| 787 | 
            +
                        },
         | 
| 788 | 
            +
                        "frequency_cache": {
         | 
| 789 | 
            +
                            "path": str(self.vocab_manager.frequency_cache_path),
         | 
| 790 | 
            +
                            "exists": freq_exists,
         | 
| 791 | 
            +
                            "readable": freq_exists and os.access(self.vocab_manager.frequency_cache_path, os.R_OK)
         | 
| 792 | 
            +
                        },
         | 
| 793 | 
            +
                        "embeddings_cache": {
         | 
| 794 | 
            +
                            "path": str(self.embeddings_cache_path),
         | 
| 795 | 
            +
                            "exists": embeddings_exists,
         | 
| 796 | 
            +
                            "readable": embeddings_exists and os.access(self.embeddings_cache_path, os.R_OK)
         | 
| 797 | 
            +
                        },
         | 
| 798 | 
            +
                        "complete": vocab_exists and freq_exists and embeddings_exists
         | 
| 799 | 
            +
                    }
         | 
| 800 | 
            +
                    
         | 
| 801 | 
            +
                    # Add size information if files exist
         | 
| 802 | 
            +
                    for cache_type in ["vocabulary_cache", "frequency_cache", "embeddings_cache"]:
         | 
| 803 | 
            +
                        cache_info = status[cache_type]
         | 
| 804 | 
            +
                        if cache_info["exists"]:
         | 
| 805 | 
            +
                            try:
         | 
| 806 | 
            +
                                file_path = Path(cache_info["path"])
         | 
| 807 | 
            +
                                cache_info["size_bytes"] = file_path.stat().st_size
         | 
| 808 | 
            +
                                cache_info["size_mb"] = round(cache_info["size_bytes"] / (1024 * 1024), 2)
         | 
| 809 | 
            +
                            except Exception as e:
         | 
| 810 | 
            +
                                cache_info["size_error"] = str(e)
         | 
| 811 | 
            +
                    
         | 
| 812 | 
            +
                    return status
         | 
| 813 | 
            +
             | 
| 814 | 
            +
                async def find_words_for_crossword(self, topics: List[str], difficulty: str, requested_words: int = 10, custom_sentence: str = None, multi_theme: bool = True) -> List[Dict[str, Any]]:
         | 
| 815 | 
            +
                    """
         | 
| 816 | 
            +
                    Crossword-specific word finding method with 50% overgeneration and clue quality filtering.
         | 
| 817 | 
            +
                    
         | 
| 818 | 
            +
                    Args:
         | 
| 819 | 
            +
                        topics: List of topic strings
         | 
| 820 | 
            +
                        difficulty: "easy", "medium", or "hard"
         | 
| 821 | 
            +
                        requested_words: Number of words requested by frontend
         | 
| 822 | 
            +
                        custom_sentence: Optional custom sentence to influence word selection
         | 
| 823 | 
            +
                        multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
         | 
| 824 | 
            +
                        
         | 
| 825 | 
            +
                    Returns:
         | 
| 826 | 
            +
                        List of word dictionaries: [{"word": str, "clue": str, "similarity": float, "source": "thematic", "tier": str}]
         | 
| 827 | 
            +
                    """
         | 
| 828 | 
            +
                    if not self.is_initialized:
         | 
| 829 | 
            +
                        await self.initialize_async()
         | 
| 830 | 
            +
                    
         | 
| 831 | 
            +
                    sentence_info = f", custom sentence: '{custom_sentence}'" if custom_sentence else ""
         | 
| 832 | 
            +
                    theme_mode = "multi-theme" if multi_theme else "single-theme"
         | 
| 833 | 
            +
                    
         | 
| 834 | 
            +
                    # Calculate generation target (3x more for quality filtering - need large pool for clue generation)
         | 
| 835 | 
            +
                    generation_target = int(requested_words * 3)
         | 
| 836 | 
            +
                    logger.info(f"🎯 Finding words for crossword - topics: {topics}, difficulty: {difficulty}{sentence_info}, mode: {theme_mode}")
         | 
| 837 | 
            +
                    logger.info(f"📊 Generating {generation_target} candidates to select best {requested_words} words after clue filtering")
         | 
| 838 | 
            +
                    
         | 
| 839 | 
            +
                    # Map difficulty to tier preferences
         | 
| 840 | 
            +
                    difficulty_tier_map = {
         | 
| 841 | 
            +
                        "easy": ["tier_2_extremely_common", "tier_3_very_common", "tier_4_highly_common"],
         | 
| 842 | 
            +
                        "medium": ["tier_4_highly_common", "tier_5_common", "tier_6_moderately_common", "tier_7_somewhat_uncommon"],
         | 
| 843 | 
            +
                        "hard": ["tier_7_somewhat_uncommon", "tier_8_uncommon", "tier_9_rare"]
         | 
| 844 | 
            +
                    }
         | 
| 845 | 
            +
                    
         | 
| 846 | 
            +
                    # Map difficulty to similarity thresholds
         | 
| 847 | 
            +
                    difficulty_similarity_map = {
         | 
| 848 | 
            +
                        "easy": 0.4,
         | 
| 849 | 
            +
                        "medium": 0.3,
         | 
| 850 | 
            +
                        "hard": 0.25
         | 
| 851 | 
            +
                    }
         | 
| 852 | 
            +
                    
         | 
| 853 | 
            +
                    preferred_tiers = difficulty_tier_map.get(difficulty, difficulty_tier_map["medium"])
         | 
| 854 | 
            +
                    min_similarity = difficulty_similarity_map.get(difficulty, 0.3)
         | 
| 855 | 
            +
                    
         | 
| 856 | 
            +
                    # Build input list for thematic word generation
         | 
| 857 | 
            +
                    input_list = topics.copy()  # Start with topics: ["Art"]
         | 
| 858 | 
            +
                    
         | 
| 859 | 
            +
                    # Add custom sentence as separate input if provided
         | 
| 860 | 
            +
                    if custom_sentence:
         | 
| 861 | 
            +
                        input_list.append(custom_sentence)  # Now: ["Art", "i will always love you"]
         | 
| 862 | 
            +
                    
         | 
| 863 | 
            +
                    # Determine if multi-theme processing is needed
         | 
| 864 | 
            +
                    is_multi_theme = len(input_list) > 1
         | 
| 865 | 
            +
                    
         | 
| 866 | 
            +
                    # Set topic_input for generate_thematic_words
         | 
| 867 | 
            +
                    topic_input = input_list if is_multi_theme else input_list[0]
         | 
| 868 | 
            +
                    
         | 
| 869 | 
            +
                    # Get thematic words (get extra for filtering)
         | 
| 870 | 
            +
                    raw_results = self.generate_thematic_words(
         | 
| 871 | 
            +
                        topic_input,
         | 
| 872 | 
            +
                        num_words=150,  # Get extra for difficulty filtering
         | 
| 873 | 
            +
                        min_similarity=min_similarity,
         | 
| 874 | 
            +
                        multi_theme=multi_theme
         | 
| 875 | 
            +
                    )
         | 
| 876 | 
            +
                    
         | 
| 877 | 
            +
                    # Log generated thematic words sorted by tiers
         | 
| 878 | 
            +
                    if raw_results:
         | 
| 879 | 
            +
                        # Group results by tier for sorted display
         | 
| 880 | 
            +
                        tier_groups = {}
         | 
| 881 | 
            +
                        for word, similarity, tier in raw_results:
         | 
| 882 | 
            +
                            if tier not in tier_groups:
         | 
| 883 | 
            +
                                tier_groups[tier] = []
         | 
| 884 | 
            +
                            tier_groups[tier].append((word, similarity))
         | 
| 885 | 
            +
                        
         | 
| 886 | 
            +
                        # Sort tiers from most common to least common
         | 
| 887 | 
            +
                        tier_order = [
         | 
| 888 | 
            +
                            "tier_1_ultra_common",
         | 
| 889 | 
            +
                            "tier_2_extremely_common", 
         | 
| 890 | 
            +
                            "tier_3_very_common",
         | 
| 891 | 
            +
                            "tier_4_highly_common",
         | 
| 892 | 
            +
                            "tier_5_common",
         | 
| 893 | 
            +
                            "tier_6_moderately_common",
         | 
| 894 | 
            +
                            "tier_7_somewhat_uncommon",
         | 
| 895 | 
            +
                            "tier_8_uncommon",
         | 
| 896 | 
            +
                            "tier_9_rare",
         | 
| 897 | 
            +
                            "tier_10_very_rare"
         | 
| 898 | 
            +
                        ]
         | 
| 899 | 
            +
                        
         | 
| 900 | 
            +
                        # Build single log message with all tier information
         | 
| 901 | 
            +
                        log_lines = [f"📊 Generated {len(raw_results)} thematic words, grouped by tiers:"]
         | 
| 902 | 
            +
                        
         | 
| 903 | 
            +
                        for tier in tier_order:
         | 
| 904 | 
            +
                            if tier in tier_groups:
         | 
| 905 | 
            +
                                tier_desc = self.tier_descriptions.get(tier, tier)
         | 
| 906 | 
            +
                                log_lines.append(f"  📊 {tier_desc}:")
         | 
| 907 | 
            +
                                # Sort words within tier alphabetically
         | 
| 908 | 
            +
                                tier_words = sorted(tier_groups[tier], key=lambda x: x[0])
         | 
| 909 | 
            +
                                for word, similarity in tier_words:
         | 
| 910 | 
            +
                                    log_lines.append(f"    {word:<15} (similarity: {similarity:.3f})")
         | 
| 911 | 
            +
                        
         | 
| 912 | 
            +
                        # uncomment this log line if want to print all words returned
         | 
| 913 | 
            +
                        logger.info("\n".join(log_lines))
         | 
| 914 | 
            +
                    else:
         | 
| 915 | 
            +
                        logger.info("📊 No thematic words generated")
         | 
| 916 | 
            +
                    
         | 
| 917 | 
            +
                    # Weighted random tier selection for crossword backend
         | 
| 918 | 
            +
                    # Step 1: Group raw_results by tier and filter by difficulty/length
         | 
| 919 | 
            +
                    tier_groups_filtered = {}
         | 
| 920 | 
            +
                    for word, similarity, tier in raw_results:
         | 
| 921 | 
            +
                        # Only consider words from preferred tiers for this difficulty
         | 
| 922 | 
            +
                        if tier in preferred_tiers: # and self._matches_crossword_difficulty(word, difficulty):
         | 
| 923 | 
            +
                            if tier not in tier_groups_filtered:
         | 
| 924 | 
            +
                                tier_groups_filtered[tier] = []
         | 
| 925 | 
            +
                            tier_groups_filtered[tier].append((word, similarity, tier))
         | 
| 926 | 
            +
                    
         | 
| 927 | 
            +
                    # Step 2: Calculate word distribution across preferred tiers
         | 
| 928 | 
            +
                    tier_word_counts = {tier: len(words) for tier, words in tier_groups_filtered.items()}
         | 
| 929 | 
            +
                    total_available_words = sum(tier_word_counts.values())
         | 
| 930 | 
            +
                    
         | 
| 931 | 
            +
                    logger.info(f"📊 Available words by preferred tier: {tier_word_counts}")
         | 
| 932 | 
            +
                    
         | 
| 933 | 
            +
                    if total_available_words == 0:
         | 
| 934 | 
            +
                        logger.info("⚠️ No words found in preferred tiers, returning empty list")
         | 
| 935 | 
            +
                        return []
         | 
| 936 | 
            +
                    
         | 
| 937 | 
            +
                    # Step 3: Generate clues for ALL words in preferred tiers (no pre-selection)
         | 
| 938 | 
            +
                    candidate_words = []
         | 
| 939 | 
            +
                    
         | 
| 940 | 
            +
                    # Generate clues for all available words in preferred tiers
         | 
| 941 | 
            +
                    # This gives us a large pool to filter by clue quality
         | 
| 942 | 
            +
                    logger.info(f"📊 Generating clues for all {total_available_words} words in preferred tiers")
         | 
| 943 | 
            +
                    for tier, words in tier_groups_filtered.items():
         | 
| 944 | 
            +
                        for word, similarity, tier in words:
         | 
| 945 | 
            +
                            word_data = {
         | 
| 946 | 
            +
                                "word": word.upper(),
         | 
| 947 | 
            +
                                "clue": self._generate_crossword_clue(word, topics),
         | 
| 948 | 
            +
                                "similarity": float(similarity),
         | 
| 949 | 
            +
                                "source": "thematic",
         | 
| 950 | 
            +
                                "tier": tier
         | 
| 951 | 
            +
                            }
         | 
| 952 | 
            +
                            candidate_words.append(word_data)
         | 
| 953 | 
            +
                    
         | 
| 954 | 
            +
                    # Step 5: Filter candidates by clue quality and select best words
         | 
| 955 | 
            +
                    logger.info(f"📊 Generated {len(candidate_words)} candidate words, filtering for clue quality")
         | 
| 956 | 
            +
                    
         | 
| 957 | 
            +
                    # Separate words by clue quality
         | 
| 958 | 
            +
                    quality_words = []  # Words with proper WordNet-based clues
         | 
| 959 | 
            +
                    fallback_words = []  # Words with generic fallback clues
         | 
| 960 | 
            +
                    
         | 
| 961 | 
            +
                    fallback_patterns = ["Related to", "Crossword answer"]
         | 
| 962 | 
            +
                    
         | 
| 963 | 
            +
                    for word_data in candidate_words:
         | 
| 964 | 
            +
                        clue = word_data["clue"]
         | 
| 965 | 
            +
                        has_fallback = any(pattern in clue for pattern in fallback_patterns)
         | 
| 966 | 
            +
                        
         | 
| 967 | 
            +
                        if has_fallback:
         | 
| 968 | 
            +
                            fallback_words.append(word_data)
         | 
| 969 | 
            +
                        else:
         | 
| 970 | 
            +
                            quality_words.append(word_data)
         | 
| 971 | 
            +
                    
         | 
| 972 | 
            +
                    # Prioritize quality words, use fallback only if needed
         | 
| 973 | 
            +
                    final_words = []
         | 
| 974 | 
            +
                    
         | 
| 975 | 
            +
                    # First, add quality words up to requested count
         | 
| 976 | 
            +
                    if quality_words:
         | 
| 977 | 
            +
                        random.shuffle(quality_words)  # Randomize selection
         | 
| 978 | 
            +
                        final_words.extend(quality_words[:requested_words])
         | 
| 979 | 
            +
                    
         | 
| 980 | 
            +
                    # If we don't have enough quality words, add some fallback words
         | 
| 981 | 
            +
                    if len(final_words) < requested_words and fallback_words:
         | 
| 982 | 
            +
                        needed = requested_words - len(final_words)
         | 
| 983 | 
            +
                        random.shuffle(fallback_words)
         | 
| 984 | 
            +
                        final_words.extend(fallback_words[:needed])
         | 
| 985 | 
            +
                    
         | 
| 986 | 
            +
                    # Final shuffle to avoid quality-based ordering
         | 
| 987 | 
            +
                    random.shuffle(final_words)
         | 
| 988 | 
            +
                    
         | 
| 989 | 
            +
                    logger.info(f"✅ Selected {len(final_words)} words ({len([w for w in final_words if not any(p in w['clue'] for p in fallback_patterns)])} quality, {len([w for w in final_words if any(p in w['clue'] for p in fallback_patterns)])} fallback)")
         | 
| 990 | 
            +
                    logger.info(f"📝 Final words: {[w['word'] for w in final_words]}")
         | 
| 991 | 
            +
                    return final_words
         | 
| 992 | 
            +
             | 
| 993 | 
            +
                def _matches_crossword_difficulty(self, word: str, difficulty: str) -> bool:
         | 
| 994 | 
            +
                    """Check if word matches crossword difficulty criteria."""
         | 
| 995 | 
            +
                    difficulty_criteria = {
         | 
| 996 | 
            +
                        "easy": {"min_len": 3, "max_len": 8},
         | 
| 997 | 
            +
                        "medium": {"min_len": 4, "max_len": 10},
         | 
| 998 | 
            +
                        "hard": {"min_len": 5, "max_len": 12}
         | 
| 999 | 
            +
                    }
         | 
| 1000 | 
            +
                    
         | 
| 1001 | 
            +
                    criteria = difficulty_criteria.get(difficulty, difficulty_criteria["medium"])
         | 
| 1002 | 
            +
                    return criteria["min_len"] <= len(word) <= criteria["max_len"]
         | 
| 1003 | 
            +
             | 
| 1004 | 
            +
                def _generate_crossword_clue(self, word: str, topics: List[str]) -> str:
         | 
| 1005 | 
            +
                    """Generate a crossword clue for the word using WordNet."""
         | 
| 1006 | 
            +
                    # Initialize WordNet clue generator if not already done
         | 
| 1007 | 
            +
                    if not hasattr(self, '_wordnet_generator') or self._wordnet_generator is None:
         | 
| 1008 | 
            +
                        try:
         | 
| 1009 | 
            +
                            from .wordnet_clue_generator import WordNetClueGenerator
         | 
| 1010 | 
            +
                            self._wordnet_generator = WordNetClueGenerator(
         | 
| 1011 | 
            +
                                cache_dir=str(self.cache_dir)
         | 
| 1012 | 
            +
                            )
         | 
| 1013 | 
            +
                            self._wordnet_generator.initialize()
         | 
| 1014 | 
            +
                            logger.info("✅ WordNet clue generator initialized on-demand")
         | 
| 1015 | 
            +
                        except Exception as e:
         | 
| 1016 | 
            +
                            logger.warning(f"⚠️ Failed to initialize WordNet clue generator: {e}")
         | 
| 1017 | 
            +
                            self._wordnet_generator = None
         | 
| 1018 | 
            +
                    
         | 
| 1019 | 
            +
                    # Use WordNet generator if available
         | 
| 1020 | 
            +
                    if self._wordnet_generator:
         | 
| 1021 | 
            +
                        try:
         | 
| 1022 | 
            +
                            primary_topic = topics[0] if topics else "general"
         | 
| 1023 | 
            +
                            clue = self._wordnet_generator.generate_clue(word, primary_topic)
         | 
| 1024 | 
            +
                            if clue and len(clue.strip()) > 0:
         | 
| 1025 | 
            +
                                return clue
         | 
| 1026 | 
            +
                        except Exception as e:
         | 
| 1027 | 
            +
                            logger.warning(f"⚠️ WordNet clue generation failed for '{word}': {e}")
         | 
| 1028 | 
            +
                    
         | 
| 1029 | 
            +
                    # Fallback to simple templates if WordNet fails
         | 
| 1030 | 
            +
                    word_lower = word.lower()
         | 
| 1031 | 
            +
                    primary_topic = topics[0] if topics else "general"
         | 
| 1032 | 
            +
                    topic_lower = primary_topic.lower()
         | 
| 1033 | 
            +
                    
         | 
| 1034 | 
            +
                    # Topic-specific clue templates as fallback
         | 
| 1035 | 
            +
                    if any(keyword in topic_lower for keyword in ["animal", "pet", "wildlife"]):
         | 
| 1036 | 
            +
                        return f"{word_lower} (animal)"
         | 
| 1037 | 
            +
                    elif any(keyword in topic_lower for keyword in ["tech", "computer", "software", "digital"]):
         | 
| 1038 | 
            +
                        return f"{word_lower} (technology)"
         | 
| 1039 | 
            +
                    elif any(keyword in topic_lower for keyword in ["science", "biology", "chemistry", "physics"]):
         | 
| 1040 | 
            +
                        return f"{word_lower} (science)"
         | 
| 1041 | 
            +
                    elif any(keyword in topic_lower for keyword in ["geo", "place", "city", "country", "location"]):
         | 
| 1042 | 
            +
                        return f"{word_lower} (geography)"
         | 
| 1043 | 
            +
                    elif any(keyword in topic_lower for keyword in ["food", "cooking", "cuisine", "recipe"]):
         | 
| 1044 | 
            +
                        return f"{word_lower} (food)"
         | 
| 1045 | 
            +
                    elif any(keyword in topic_lower for keyword in ["music", "song", "instrument", "audio"]):
         | 
| 1046 | 
            +
                        return f"{word_lower} (music)"
         | 
| 1047 | 
            +
                    elif any(keyword in topic_lower for keyword in ["sport", "game", "athletic", "exercise"]):
         | 
| 1048 | 
            +
                        return f"{word_lower} (sports)"
         | 
| 1049 | 
            +
                    else:
         | 
| 1050 | 
            +
                        return f"{word_lower} (related to {topic_lower})"
         | 
| 1051 | 
            +
             | 
| 1052 | 
            +
             | 
| 1053 | 
            +
            # Backwards compatibility aliases
         | 
| 1054 | 
            +
            ThematicWordGenerator = ThematicWordService  # For existing code
         | 
| 1055 | 
            +
            UnifiedThematicWordGenerator = ThematicWordService  # For existing code
         | 
| 1056 | 
            +
             | 
| 1057 | 
            +
            # Backend service - no interactive demo needed
         | 
    	
        crossword-app/backend-py/src/services/unified_word_service.py
    ADDED
    
    | @@ -0,0 +1,250 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Integration adapter for Unified Thematic Word Generator
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            This service provides a bridge between the new unified word generator
         | 
| 5 | 
            +
            and the existing crossword backend, enabling the backend to use the
         | 
| 6 | 
            +
            comprehensive WordFreq vocabulary instead of the limited model vocabulary.
         | 
| 7 | 
            +
            """
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            import os
         | 
| 10 | 
            +
            import sys
         | 
| 11 | 
            +
            import logging
         | 
| 12 | 
            +
            from typing import List, Dict, Any, Optional
         | 
| 13 | 
            +
            from pathlib import Path
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            class UnifiedWordService:
         | 
| 18 | 
            +
                """
         | 
| 19 | 
            +
                Service adapter for integrating UnifiedThematicWordGenerator with the crossword backend.
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                Provides the same interface as VectorSearchService but uses the comprehensive
         | 
| 22 | 
            +
                WordFreq vocabulary instead of model-limited vocabulary.
         | 
| 23 | 
            +
                """
         | 
| 24 | 
            +
                
         | 
| 25 | 
            +
                def __init__(self, vocab_size_limit: Optional[int] = None):
         | 
| 26 | 
            +
                    """Initialize the unified word service.
         | 
| 27 | 
            +
                    
         | 
| 28 | 
            +
                    Args:
         | 
| 29 | 
            +
                        vocab_size_limit: Maximum vocabulary size (None for default 100K)
         | 
| 30 | 
            +
                    """
         | 
| 31 | 
            +
                    self.generator = None
         | 
| 32 | 
            +
                    self.vocab_size_limit = vocab_size_limit or int(os.getenv("MAX_VOCABULARY_SIZE", "100000"))
         | 
| 33 | 
            +
                    self.is_initialized = False
         | 
| 34 | 
            +
                    
         | 
| 35 | 
            +
                    # Import the generator from hack directory
         | 
| 36 | 
            +
                    self._import_generator()
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                def _import_generator(self):
         | 
| 39 | 
            +
                    """Import the UnifiedThematicWordGenerator from hack directory."""
         | 
| 40 | 
            +
                    try:
         | 
| 41 | 
            +
                        # Add hack directory to path
         | 
| 42 | 
            +
                        hack_dir = Path(__file__).parent.parent.parent.parent.parent / "hack"
         | 
| 43 | 
            +
                        if hack_dir.exists():
         | 
| 44 | 
            +
                            sys.path.insert(0, str(hack_dir))
         | 
| 45 | 
            +
                            logger.info(f"📁 Added hack directory to path: {hack_dir}")
         | 
| 46 | 
            +
                        
         | 
| 47 | 
            +
                        # Import the generator
         | 
| 48 | 
            +
                        from thematic_word_generator import UnifiedThematicWordGenerator
         | 
| 49 | 
            +
                        
         | 
| 50 | 
            +
                        # Initialize with appropriate cache directory
         | 
| 51 | 
            +
                        cache_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'cache', 'unified_generator')
         | 
| 52 | 
            +
                        
         | 
| 53 | 
            +
                        self.generator = UnifiedThematicWordGenerator(
         | 
| 54 | 
            +
                            cache_dir=cache_dir,
         | 
| 55 | 
            +
                            vocab_size_limit=self.vocab_size_limit
         | 
| 56 | 
            +
                        )
         | 
| 57 | 
            +
                        
         | 
| 58 | 
            +
                        logger.info(f"✅ Imported UnifiedThematicWordGenerator with vocab limit: {self.vocab_size_limit:,}")
         | 
| 59 | 
            +
                        
         | 
| 60 | 
            +
                    except ImportError as e:
         | 
| 61 | 
            +
                        logger.error(f"❌ Failed to import UnifiedThematicWordGenerator: {e}")
         | 
| 62 | 
            +
                        logger.error("   Make sure the hack directory contains thematic_word_generator.py")
         | 
| 63 | 
            +
                        self.generator = None
         | 
| 64 | 
            +
                    except Exception as e:
         | 
| 65 | 
            +
                        logger.error(f"❌ Error setting up UnifiedThematicWordGenerator: {e}")
         | 
| 66 | 
            +
                        self.generator = None
         | 
| 67 | 
            +
                
         | 
| 68 | 
            +
                async def initialize(self):
         | 
| 69 | 
            +
                    """Initialize the unified word service."""
         | 
| 70 | 
            +
                    if not self.generator:
         | 
| 71 | 
            +
                        logger.error("❌ Cannot initialize: generator not available")
         | 
| 72 | 
            +
                        return False
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                    try:
         | 
| 75 | 
            +
                        logger.info("🚀 Initializing Unified Word Service...")
         | 
| 76 | 
            +
                        start_time = time.time()
         | 
| 77 | 
            +
                        
         | 
| 78 | 
            +
                        # Initialize the generator (async compatible)
         | 
| 79 | 
            +
                        await self.generator.initialize_async()
         | 
| 80 | 
            +
                        
         | 
| 81 | 
            +
                        self.is_initialized = True
         | 
| 82 | 
            +
                        init_time = time.time() - start_time
         | 
| 83 | 
            +
                        
         | 
| 84 | 
            +
                        logger.info(f"✅ Unified Word Service initialized in {init_time:.2f}s")
         | 
| 85 | 
            +
                        logger.info(f"📊 Vocabulary size: {self.generator.get_vocabulary_size():,} words")
         | 
| 86 | 
            +
                        logger.info(f"🎯 Tier distribution: {self.generator.get_tier_distribution()}")
         | 
| 87 | 
            +
                        
         | 
| 88 | 
            +
                        return True
         | 
| 89 | 
            +
                        
         | 
| 90 | 
            +
                    except Exception as e:
         | 
| 91 | 
            +
                        logger.error(f"❌ Failed to initialize Unified Word Service: {e}")
         | 
| 92 | 
            +
                        self.is_initialized = False
         | 
| 93 | 
            +
                        return False
         | 
| 94 | 
            +
                
         | 
| 95 | 
            +
                async def find_similar_words(
         | 
| 96 | 
            +
                    self, 
         | 
| 97 | 
            +
                    topic: str, 
         | 
| 98 | 
            +
                    difficulty: str = "medium", 
         | 
| 99 | 
            +
                    max_words: int = 15
         | 
| 100 | 
            +
                ) -> List[Dict[str, Any]]:
         | 
| 101 | 
            +
                    """
         | 
| 102 | 
            +
                    Find similar words using the unified generator.
         | 
| 103 | 
            +
                    
         | 
| 104 | 
            +
                    Compatible with VectorSearchService interface.
         | 
| 105 | 
            +
                    
         | 
| 106 | 
            +
                    Args:
         | 
| 107 | 
            +
                        topic: Topic to find words for
         | 
| 108 | 
            +
                        difficulty: Difficulty level (easy/medium/hard)
         | 
| 109 | 
            +
                        max_words: Maximum number of words to return
         | 
| 110 | 
            +
                        
         | 
| 111 | 
            +
                    Returns:
         | 
| 112 | 
            +
                        List of word dictionaries: [{"word": str, "clue": str}, ...]
         | 
| 113 | 
            +
                    """
         | 
| 114 | 
            +
                    if not self.is_initialized or not self.generator:
         | 
| 115 | 
            +
                        logger.error("❌ Service not initialized or generator not available")
         | 
| 116 | 
            +
                        return []
         | 
| 117 | 
            +
                    
         | 
| 118 | 
            +
                    try:
         | 
| 119 | 
            +
                        # Use the generator's backend-compatible method
         | 
| 120 | 
            +
                        results = await self.generator.find_similar_words(topic, difficulty, max_words)
         | 
| 121 | 
            +
                        
         | 
| 122 | 
            +
                        logger.info(f"🎯 Generated {len(results)} words for '{topic}' (difficulty: {difficulty})")
         | 
| 123 | 
            +
                        return results
         | 
| 124 | 
            +
                        
         | 
| 125 | 
            +
                    except Exception as e:
         | 
| 126 | 
            +
                        logger.error(f"❌ Error finding similar words for '{topic}': {e}")
         | 
| 127 | 
            +
                        return []
         | 
| 128 | 
            +
                
         | 
| 129 | 
            +
                async def _get_cached_fallback(self, topic: str, difficulty: str, max_words: int) -> List[Dict[str, Any]]:
         | 
| 130 | 
            +
                    """
         | 
| 131 | 
            +
                    Fallback method for compatibility with existing backend code.
         | 
| 132 | 
            +
                    
         | 
| 133 | 
            +
                    Since our unified generator already has comprehensive vocabulary,
         | 
| 134 | 
            +
                    this just calls find_similar_words with relaxed criteria.
         | 
| 135 | 
            +
                    """
         | 
| 136 | 
            +
                    if not self.is_initialized or not self.generator:
         | 
| 137 | 
            +
                        return []
         | 
| 138 | 
            +
                    
         | 
| 139 | 
            +
                    try:
         | 
| 140 | 
            +
                        # Try with lower similarity threshold as "fallback"
         | 
| 141 | 
            +
                        results = self.generator.generate_thematic_words(
         | 
| 142 | 
            +
                            topic, 
         | 
| 143 | 
            +
                            num_words=max_words,
         | 
| 144 | 
            +
                            min_similarity=0.2  # Lower threshold for fallback
         | 
| 145 | 
            +
                        )
         | 
| 146 | 
            +
                        
         | 
| 147 | 
            +
                        # Format for backend compatibility
         | 
| 148 | 
            +
                        backend_words = []
         | 
| 149 | 
            +
                        for word, similarity, tier in results:
         | 
| 150 | 
            +
                            if self.generator._matches_backend_difficulty(word, difficulty):
         | 
| 151 | 
            +
                                backend_word = {
         | 
| 152 | 
            +
                                    "word": word.upper(),
         | 
| 153 | 
            +
                                    "clue": self.generator._generate_simple_clue(word, topic),
         | 
| 154 | 
            +
                                    "similarity": similarity,
         | 
| 155 | 
            +
                                    "tier": tier
         | 
| 156 | 
            +
                                }
         | 
| 157 | 
            +
                                backend_words.append(backend_word)
         | 
| 158 | 
            +
                        
         | 
| 159 | 
            +
                        logger.info(f"📦 Fallback generated {len(backend_words)} words for '{topic}'")
         | 
| 160 | 
            +
                        return backend_words[:max_words]
         | 
| 161 | 
            +
                        
         | 
| 162 | 
            +
                    except Exception as e:
         | 
| 163 | 
            +
                        logger.error(f"❌ Error in cached fallback for '{topic}': {e}")
         | 
| 164 | 
            +
                        return []
         | 
| 165 | 
            +
                
         | 
| 166 | 
            +
                def get_vocabulary_size(self) -> int:
         | 
| 167 | 
            +
                    """Get the vocabulary size."""
         | 
| 168 | 
            +
                    if self.generator:
         | 
| 169 | 
            +
                        return self.generator.get_vocabulary_size()
         | 
| 170 | 
            +
                    return 0
         | 
| 171 | 
            +
                
         | 
| 172 | 
            +
                def get_tier_info(self) -> Dict[str, Any]:
         | 
| 173 | 
            +
                    """Get frequency tier information."""
         | 
| 174 | 
            +
                    if not self.generator:
         | 
| 175 | 
            +
                        return {}
         | 
| 176 | 
            +
                        
         | 
| 177 | 
            +
                    return {
         | 
| 178 | 
            +
                        "tier_distribution": self.generator.get_tier_distribution(),
         | 
| 179 | 
            +
                        "tier_descriptions": getattr(self.generator, 'tier_descriptions', {}),
         | 
| 180 | 
            +
                        "vocabulary_size": self.generator.get_vocabulary_size()
         | 
| 181 | 
            +
                    }
         | 
| 182 | 
            +
             | 
| 183 | 
            +
             | 
| 184 | 
            +
            # Import time for initialization timing
         | 
| 185 | 
            +
            import time
         | 
| 186 | 
            +
             | 
| 187 | 
            +
            # Factory function for easy backend integration
         | 
| 188 | 
            +
            async def create_unified_word_service(vocab_size_limit: Optional[int] = None) -> Optional[UnifiedWordService]:
         | 
| 189 | 
            +
                """
         | 
| 190 | 
            +
                Factory function to create and initialize a UnifiedWordService.
         | 
| 191 | 
            +
                
         | 
| 192 | 
            +
                Args:
         | 
| 193 | 
            +
                    vocab_size_limit: Maximum vocabulary size (None for default)
         | 
| 194 | 
            +
                    
         | 
| 195 | 
            +
                Returns:
         | 
| 196 | 
            +
                    Initialized UnifiedWordService or None if initialization failed
         | 
| 197 | 
            +
                """
         | 
| 198 | 
            +
                try:
         | 
| 199 | 
            +
                    service = UnifiedWordService(vocab_size_limit)
         | 
| 200 | 
            +
                    
         | 
| 201 | 
            +
                    if await service.initialize():
         | 
| 202 | 
            +
                        return service
         | 
| 203 | 
            +
                    else:
         | 
| 204 | 
            +
                        logger.error("❌ Failed to initialize UnifiedWordService")
         | 
| 205 | 
            +
                        return None
         | 
| 206 | 
            +
                        
         | 
| 207 | 
            +
                except Exception as e:
         | 
| 208 | 
            +
                    logger.error(f"❌ Error creating UnifiedWordService: {e}")
         | 
| 209 | 
            +
                    return None
         | 
| 210 | 
            +
             | 
| 211 | 
            +
             | 
| 212 | 
            +
            # Example usage for testing
         | 
| 213 | 
            +
            async def main():
         | 
| 214 | 
            +
                """Test the unified word service."""
         | 
| 215 | 
            +
                print("🧪 Testing Unified Word Service")
         | 
| 216 | 
            +
                print("=" * 50)
         | 
| 217 | 
            +
                
         | 
| 218 | 
            +
                # Create and initialize service
         | 
| 219 | 
            +
                service = await create_unified_word_service(vocab_size_limit=50000)  # Smaller vocab for testing
         | 
| 220 | 
            +
                
         | 
| 221 | 
            +
                if not service:
         | 
| 222 | 
            +
                    print("❌ Failed to create service")
         | 
| 223 | 
            +
                    return
         | 
| 224 | 
            +
                
         | 
| 225 | 
            +
                # Test word generation
         | 
| 226 | 
            +
                test_topics = ["animal", "science", "technology"]
         | 
| 227 | 
            +
                
         | 
| 228 | 
            +
                for topic in test_topics:
         | 
| 229 | 
            +
                    print(f"\n🎯 Testing topic: '{topic}'")
         | 
| 230 | 
            +
                    print("-" * 30)
         | 
| 231 | 
            +
                    
         | 
| 232 | 
            +
                    for difficulty in ["easy", "medium", "hard"]:
         | 
| 233 | 
            +
                        words = await service.find_similar_words(topic, difficulty, max_words=5)
         | 
| 234 | 
            +
                        
         | 
| 235 | 
            +
                        print(f"  {difficulty.capitalize()}: {len(words)} words")
         | 
| 236 | 
            +
                        for word_data in words:
         | 
| 237 | 
            +
                            word = word_data['word']
         | 
| 238 | 
            +
                            tier = word_data.get('tier', 'unknown')
         | 
| 239 | 
            +
                            print(f"    {word:<12} ({tier})")
         | 
| 240 | 
            +
                
         | 
| 241 | 
            +
                print(f"\n📊 Service Info:")
         | 
| 242 | 
            +
                print(f"   Vocabulary size: {service.get_vocabulary_size():,}")
         | 
| 243 | 
            +
                print(f"   Tier info: {service.get_tier_info()}")
         | 
| 244 | 
            +
                
         | 
| 245 | 
            +
                print("\n✅ Test completed!")
         | 
| 246 | 
            +
             | 
| 247 | 
            +
             | 
| 248 | 
            +
            if __name__ == "__main__":
         | 
| 249 | 
            +
                import asyncio
         | 
| 250 | 
            +
                asyncio.run(main())
         | 
    	
        crossword-app/backend-py/src/services/vector_search.py
    CHANGED
    
    | @@ -3,6 +3,7 @@ Vector similarity search service using sentence-transformers and FAISS. | |
| 3 | 
             
            This implements true AI word generation via vector space nearest neighbor search.
         | 
| 4 | 
             
            """
         | 
| 5 |  | 
|  | |
| 6 | 
             
            import os
         | 
| 7 | 
             
            import logging
         | 
| 8 | 
             
            import asyncio
         | 
| @@ -21,10 +22,7 @@ from pathlib import Path | |
| 21 |  | 
| 22 | 
             
            logger = logging.getLogger(__name__)
         | 
| 23 |  | 
| 24 | 
            -
             | 
| 25 | 
            -
                """Helper to log with precise timestamp."""
         | 
| 26 | 
            -
                timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
         | 
| 27 | 
            -
                logger.info(f"[{timestamp}] {message}")
         | 
| 28 |  | 
| 29 | 
             
            class VectorSearchService:
         | 
| 30 | 
             
                """
         | 
| @@ -70,29 +68,29 @@ class VectorSearchService: | |
| 70 | 
             
                        start_time = time.time()
         | 
| 71 |  | 
| 72 | 
             
                        # Log environment configuration for debugging
         | 
| 73 | 
            -
                         | 
| 74 | 
            -
                         | 
| 75 | 
            -
                         | 
| 76 | 
            -
                         | 
| 77 | 
            -
                         | 
| 78 | 
            -
                         | 
| 79 | 
            -
                         | 
| 80 | 
            -
                         | 
| 81 |  | 
| 82 | 
            -
                         | 
| 83 |  | 
| 84 | 
             
                        # Load sentence transformer model
         | 
| 85 | 
             
                        model_start = time.time()
         | 
| 86 | 
             
                        self.model = SentenceTransformer(self.model_name)
         | 
| 87 | 
             
                        model_time = time.time() - model_start
         | 
| 88 | 
            -
                         | 
| 89 |  | 
| 90 | 
             
                        # Try to load from cache first
         | 
| 91 | 
             
                        if self._load_cached_index():
         | 
| 92 | 
            -
                             | 
| 93 | 
             
                        else:
         | 
| 94 | 
             
                            # Build from scratch
         | 
| 95 | 
            -
                             | 
| 96 |  | 
| 97 | 
             
                            # Get model vocabulary from tokenizer
         | 
| 98 | 
             
                            vocab_start = time.time()
         | 
| @@ -102,36 +100,36 @@ class VectorSearchService: | |
| 102 | 
             
                            # Filter vocabulary for crossword-suitable words
         | 
| 103 | 
             
                            self.vocab = self._filter_vocabulary(vocab_dict)
         | 
| 104 | 
             
                            vocab_time = time.time() - vocab_start
         | 
| 105 | 
            -
                             | 
| 106 |  | 
| 107 | 
             
                            # Pre-compute embeddings for all vocabulary words
         | 
| 108 | 
             
                            embedding_start = time.time()
         | 
| 109 | 
            -
                             | 
| 110 | 
             
                            await self._build_embeddings_index()
         | 
| 111 | 
             
                            embedding_time = time.time() - embedding_start
         | 
| 112 | 
            -
                             | 
| 113 |  | 
| 114 | 
             
                            # Save to cache for next time
         | 
| 115 | 
             
                            self._save_index_to_cache()
         | 
| 116 |  | 
| 117 | 
             
                        # Initialize cache manager
         | 
| 118 | 
             
                        cache_start = time.time()
         | 
| 119 | 
            -
                         | 
| 120 | 
             
                        try:
         | 
| 121 | 
             
                            from .word_cache import WordCacheManager
         | 
| 122 | 
             
                            self.cache_manager = WordCacheManager()
         | 
| 123 | 
             
                            await self.cache_manager.initialize()
         | 
| 124 | 
             
                            cache_time = time.time() - cache_start
         | 
| 125 | 
            -
                             | 
| 126 | 
             
                        except Exception as e:
         | 
| 127 | 
             
                            cache_time = time.time() - cache_start
         | 
| 128 | 
            -
                             | 
| 129 | 
            -
                             | 
| 130 | 
             
                            self.cache_manager = None
         | 
| 131 |  | 
| 132 | 
             
                        self.is_initialized = True
         | 
| 133 | 
             
                        total_time = time.time() - start_time
         | 
| 134 | 
            -
                         | 
| 135 |  | 
| 136 | 
             
                    except Exception as e:
         | 
| 137 | 
             
                        logger.error(f"❌ Failed to initialize vector search: {e}")
         | 
| @@ -140,7 +138,7 @@ class VectorSearchService: | |
| 140 |  | 
| 141 | 
             
                def _filter_vocabulary(self, vocab_dict: Dict[str, int]) -> List[str]:
         | 
| 142 | 
             
                    """Filter vocabulary to keep only crossword-suitable words."""
         | 
| 143 | 
            -
                     | 
| 144 |  | 
| 145 | 
             
                    # Pre-compile excluded words set for faster lookup
         | 
| 146 | 
             
                    excluded_words = {
         | 
| @@ -159,7 +157,7 @@ class VectorSearchService: | |
| 159 |  | 
| 160 | 
             
                        # Progress logging for large vocabularies
         | 
| 161 | 
             
                        if processed % 10000 == 0:
         | 
| 162 | 
            -
                             | 
| 163 |  | 
| 164 | 
             
                        # Clean word (remove special tokens) - optimized
         | 
| 165 | 
             
                        if word.startswith('##'):
         | 
| @@ -191,7 +189,7 @@ class VectorSearchService: | |
| 191 |  | 
| 192 | 
             
                    # Remove duplicates efficiently and sort
         | 
| 193 | 
             
                    unique_filtered = sorted(list(set(filtered)))
         | 
| 194 | 
            -
                     | 
| 195 |  | 
| 196 | 
             
                    return unique_filtered
         | 
| 197 |  | 
| @@ -225,7 +223,7 @@ class VectorSearchService: | |
| 225 | 
             
                    cpu_count = os.cpu_count() or 1
         | 
| 226 | 
             
                    # Larger batches for better throughput, smaller for HF Spaces limited memory
         | 
| 227 | 
             
                    batch_size = min(200 if cpu_count > 2 else 100, len(self.vocab) // 4)
         | 
| 228 | 
            -
                     | 
| 229 |  | 
| 230 | 
             
                    embeddings_list = []
         | 
| 231 | 
             
                    total_batches = (len(self.vocab) + batch_size - 1) // batch_size
         | 
| @@ -249,24 +247,24 @@ class VectorSearchService: | |
| 249 | 
             
                        # Progress logging - more frequent for slower HF Spaces
         | 
| 250 | 
             
                        if batch_num % max(1, total_batches // 10) == 0:
         | 
| 251 | 
             
                            progress = (batch_num / total_batches) * 100
         | 
| 252 | 
            -
                             | 
| 253 |  | 
| 254 | 
             
                    # Combine all embeddings
         | 
| 255 | 
            -
                     | 
| 256 | 
             
                    self.word_embeddings = np.vstack(embeddings_list)
         | 
| 257 | 
             
                    logger.info(f"📈 Generated embeddings shape: {self.word_embeddings.shape}")
         | 
| 258 |  | 
| 259 | 
             
                    # Build FAISS index for fast similarity search
         | 
| 260 | 
            -
                     | 
| 261 | 
             
                    dimension = self.word_embeddings.shape[1]
         | 
| 262 | 
             
                    self.faiss_index = faiss.IndexFlatIP(dimension)  # Inner product similarity
         | 
| 263 |  | 
| 264 | 
             
                    # Normalize embeddings for cosine similarity
         | 
| 265 | 
            -
                     | 
| 266 | 
             
                    faiss.normalize_L2(self.word_embeddings)
         | 
| 267 |  | 
| 268 | 
             
                    # Add to FAISS index
         | 
| 269 | 
            -
                     | 
| 270 | 
             
                    self.faiss_index.add(self.word_embeddings)
         | 
| 271 |  | 
| 272 | 
             
                    logger.info(f"🔍 FAISS index built with {self.faiss_index.ntotal} vectors")
         | 
| @@ -316,7 +314,7 @@ class VectorSearchService: | |
| 316 |  | 
| 317 | 
             
                                # Track these words to prevent future repetition
         | 
| 318 | 
             
                                if similar_words:
         | 
| 319 | 
            -
                                    self._track_used_words(topic,  | 
| 320 |  | 
| 321 | 
             
                                # Cache successful results for future use
         | 
| 322 | 
             
                                if similar_words:
         | 
| @@ -338,7 +336,7 @@ class VectorSearchService: | |
| 338 |  | 
| 339 | 
             
                            # Track these words to prevent future repetition
         | 
| 340 | 
             
                            if similar_words:
         | 
| 341 | 
            -
                                self._track_used_words(topic,  | 
| 342 |  | 
| 343 | 
             
                        # If not enough words found, supplement with cached words (more aggressive)
         | 
| 344 | 
             
                        if len(similar_words) < max_words * 0.75:  # If less than 75% of target, supplement
         | 
| @@ -578,7 +576,8 @@ class VectorSearchService: | |
| 578 | 
             
                                     'urban', 'rural', 'geological', 'topographical', 'cartographic']
         | 
| 579 | 
             
                    }
         | 
| 580 |  | 
| 581 | 
            -
                    for candidate in candidates[:10]:  # Only consider top 10 for performance
         | 
|  | |
| 582 | 
             
                        word = candidate['word'].lower()
         | 
| 583 | 
             
                        similarity = candidate['similarity']
         | 
| 584 |  | 
| @@ -691,6 +690,8 @@ class VectorSearchService: | |
| 691 |  | 
| 692 | 
             
                        main_topic_candidates.extend(variation_candidates)
         | 
| 693 |  | 
|  | |
|  | |
| 694 | 
             
                    logger.info(f"🔍 Main topic search found {len(main_topic_candidates)} candidates")
         | 
| 695 |  | 
| 696 | 
             
                    # Phase 2: Identify subcategories from best candidates
         | 
| @@ -919,7 +920,18 @@ class VectorSearchService: | |
| 919 | 
             
                    max_words: int
         | 
| 920 | 
             
                ) -> List[Dict[str, Any]]:
         | 
| 921 | 
             
                    """
         | 
| 922 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 923 | 
             
                    """
         | 
| 924 | 
             
                    if len(candidates) <= max_words:
         | 
| 925 | 
             
                        return candidates
         | 
| @@ -1047,38 +1059,38 @@ class VectorSearchService: | |
| 1047 | 
             
                    """Load FAISS index from cache if available."""
         | 
| 1048 | 
             
                    try:
         | 
| 1049 | 
             
                        if not self._cache_exists():
         | 
| 1050 | 
            -
                             | 
| 1051 | 
             
                            return False
         | 
| 1052 |  | 
| 1053 | 
            -
                         | 
| 1054 | 
             
                        cache_start = time.time()
         | 
| 1055 |  | 
| 1056 | 
             
                        # Load vocabulary
         | 
| 1057 | 
             
                        with open(self.vocab_cache_path, 'rb') as f:
         | 
| 1058 | 
             
                            self.vocab = pickle.load(f)
         | 
| 1059 | 
            -
                         | 
| 1060 |  | 
| 1061 | 
             
                        # Load embeddings
         | 
| 1062 | 
             
                        self.word_embeddings = np.load(self.embeddings_cache_path)
         | 
| 1063 | 
            -
                         | 
| 1064 |  | 
| 1065 | 
             
                        # Load FAISS index
         | 
| 1066 | 
             
                        self.faiss_index = faiss.read_index(self.faiss_cache_path)
         | 
| 1067 | 
            -
                         | 
| 1068 |  | 
| 1069 | 
             
                        cache_time = time.time() - cache_start
         | 
| 1070 | 
            -
                         | 
| 1071 | 
             
                        return True
         | 
| 1072 |  | 
| 1073 | 
             
                    except Exception as e:
         | 
| 1074 | 
            -
                         | 
| 1075 | 
            -
                         | 
| 1076 | 
             
                        return False
         | 
| 1077 |  | 
| 1078 | 
             
                def _save_index_to_cache(self):
         | 
| 1079 | 
             
                    """Save the built FAISS index to cache for future use."""
         | 
| 1080 | 
             
                    try:
         | 
| 1081 | 
            -
                         | 
| 1082 | 
             
                        save_start = time.time()
         | 
| 1083 |  | 
| 1084 | 
             
                        # Save vocabulary
         | 
| @@ -1092,12 +1104,12 @@ class VectorSearchService: | |
| 1092 | 
             
                        faiss.write_index(self.faiss_index, self.faiss_cache_path)
         | 
| 1093 |  | 
| 1094 | 
             
                        save_time = time.time() - save_start
         | 
| 1095 | 
            -
                         | 
| 1096 | 
            -
                         | 
| 1097 |  | 
| 1098 | 
             
                    except Exception as e:
         | 
| 1099 | 
            -
                         | 
| 1100 | 
            -
                         | 
| 1101 |  | 
| 1102 | 
             
                def _is_topic_relevant(self, word: str, topic: str) -> bool:
         | 
| 1103 | 
             
                    """
         | 
| @@ -1182,16 +1194,16 @@ class VectorSearchService: | |
| 1182 | 
             
                        # Filter by difficulty and quality
         | 
| 1183 | 
             
                        if self._matches_difficulty(word, difficulty):
         | 
| 1184 | 
             
                            difficulty_passed += 1
         | 
| 1185 | 
            -
                            if self._is_interesting_word(word, topic) and self._is_topic_relevant(word, topic):
         | 
| 1186 | 
            -
             | 
| 1187 | 
            -
             | 
| 1188 | 
            -
             | 
| 1189 | 
            -
             | 
| 1190 | 
            -
             | 
| 1191 | 
            -
             | 
| 1192 | 
            -
             | 
| 1193 | 
            -
                            else:
         | 
| 1194 | 
            -
             | 
| 1195 | 
             
                        else:
         | 
| 1196 | 
             
                            rejected_words.append(f"{word}({score:.3f})")
         | 
| 1197 |  | 
| @@ -1341,58 +1353,49 @@ class VectorSearchService: | |
| 1341 | 
             
                        "animals": [
         | 
| 1342 | 
             
                            {"word": "DOG", "clue": "Man's best friend"},
         | 
| 1343 | 
             
                            {"word": "CAT", "clue": "Feline pet"},
         | 
| 1344 | 
            -
                            {"word": "ELEPHANT", "clue": "Large mammal with trunk"},
         | 
| 1345 | 
            -
                            {"word": "TIGER", "clue": "Striped big cat"},
         | 
| 1346 | 
            -
                            {"word": "BIRD", "clue": "Flying creature"},
         | 
| 1347 | 
             
                            {"word": "FISH", "clue": "Aquatic animal"},
         | 
| 1348 | 
            -
                            {"word": "HORSE", "clue": "Riding animal"},
         | 
| 1349 | 
            -
                            {"word": "BEAR", "clue": "Large mammal"},
         | 
| 1350 | 
            -
                            {"word": "WHALE", "clue": "Marine mammal"},
         | 
| 1351 | 
            -
                            {"word": "LION", "clue": "King of jungle"},
         | 
| 1352 | 
            -
                            {"word": "RABBIT", "clue": "Hopping mammal"},
         | 
| 1353 | 
            -
                            {"word": "SNAKE", "clue": "Slithering reptile"}
         | 
| 1354 | 
             
                        ],
         | 
| 1355 | 
             
                        "science": [
         | 
| 1356 | 
            -
                            {"word": "ATOM", "clue": "Basic unit of matter"},
         | 
| 1357 | 
            -
                            {"word": "CELL", "clue": "Basic unit of life"},
         | 
| 1358 | 
            -
                            {"word": "DNA", "clue": "Genetic material"},
         | 
| 1359 | 
            -
                            {"word": "ENERGY", "clue": "Capacity to do work"},
         | 
| 1360 | 
            -
                            {"word": "FORCE", "clue": "Push or pull"},
         | 
| 1361 | 
            -
                            {"word": "GRAVITY", "clue": "Force of attraction"},
         | 
| 1362 | 
            -
                            {"word": "LIGHT", "clue": "Electromagnetic radiation"},
         | 
| 1363 | 
            -
                            {"word": "MATTER", "clue": "Physical substance"},
         | 
| 1364 | 
            -
                            {"word": "MOTION", "clue": "Change in position"},
         | 
| 1365 | 
            -
                            {"word": "OXYGEN", "clue": "Essential gas"},
         | 
| 1366 | 
            -
                            {"word": "PHYSICS", "clue": "Study of matter and energy"},
         | 
| 1367 | 
            -
                            {"word": "THEORY", "clue": "Scientific explanation"}
         | 
| 1368 | 
             
                        ],
         | 
| 1369 | 
             
                        "technology": [
         | 
| 1370 | 
            -
                            {"word": "COMPUTER", "clue": "Electronic device"},
         | 
| 1371 | 
            -
                            {"word": "INTERNET", "clue": "Global network"},
         | 
| 1372 | 
            -
                            {"word": "SOFTWARE", "clue": "Computer programs"},
         | 
| 1373 | 
            -
                            {"word": "ROBOT", "clue": "Automated machine"},
         | 
| 1374 | 
            -
                            {"word": "DATA", "clue": "Information"},
         | 
| 1375 | 
            -
                            {"word": "CODE", "clue": "Programming instructions"},
         | 
| 1376 | 
            -
                            {"word": "DIGITAL", "clue": "Electronic format"},
         | 
| 1377 | 
            -
                            {"word": "NETWORK", "clue": "Connected systems"},
         | 
| 1378 | 
            -
                            {"word": "SYSTEM", "clue": "Organized whole"},
         | 
| 1379 | 
            -
                            {"word": "DEVICE", "clue": "Technical apparatus"},
         | 
| 1380 | 
            -
                            {"word": "MOBILE", "clue": "Portable technology"},
         | 
| 1381 | 
            -
                            {"word": "SCREEN", "clue": "Display surface"}
         | 
| 1382 | 
             
                        ],
         | 
| 1383 | 
             
                        "geography": [
         | 
| 1384 | 
            -
                            {"word": "MOUNTAIN", "clue": "High landform"},
         | 
| 1385 | 
            -
                            {"word": "RIVER", "clue": "Flowing water"},
         | 
| 1386 | 
            -
                            {"word": "OCEAN", "clue": "Large body of water"},
         | 
| 1387 | 
            -
                            {"word": "DESERT", "clue": "Arid region"},
         | 
| 1388 | 
            -
                            {"word": "FOREST", "clue": "Dense trees"},
         | 
| 1389 | 
            -
                            {"word": "ISLAND", "clue": "Land surrounded by water"},
         | 
| 1390 | 
            -
                            {"word": "VALLEY", "clue": "Low area between hills"},
         | 
| 1391 | 
            -
                            {"word": "LAKE", "clue": "Inland water body"},
         | 
| 1392 | 
            -
                            {"word": "COAST", "clue": "Land by the sea"},
         | 
| 1393 | 
            -
                            {"word": "PLAIN", "clue": "Flat land"},
         | 
| 1394 | 
            -
                            {"word": "HILL", "clue": "Small elevation"},
         | 
| 1395 | 
            -
                            {"word": "CLIFF", "clue": "Steep rock face"}
         | 
| 1396 | 
             
                        ]
         | 
| 1397 | 
             
                    }
         | 
| 1398 |  | 
| @@ -1441,4 +1444,4 @@ class VectorSearchService: | |
| 1441 | 
             
                        del self.faiss_index
         | 
| 1442 | 
             
                    if self.cache_manager:
         | 
| 1443 | 
             
                        await self.cache_manager.cleanup_expired_caches()
         | 
| 1444 | 
            -
                    self.is_initialized = False
         | 
|  | |
| 3 | 
             
            This implements true AI word generation via vector space nearest neighbor search.
         | 
| 4 | 
             
            """
         | 
| 5 |  | 
| 6 | 
            +
            from math import log
         | 
| 7 | 
             
            import os
         | 
| 8 | 
             
            import logging
         | 
| 9 | 
             
            import asyncio
         | 
|  | |
| 22 |  | 
| 23 | 
             
            logger = logging.getLogger(__name__)
         | 
| 24 |  | 
| 25 | 
            +
            # All logging now uses standard logger with filename/line numbers
         | 
|  | |
|  | |
|  | |
| 26 |  | 
| 27 | 
             
            class VectorSearchService:
         | 
| 28 | 
             
                """
         | 
|  | |
| 68 | 
             
                        start_time = time.time()
         | 
| 69 |  | 
| 70 | 
             
                        # Log environment configuration for debugging
         | 
| 71 | 
            +
                        logger.info(f"🔧 Environment Configuration:")
         | 
| 72 | 
            +
                        logger.info(f"   📊 Model: {self.model_name}")
         | 
| 73 | 
            +
                        logger.info(f"   🎯 Base Similarity Threshold: {self.base_similarity_threshold}")
         | 
| 74 | 
            +
                        logger.info(f"   📉 Min Similarity Threshold: {self.min_similarity_threshold}")
         | 
| 75 | 
            +
                        logger.info(f"   📈 Max Results: {self.max_results}")
         | 
| 76 | 
            +
                        logger.info(f"   🌟 Hierarchical Search: {self.use_hierarchical_search}")
         | 
| 77 | 
            +
                        logger.info(f"   🔀 Search Randomness: {os.getenv('SEARCH_RANDOMNESS', '0.02')}")
         | 
| 78 | 
            +
                        logger.info(f"   💾 Cache Dir: {os.getenv('WORD_CACHE_DIR', 'auto-detect')}")
         | 
| 79 |  | 
| 80 | 
            +
                        logger.info(f"🔧 Loading model: {self.model_name}")
         | 
| 81 |  | 
| 82 | 
             
                        # Load sentence transformer model
         | 
| 83 | 
             
                        model_start = time.time()
         | 
| 84 | 
             
                        self.model = SentenceTransformer(self.model_name)
         | 
| 85 | 
             
                        model_time = time.time() - model_start
         | 
| 86 | 
            +
                        logger.info(f"✅ Model loaded in {model_time:.2f}s: {self.model_name}")
         | 
| 87 |  | 
| 88 | 
             
                        # Try to load from cache first
         | 
| 89 | 
             
                        if self._load_cached_index():
         | 
| 90 | 
            +
                            logger.info("🚀 Using cached FAISS index - startup accelerated!")
         | 
| 91 | 
             
                        else:
         | 
| 92 | 
             
                            # Build from scratch
         | 
| 93 | 
            +
                            logger.info("🔨 Building FAISS index from scratch...")
         | 
| 94 |  | 
| 95 | 
             
                            # Get model vocabulary from tokenizer
         | 
| 96 | 
             
                            vocab_start = time.time()
         | 
|  | |
| 100 | 
             
                            # Filter vocabulary for crossword-suitable words
         | 
| 101 | 
             
                            self.vocab = self._filter_vocabulary(vocab_dict)
         | 
| 102 | 
             
                            vocab_time = time.time() - vocab_start
         | 
| 103 | 
            +
                            logger.info(f"📚 Filtered vocabulary in {vocab_time:.2f}s: {len(self.vocab)} words")
         | 
| 104 |  | 
| 105 | 
             
                            # Pre-compute embeddings for all vocabulary words
         | 
| 106 | 
             
                            embedding_start = time.time()
         | 
| 107 | 
            +
                            logger.info("🔄 Starting embedding generation...")
         | 
| 108 | 
             
                            await self._build_embeddings_index()
         | 
| 109 | 
             
                            embedding_time = time.time() - embedding_start
         | 
| 110 | 
            +
                            logger.info(f"🔄 Embeddings built in {embedding_time:.2f}s")
         | 
| 111 |  | 
| 112 | 
             
                            # Save to cache for next time
         | 
| 113 | 
             
                            self._save_index_to_cache()
         | 
| 114 |  | 
| 115 | 
             
                        # Initialize cache manager
         | 
| 116 | 
             
                        cache_start = time.time()
         | 
| 117 | 
            +
                        logger.info("📦 Initializing word cache manager...")
         | 
| 118 | 
             
                        try:
         | 
| 119 | 
             
                            from .word_cache import WordCacheManager
         | 
| 120 | 
             
                            self.cache_manager = WordCacheManager()
         | 
| 121 | 
             
                            await self.cache_manager.initialize()
         | 
| 122 | 
             
                            cache_time = time.time() - cache_start
         | 
| 123 | 
            +
                            logger.info(f"📦 Cache manager initialized in {cache_time:.2f}s")
         | 
| 124 | 
             
                        except Exception as e:
         | 
| 125 | 
             
                            cache_time = time.time() - cache_start
         | 
| 126 | 
            +
                            logger.info(f"⚠️ Cache manager initialization failed in {cache_time:.2f}s: {e}")
         | 
| 127 | 
            +
                            logger.info("📝 Continuing without persistent caching (in-memory only)")
         | 
| 128 | 
             
                            self.cache_manager = None
         | 
| 129 |  | 
| 130 | 
             
                        self.is_initialized = True
         | 
| 131 | 
             
                        total_time = time.time() - start_time
         | 
| 132 | 
            +
                        logger.info(f"✅ Vector search service fully initialized in {total_time:.2f}s")
         | 
| 133 |  | 
| 134 | 
             
                    except Exception as e:
         | 
| 135 | 
             
                        logger.error(f"❌ Failed to initialize vector search: {e}")
         | 
|  | |
| 138 |  | 
| 139 | 
             
                def _filter_vocabulary(self, vocab_dict: Dict[str, int]) -> List[str]:
         | 
| 140 | 
             
                    """Filter vocabulary to keep only crossword-suitable words."""
         | 
| 141 | 
            +
                    logger.info(f"📚 Filtering {len(vocab_dict)} vocabulary words...")
         | 
| 142 |  | 
| 143 | 
             
                    # Pre-compile excluded words set for faster lookup
         | 
| 144 | 
             
                    excluded_words = {
         | 
|  | |
| 157 |  | 
| 158 | 
             
                        # Progress logging for large vocabularies
         | 
| 159 | 
             
                        if processed % 10000 == 0:
         | 
| 160 | 
            +
                            logger.info(f"📊 Vocabulary filtering progress: {processed}/{len(vocab_dict)}")
         | 
| 161 |  | 
| 162 | 
             
                        # Clean word (remove special tokens) - optimized
         | 
| 163 | 
             
                        if word.startswith('##'):
         | 
|  | |
| 189 |  | 
| 190 | 
             
                    # Remove duplicates efficiently and sort
         | 
| 191 | 
             
                    unique_filtered = sorted(list(set(filtered)))
         | 
| 192 | 
            +
                    logger.info(f"📚 Vocabulary filtered: {len(vocab_dict)} → {len(unique_filtered)} words")
         | 
| 193 |  | 
| 194 | 
             
                    return unique_filtered
         | 
| 195 |  | 
|  | |
| 223 | 
             
                    cpu_count = os.cpu_count() or 1
         | 
| 224 | 
             
                    # Larger batches for better throughput, smaller for HF Spaces limited memory
         | 
| 225 | 
             
                    batch_size = min(200 if cpu_count > 2 else 100, len(self.vocab) // 4)
         | 
| 226 | 
            +
                    logger.info(f"⚡ Using batch size {batch_size} with {cpu_count} CPUs")
         | 
| 227 |  | 
| 228 | 
             
                    embeddings_list = []
         | 
| 229 | 
             
                    total_batches = (len(self.vocab) + batch_size - 1) // batch_size
         | 
|  | |
| 247 | 
             
                        # Progress logging - more frequent for slower HF Spaces
         | 
| 248 | 
             
                        if batch_num % max(1, total_batches // 10) == 0:
         | 
| 249 | 
             
                            progress = (batch_num / total_batches) * 100
         | 
| 250 | 
            +
                            logger.info(f"📊 Embedding progress: {progress:.1f}% ({i}/{len(self.vocab)} words)")
         | 
| 251 |  | 
| 252 | 
             
                    # Combine all embeddings
         | 
| 253 | 
            +
                    logger.info("🔗 Combining embeddings...")
         | 
| 254 | 
             
                    self.word_embeddings = np.vstack(embeddings_list)
         | 
| 255 | 
             
                    logger.info(f"📈 Generated embeddings shape: {self.word_embeddings.shape}")
         | 
| 256 |  | 
| 257 | 
             
                    # Build FAISS index for fast similarity search
         | 
| 258 | 
            +
                    logger.info("🏗️ Building FAISS index...")
         | 
| 259 | 
             
                    dimension = self.word_embeddings.shape[1]
         | 
| 260 | 
             
                    self.faiss_index = faiss.IndexFlatIP(dimension)  # Inner product similarity
         | 
| 261 |  | 
| 262 | 
             
                    # Normalize embeddings for cosine similarity
         | 
| 263 | 
            +
                    logger.info("📏 Normalizing embeddings for cosine similarity...")
         | 
| 264 | 
             
                    faiss.normalize_L2(self.word_embeddings)
         | 
| 265 |  | 
| 266 | 
             
                    # Add to FAISS index
         | 
| 267 | 
            +
                    logger.info("📥 Adding embeddings to FAISS index...")
         | 
| 268 | 
             
                    self.faiss_index.add(self.word_embeddings)
         | 
| 269 |  | 
| 270 | 
             
                    logger.info(f"🔍 FAISS index built with {self.faiss_index.ntotal} vectors")
         | 
|  | |
| 314 |  | 
| 315 | 
             
                                # Track these words to prevent future repetition
         | 
| 316 | 
             
                                if similar_words:
         | 
| 317 | 
            +
                                    self._track_used_words(topic, similar_words)
         | 
| 318 |  | 
| 319 | 
             
                                # Cache successful results for future use
         | 
| 320 | 
             
                                if similar_words:
         | 
|  | |
| 336 |  | 
| 337 | 
             
                            # Track these words to prevent future repetition
         | 
| 338 | 
             
                            if similar_words:
         | 
| 339 | 
            +
                                self._track_used_words(topic, similar_words)
         | 
| 340 |  | 
| 341 | 
             
                        # If not enough words found, supplement with cached words (more aggressive)
         | 
| 342 | 
             
                        if len(similar_words) < max_words * 0.75:  # If less than 75% of target, supplement
         | 
|  | |
| 576 | 
             
                                     'urban', 'rural', 'geological', 'topographical', 'cartographic']
         | 
| 577 | 
             
                    }
         | 
| 578 |  | 
| 579 | 
            +
                    # for candidate in candidates[:10]:  # Only consider top 10 for performance
         | 
| 580 | 
            +
                    for candidate in candidates:  # Only consider top 10 for performance
         | 
| 581 | 
             
                        word = candidate['word'].lower()
         | 
| 582 | 
             
                        similarity = candidate['similarity']
         | 
| 583 |  | 
|  | |
| 690 |  | 
| 691 | 
             
                        main_topic_candidates.extend(variation_candidates)
         | 
| 692 |  | 
| 693 | 
            +
                    if len(main_topic_candidates) <= 10:
         | 
| 694 | 
            +
                        logger.info(f"🔍 Main topic search found candidates: {main_topic_candidates}")
         | 
| 695 | 
             
                    logger.info(f"🔍 Main topic search found {len(main_topic_candidates)} candidates")
         | 
| 696 |  | 
| 697 | 
             
                    # Phase 2: Identify subcategories from best candidates
         | 
|  | |
| 920 | 
             
                    max_words: int
         | 
| 921 | 
             
                ) -> List[Dict[str, Any]]:
         | 
| 922 | 
             
                    """
         | 
| 923 | 
            +
                    Balance word selection across different search sources for optimal variety.
         | 
| 924 | 
            +
                    
         | 
| 925 | 
            +
                    Allocates selection quotas to ensure representation from main topic searches
         | 
| 926 | 
            +
                    and subcategory searches, preventing over-concentration from any single source
         | 
| 927 | 
            +
                    while maintaining quality standards.
         | 
| 928 | 
            +
                    
         | 
| 929 | 
            +
                    Args:
         | 
| 930 | 
            +
                        candidates: Word candidates with search source metadata
         | 
| 931 | 
            +
                        max_words: Target number of words to select
         | 
| 932 | 
            +
                        
         | 
| 933 | 
            +
                    Returns:
         | 
| 934 | 
            +
                        Balanced selection ensuring source diversity
         | 
| 935 | 
             
                    """
         | 
| 936 | 
             
                    if len(candidates) <= max_words:
         | 
| 937 | 
             
                        return candidates
         | 
|  | |
| 1059 | 
             
                    """Load FAISS index from cache if available."""
         | 
| 1060 | 
             
                    try:
         | 
| 1061 | 
             
                        if not self._cache_exists():
         | 
| 1062 | 
            +
                            logger.info("📁 No cached index found - will build new index")
         | 
| 1063 | 
             
                            return False
         | 
| 1064 |  | 
| 1065 | 
            +
                        logger.info("📁 Loading cached FAISS index...")
         | 
| 1066 | 
             
                        cache_start = time.time()
         | 
| 1067 |  | 
| 1068 | 
             
                        # Load vocabulary
         | 
| 1069 | 
             
                        with open(self.vocab_cache_path, 'rb') as f:
         | 
| 1070 | 
             
                            self.vocab = pickle.load(f)
         | 
| 1071 | 
            +
                        logger.info(f"📚 Loaded {len(self.vocab)} vocabulary words from cache")
         | 
| 1072 |  | 
| 1073 | 
             
                        # Load embeddings
         | 
| 1074 | 
             
                        self.word_embeddings = np.load(self.embeddings_cache_path)
         | 
| 1075 | 
            +
                        logger.info(f"📈 Loaded embeddings shape: {self.word_embeddings.shape}")
         | 
| 1076 |  | 
| 1077 | 
             
                        # Load FAISS index
         | 
| 1078 | 
             
                        self.faiss_index = faiss.read_index(self.faiss_cache_path)
         | 
| 1079 | 
            +
                        logger.info(f"🔍 Loaded FAISS index with {self.faiss_index.ntotal} vectors")
         | 
| 1080 |  | 
| 1081 | 
             
                        cache_time = time.time() - cache_start
         | 
| 1082 | 
            +
                        logger.info(f"✅ Successfully loaded cached index in {cache_time:.2f}s")
         | 
| 1083 | 
             
                        return True
         | 
| 1084 |  | 
| 1085 | 
             
                    except Exception as e:
         | 
| 1086 | 
            +
                        logger.info(f"❌ Failed to load cached index: {e}")
         | 
| 1087 | 
            +
                        logger.info("🔄 Will rebuild index from scratch")
         | 
| 1088 | 
             
                        return False
         | 
| 1089 |  | 
| 1090 | 
             
                def _save_index_to_cache(self):
         | 
| 1091 | 
             
                    """Save the built FAISS index to cache for future use."""
         | 
| 1092 | 
             
                    try:
         | 
| 1093 | 
            +
                        logger.info("💾 Saving FAISS index to cache...")
         | 
| 1094 | 
             
                        save_start = time.time()
         | 
| 1095 |  | 
| 1096 | 
             
                        # Save vocabulary
         | 
|  | |
| 1104 | 
             
                        faiss.write_index(self.faiss_index, self.faiss_cache_path)
         | 
| 1105 |  | 
| 1106 | 
             
                        save_time = time.time() - save_start
         | 
| 1107 | 
            +
                        logger.info(f"✅ Index cached successfully in {save_time:.2f}s")
         | 
| 1108 | 
            +
                        logger.info(f"📁 Cache location: {self.index_cache_dir}")
         | 
| 1109 |  | 
| 1110 | 
             
                    except Exception as e:
         | 
| 1111 | 
            +
                        logger.info(f"⚠️ Failed to cache index: {e}")
         | 
| 1112 | 
            +
                        logger.info("📝 Continuing without caching (performance will be slower next startup)")
         | 
| 1113 |  | 
| 1114 | 
             
                def _is_topic_relevant(self, word: str, topic: str) -> bool:
         | 
| 1115 | 
             
                    """
         | 
|  | |
| 1194 | 
             
                        # Filter by difficulty and quality
         | 
| 1195 | 
             
                        if self._matches_difficulty(word, difficulty):
         | 
| 1196 | 
             
                            difficulty_passed += 1
         | 
| 1197 | 
            +
                            # if self._is_interesting_word(word, topic) and self._is_topic_relevant(word, topic):
         | 
| 1198 | 
            +
                            interesting_passed += 1
         | 
| 1199 | 
            +
                            candidates.append({
         | 
| 1200 | 
            +
                                "word": word,
         | 
| 1201 | 
            +
                                "clue": self._generate_clue(word, topic),
         | 
| 1202 | 
            +
                                "similarity": float(score),
         | 
| 1203 | 
            +
                                "source": "vector_search"
         | 
| 1204 | 
            +
                            })
         | 
| 1205 | 
            +
                            # else:
         | 
| 1206 | 
            +
                            #     rejected_words.append(f"{word}({score:.3f})")
         | 
| 1207 | 
             
                        else:
         | 
| 1208 | 
             
                            rejected_words.append(f"{word}({score:.3f})")
         | 
| 1209 |  | 
|  | |
| 1353 | 
             
                        "animals": [
         | 
| 1354 | 
             
                            {"word": "DOG", "clue": "Man's best friend"},
         | 
| 1355 | 
             
                            {"word": "CAT", "clue": "Feline pet"},
         | 
|  | |
|  | |
|  | |
| 1356 | 
             
                            {"word": "FISH", "clue": "Aquatic animal"},
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1357 | 
             
                        ],
         | 
| 1358 | 
             
                        "science": [
         | 
| 1359 | 
            +
                            # {"word": "ATOM", "clue": "Basic unit of matter"},
         | 
| 1360 | 
            +
                            # {"word": "CELL", "clue": "Basic unit of life"},
         | 
| 1361 | 
            +
                            # {"word": "DNA", "clue": "Genetic material"},
         | 
| 1362 | 
            +
                            # {"word": "ENERGY", "clue": "Capacity to do work"},
         | 
| 1363 | 
            +
                            # {"word": "FORCE", "clue": "Push or pull"},
         | 
| 1364 | 
            +
                            # {"word": "GRAVITY", "clue": "Force of attraction"},
         | 
| 1365 | 
            +
                            # {"word": "LIGHT", "clue": "Electromagnetic radiation"},
         | 
| 1366 | 
            +
                            # {"word": "MATTER", "clue": "Physical substance"},
         | 
| 1367 | 
            +
                            # {"word": "MOTION", "clue": "Change in position"},
         | 
| 1368 | 
            +
                            # {"word": "OXYGEN", "clue": "Essential gas"},
         | 
| 1369 | 
            +
                            # {"word": "PHYSICS", "clue": "Study of matter and energy"},
         | 
| 1370 | 
            +
                            # {"word": "THEORY", "clue": "Scientific explanation"}
         | 
| 1371 | 
             
                        ],
         | 
| 1372 | 
             
                        "technology": [
         | 
| 1373 | 
            +
                            # {"word": "COMPUTER", "clue": "Electronic device"},
         | 
| 1374 | 
            +
                            # {"word": "INTERNET", "clue": "Global network"},
         | 
| 1375 | 
            +
                            # {"word": "SOFTWARE", "clue": "Computer programs"},
         | 
| 1376 | 
            +
                            # {"word": "ROBOT", "clue": "Automated machine"},
         | 
| 1377 | 
            +
                            # {"word": "DATA", "clue": "Information"},
         | 
| 1378 | 
            +
                            # {"word": "CODE", "clue": "Programming instructions"},
         | 
| 1379 | 
            +
                            # {"word": "DIGITAL", "clue": "Electronic format"},
         | 
| 1380 | 
            +
                            # {"word": "NETWORK", "clue": "Connected systems"},
         | 
| 1381 | 
            +
                            # {"word": "SYSTEM", "clue": "Organized whole"},
         | 
| 1382 | 
            +
                            # {"word": "DEVICE", "clue": "Technical apparatus"},
         | 
| 1383 | 
            +
                            # {"word": "MOBILE", "clue": "Portable technology"},
         | 
| 1384 | 
            +
                            # {"word": "SCREEN", "clue": "Display surface"}
         | 
| 1385 | 
             
                        ],
         | 
| 1386 | 
             
                        "geography": [
         | 
| 1387 | 
            +
                            # {"word": "MOUNTAIN", "clue": "High landform"},
         | 
| 1388 | 
            +
                            # {"word": "RIVER", "clue": "Flowing water"},
         | 
| 1389 | 
            +
                            # {"word": "OCEAN", "clue": "Large body of water"},
         | 
| 1390 | 
            +
                            # {"word": "DESERT", "clue": "Arid region"},
         | 
| 1391 | 
            +
                            # {"word": "FOREST", "clue": "Dense trees"},
         | 
| 1392 | 
            +
                            # {"word": "ISLAND", "clue": "Land surrounded by water"},
         | 
| 1393 | 
            +
                            # {"word": "VALLEY", "clue": "Low area between hills"},
         | 
| 1394 | 
            +
                            # {"word": "LAKE", "clue": "Inland water body"},
         | 
| 1395 | 
            +
                            # {"word": "COAST", "clue": "Land by the sea"},
         | 
| 1396 | 
            +
                            # {"word": "PLAIN", "clue": "Flat land"},
         | 
| 1397 | 
            +
                            # {"word": "HILL", "clue": "Small elevation"},
         | 
| 1398 | 
            +
                            # {"word": "CLIFF", "clue": "Steep rock face"}
         | 
| 1399 | 
             
                        ]
         | 
| 1400 | 
             
                    }
         | 
| 1401 |  | 
|  | |
| 1444 | 
             
                        del self.faiss_index
         | 
| 1445 | 
             
                    if self.cache_manager:
         | 
| 1446 | 
             
                        await self.cache_manager.cleanup_expired_caches()
         | 
| 1447 | 
            +
                    self.is_initialized = False
         | 
    	
        crossword-app/backend-py/src/services/wordnet_clue_generator.py
    ADDED
    
    | @@ -0,0 +1,640 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
            """
         | 
| 3 | 
            +
            WordNet-Based Clue Generator for Crossword Puzzles
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Uses NLTK WordNet to generate crossword clues by analyzing word definitions,
         | 
| 6 | 
            +
            synonyms, hypernyms, and semantic relationships. Integrated with the thematic
         | 
| 7 | 
            +
            word generator for complete crossword creation without API dependencies.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Features:
         | 
| 10 | 
            +
            - WordNet-based clue generation using definitions and relationships
         | 
| 11 | 
            +
            - Integration with UnifiedThematicWordGenerator for word discovery
         | 
| 12 | 
            +
            - Interactive mode with topic-based generation
         | 
| 13 | 
            +
            - Multiple clue styles (definition, synonym, category, descriptive)
         | 
| 14 | 
            +
            - Difficulty-based clue complexity
         | 
| 15 | 
            +
            - Caching for improved performance
         | 
| 16 | 
            +
            """
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            import os
         | 
| 19 | 
            +
            import sys
         | 
| 20 | 
            +
            import re
         | 
| 21 | 
            +
            import time
         | 
| 22 | 
            +
            import logging
         | 
| 23 | 
            +
            from typing import List, Dict, Optional, Tuple, Set, Any
         | 
| 24 | 
            +
            from pathlib import Path
         | 
| 25 | 
            +
            from dataclasses import dataclass
         | 
| 26 | 
            +
            from collections import defaultdict
         | 
| 27 | 
            +
            import random
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            # NLTK imports
         | 
| 30 | 
            +
            try:
         | 
| 31 | 
            +
                import nltk
         | 
| 32 | 
            +
                from nltk.corpus import wordnet as wn
         | 
| 33 | 
            +
                from nltk.stem import WordNetLemmatizer
         | 
| 34 | 
            +
                NLTK_AVAILABLE = True
         | 
| 35 | 
            +
            except ImportError:
         | 
| 36 | 
            +
                print("❌ NLTK not available. Install with: pip install nltk")
         | 
| 37 | 
            +
                NLTK_AVAILABLE = False
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            # Add hack directory to path for imports
         | 
| 40 | 
            +
            sys.path.insert(0, str(Path(__file__).parent))
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            try:
         | 
| 43 | 
            +
                from .thematic_word_service import ThematicWordService as UnifiedThematicWordGenerator
         | 
| 44 | 
            +
                THEMATIC_AVAILABLE = True
         | 
| 45 | 
            +
            except ImportError as e:
         | 
| 46 | 
            +
                print(f"❌ Thematic generator import error: {e}")
         | 
| 47 | 
            +
                THEMATIC_AVAILABLE = False
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            # Set up logging
         | 
| 50 | 
            +
            logging.basicConfig(
         | 
| 51 | 
            +
                level=logging.INFO,
         | 
| 52 | 
            +
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
         | 
| 53 | 
            +
            )
         | 
| 54 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
             | 
| 57 | 
            +
            @dataclass
         | 
| 58 | 
            +
            class WordNetClueEntry:
         | 
| 59 | 
            +
                """Complete crossword entry with WordNet-generated clue and metadata."""
         | 
| 60 | 
            +
                word: str
         | 
| 61 | 
            +
                clue: str
         | 
| 62 | 
            +
                topic: str
         | 
| 63 | 
            +
                similarity_score: float
         | 
| 64 | 
            +
                frequency_tier: str
         | 
| 65 | 
            +
                tier_description: str
         | 
| 66 | 
            +
                clue_type: str  # definition, synonym, hypernym, etc.
         | 
| 67 | 
            +
                synset_info: Optional[str] = None
         | 
| 68 | 
            +
                definition_source: Optional[str] = None
         | 
| 69 | 
            +
             | 
| 70 | 
            +
             | 
| 71 | 
            +
            def ensure_nltk_data(nltk_data_dir: Optional[str] = None):
         | 
| 72 | 
            +
                """Ensure required NLTK data is downloaded to specified directory.
         | 
| 73 | 
            +
                
         | 
| 74 | 
            +
                Args:
         | 
| 75 | 
            +
                    nltk_data_dir: Custom directory for NLTK data. If None, uses default.
         | 
| 76 | 
            +
                """
         | 
| 77 | 
            +
                if not NLTK_AVAILABLE:
         | 
| 78 | 
            +
                    return False
         | 
| 79 | 
            +
                
         | 
| 80 | 
            +
                # Set up custom NLTK data directory
         | 
| 81 | 
            +
                if nltk_data_dir:
         | 
| 82 | 
            +
                    nltk_data_path = Path(nltk_data_dir)
         | 
| 83 | 
            +
                    nltk_data_path.mkdir(parents=True, exist_ok=True)
         | 
| 84 | 
            +
                    
         | 
| 85 | 
            +
                    # Add custom path to NLTK search path (at the beginning for priority)
         | 
| 86 | 
            +
                    if str(nltk_data_path) not in nltk.data.path:
         | 
| 87 | 
            +
                        nltk.data.path.insert(0, str(nltk_data_path))
         | 
| 88 | 
            +
                        logger.info(f"📂 Added NLTK data path: {nltk_data_path}")
         | 
| 89 | 
            +
                
         | 
| 90 | 
            +
                # Map corpus names to their actual directory paths
         | 
| 91 | 
            +
                corpus_paths = {
         | 
| 92 | 
            +
                    'wordnet': 'corpora/wordnet',
         | 
| 93 | 
            +
                    'omw-1.4': 'corpora/omw-1.4', 
         | 
| 94 | 
            +
                    'punkt': 'tokenizers/punkt',
         | 
| 95 | 
            +
                    'averaged_perceptron_tagger': 'taggers/averaged_perceptron_tagger'
         | 
| 96 | 
            +
                }
         | 
| 97 | 
            +
                
         | 
| 98 | 
            +
                required_corpora = ['wordnet', 'punkt', 'averaged_perceptron_tagger', 'omw-1.4']
         | 
| 99 | 
            +
                
         | 
| 100 | 
            +
                for corpus in required_corpora:
         | 
| 101 | 
            +
                    corpus_path = corpus_paths[corpus]
         | 
| 102 | 
            +
                    
         | 
| 103 | 
            +
                    try:
         | 
| 104 | 
            +
                        # Try to find corpus in current search paths
         | 
| 105 | 
            +
                        found_corpus = nltk.data.find(corpus_path)
         | 
| 106 | 
            +
                        logger.info(f"✅ Found {corpus} at: {found_corpus}")
         | 
| 107 | 
            +
                    except LookupError:
         | 
| 108 | 
            +
                        # Check if it exists in our custom directory
         | 
| 109 | 
            +
                        if nltk_data_dir:
         | 
| 110 | 
            +
                            local_corpus_path = Path(nltk_data_dir) / corpus_path
         | 
| 111 | 
            +
                            if local_corpus_path.exists():
         | 
| 112 | 
            +
                                logger.info(f"✅ Found {corpus} locally at: {local_corpus_path}")
         | 
| 113 | 
            +
                                continue
         | 
| 114 | 
            +
                        
         | 
| 115 | 
            +
                        # Only download if not found anywhere
         | 
| 116 | 
            +
                        logger.warning(f"❌ {corpus} not found, attempting download...")
         | 
| 117 | 
            +
                        try:
         | 
| 118 | 
            +
                            if nltk_data_dir:
         | 
| 119 | 
            +
                                # Download to custom directory
         | 
| 120 | 
            +
                                logger.info(f"📥 Downloading {corpus} to: {nltk_data_dir}")
         | 
| 121 | 
            +
                                nltk.download(corpus, download_dir=nltk_data_dir, quiet=False)
         | 
| 122 | 
            +
                                logger.info(f"✅ Downloaded {corpus} to: {nltk_data_dir}")
         | 
| 123 | 
            +
                            else:
         | 
| 124 | 
            +
                                # Download to default directory
         | 
| 125 | 
            +
                                logger.info(f"📥 Downloading {corpus} to default location")
         | 
| 126 | 
            +
                                nltk.download(corpus, quiet=False)
         | 
| 127 | 
            +
                                logger.info(f"✅ Downloaded {corpus} to default location")
         | 
| 128 | 
            +
                        except Exception as e:
         | 
| 129 | 
            +
                            logger.warning(f"⚠️ Failed to download {corpus}: {e}")
         | 
| 130 | 
            +
                            return False
         | 
| 131 | 
            +
                
         | 
| 132 | 
            +
                return True
         | 
| 133 | 
            +
             | 
| 134 | 
            +
             | 
| 135 | 
            +
            class WordNetClueGenerator:
         | 
| 136 | 
            +
                """
         | 
| 137 | 
            +
                WordNet-based clue generator that creates crossword clues using semantic
         | 
| 138 | 
            +
                relationships and definitions from the WordNet lexical database.
         | 
| 139 | 
            +
                """
         | 
| 140 | 
            +
                
         | 
| 141 | 
            +
                def __init__(self, cache_dir: Optional[str] = None):
         | 
| 142 | 
            +
                    """Initialize WordNet clue generator.
         | 
| 143 | 
            +
                    
         | 
| 144 | 
            +
                    Args:
         | 
| 145 | 
            +
                        cache_dir: Directory for caching (used for both model cache and NLTK data)
         | 
| 146 | 
            +
                    """
         | 
| 147 | 
            +
                    self.cache_dir = cache_dir or str(Path(__file__).parent / 'model_cache')
         | 
| 148 | 
            +
                    self.nltk_data_dir = str(Path(self.cache_dir) / 'nltk_data')
         | 
| 149 | 
            +
                    self.lemmatizer = None
         | 
| 150 | 
            +
                    self.clue_cache = {}
         | 
| 151 | 
            +
                    self.is_initialized = False
         | 
| 152 | 
            +
                    
         | 
| 153 | 
            +
                    # Simple clue generation using definition concatenation
         | 
| 154 | 
            +
                    
         | 
| 155 | 
            +
                    # Words to avoid in clues (common words that don't make good clues)
         | 
| 156 | 
            +
                    self.avoid_words = {
         | 
| 157 | 
            +
                        'thing', 'stuff', 'item', 'object', 'entity', 'something', 'anything',
         | 
| 158 | 
            +
                        'person', 'people', 'someone', 'anyone', 'somebody', 'anybody',
         | 
| 159 | 
            +
                        'place', 'location', 'somewhere', 'anywhere', 'area', 'spot',
         | 
| 160 | 
            +
                        'time', 'moment', 'period', 'while', 'when', 'then',
         | 
| 161 | 
            +
                        'way', 'manner', 'method', 'means', 'how', 'what', 'which'
         | 
| 162 | 
            +
                    }
         | 
| 163 | 
            +
                
         | 
| 164 | 
            +
                def initialize(self):
         | 
| 165 | 
            +
                    """Initialize the WordNet clue generator."""
         | 
| 166 | 
            +
                    if self.is_initialized:
         | 
| 167 | 
            +
                        return True
         | 
| 168 | 
            +
                    
         | 
| 169 | 
            +
                    if not NLTK_AVAILABLE:
         | 
| 170 | 
            +
                        logger.error("❌ NLTK not available - cannot initialize WordNet generator")
         | 
| 171 | 
            +
                        return False
         | 
| 172 | 
            +
                    
         | 
| 173 | 
            +
                    logger.info("🚀 Initializing WordNet Clue Generator...")
         | 
| 174 | 
            +
                    logger.info(f"📂 Using cache directory: {self.cache_dir}")
         | 
| 175 | 
            +
                    logger.info(f"📂 Using NLTK data directory: {self.nltk_data_dir}")
         | 
| 176 | 
            +
                    start_time = time.time()
         | 
| 177 | 
            +
                    
         | 
| 178 | 
            +
                    # Ensure NLTK data is available in cache directory
         | 
| 179 | 
            +
                    if not ensure_nltk_data(self.nltk_data_dir):
         | 
| 180 | 
            +
                        logger.error("❌ Failed to download required NLTK data")
         | 
| 181 | 
            +
                        return False
         | 
| 182 | 
            +
                    
         | 
| 183 | 
            +
                    # Initialize lemmatizer
         | 
| 184 | 
            +
                    try:
         | 
| 185 | 
            +
                        self.lemmatizer = WordNetLemmatizer()
         | 
| 186 | 
            +
                        logger.info("✅ WordNet lemmatizer initialized")
         | 
| 187 | 
            +
                    except Exception as e:
         | 
| 188 | 
            +
                        logger.error(f"❌ Failed to initialize lemmatizer: {e}")
         | 
| 189 | 
            +
                        return False
         | 
| 190 | 
            +
                    
         | 
| 191 | 
            +
                    self.is_initialized = True
         | 
| 192 | 
            +
                    init_time = time.time() - start_time
         | 
| 193 | 
            +
                    logger.info(f"✅ WordNet clue generator ready in {init_time:.2f}s")
         | 
| 194 | 
            +
                    
         | 
| 195 | 
            +
                    return True
         | 
| 196 | 
            +
                
         | 
| 197 | 
            +
                def generate_clue(self, word: str, topic: str = "", clue_style: str = "auto", 
         | 
| 198 | 
            +
                                 difficulty: str = "medium") -> str:
         | 
| 199 | 
            +
                    """Generate a crossword clue using WordNet definitions.
         | 
| 200 | 
            +
                    
         | 
| 201 | 
            +
                    Args:
         | 
| 202 | 
            +
                        word: Target word for clue generation
         | 
| 203 | 
            +
                        topic: Topic context (for fallback only)
         | 
| 204 | 
            +
                        clue_style: Ignored - kept for compatibility
         | 
| 205 | 
            +
                        difficulty: Ignored - kept for compatibility
         | 
| 206 | 
            +
                        
         | 
| 207 | 
            +
                    Returns:
         | 
| 208 | 
            +
                        Generated crossword clue
         | 
| 209 | 
            +
                    """
         | 
| 210 | 
            +
                    if not self.is_initialized:
         | 
| 211 | 
            +
                        if not self.initialize():
         | 
| 212 | 
            +
                            return f"Related to {topic}" if topic else "Crossword answer"
         | 
| 213 | 
            +
                    
         | 
| 214 | 
            +
                    word_clean = word.lower().strip()
         | 
| 215 | 
            +
                    
         | 
| 216 | 
            +
                    # Get synsets
         | 
| 217 | 
            +
                    synsets = wn.synsets(word_clean)
         | 
| 218 | 
            +
                    if not synsets:
         | 
| 219 | 
            +
                        return f"Related to {topic}" if topic else "Crossword answer"
         | 
| 220 | 
            +
                    
         | 
| 221 | 
            +
                    # Limit to max 3 synsets, randomly select if more than 3
         | 
| 222 | 
            +
                    if len(synsets) > 3:
         | 
| 223 | 
            +
                        import random
         | 
| 224 | 
            +
                        synsets = random.sample(synsets, 3)
         | 
| 225 | 
            +
                    
         | 
| 226 | 
            +
                    # Get all definitions and filter out those containing the target word
         | 
| 227 | 
            +
                    definitions = []
         | 
| 228 | 
            +
                    word_variants = {
         | 
| 229 | 
            +
                        word_clean,
         | 
| 230 | 
            +
                        word_clean + 's',
         | 
| 231 | 
            +
                        word_clean + 'ing',
         | 
| 232 | 
            +
                        word_clean + 'ed',
         | 
| 233 | 
            +
                        word_clean + 'er',
         | 
| 234 | 
            +
                        word_clean + 'ly'
         | 
| 235 | 
            +
                    }
         | 
| 236 | 
            +
                    
         | 
| 237 | 
            +
                    for syn in synsets:
         | 
| 238 | 
            +
                        definition = syn.definition()
         | 
| 239 | 
            +
                        definition_lower = definition.lower()
         | 
| 240 | 
            +
                        
         | 
| 241 | 
            +
                        # Check if any variant of the target word appears in the definition
         | 
| 242 | 
            +
                        contains_target = False
         | 
| 243 | 
            +
                        for variant in word_variants:
         | 
| 244 | 
            +
                            if f" {variant} " in f" {definition_lower} " or definition_lower.startswith(variant + " "):
         | 
| 245 | 
            +
                                contains_target = True
         | 
| 246 | 
            +
                                break
         | 
| 247 | 
            +
                        
         | 
| 248 | 
            +
                        # Only include definitions that don't contain the target word
         | 
| 249 | 
            +
                        if not contains_target:
         | 
| 250 | 
            +
                            definitions.append(definition)
         | 
| 251 | 
            +
                    
         | 
| 252 | 
            +
                    # If no clean definitions found, return fallback
         | 
| 253 | 
            +
                    if not definitions:
         | 
| 254 | 
            +
                        return f"Related to {topic}" if topic else "Crossword answer"
         | 
| 255 | 
            +
                    
         | 
| 256 | 
            +
                    # Concatenate clean definitions
         | 
| 257 | 
            +
                    clue = "; ".join(definitions)
         | 
| 258 | 
            +
                    
         | 
| 259 | 
            +
                    return clue
         | 
| 260 | 
            +
                
         | 
| 261 | 
            +
                def _generate_fallback_clue(self, word: str, topic: str) -> str:
         | 
| 262 | 
            +
                    """Generate fallback clue when WordNet fails."""
         | 
| 263 | 
            +
                    if topic:
         | 
| 264 | 
            +
                        return f"Related to {topic}"
         | 
| 265 | 
            +
                    return "Crossword answer"
         | 
| 266 | 
            +
                
         | 
| 267 | 
            +
                
         | 
| 268 | 
            +
                def get_clue_info(self, word: str) -> Dict[str, Any]:
         | 
| 269 | 
            +
                    """Get detailed information about WordNet data for a word."""
         | 
| 270 | 
            +
                    if not self.is_initialized:
         | 
| 271 | 
            +
                        return {"error": "Generator not initialized"}
         | 
| 272 | 
            +
                    
         | 
| 273 | 
            +
                    word_clean = word.lower().strip()
         | 
| 274 | 
            +
                    synsets = self._get_synsets(word_clean)
         | 
| 275 | 
            +
                    
         | 
| 276 | 
            +
                    info = {
         | 
| 277 | 
            +
                        "word": word,
         | 
| 278 | 
            +
                        "synsets_count": len(synsets),
         | 
| 279 | 
            +
                        "synsets": []
         | 
| 280 | 
            +
                    }
         | 
| 281 | 
            +
                    
         | 
| 282 | 
            +
                    for synset in synsets[:3]:  # Top 3 synsets
         | 
| 283 | 
            +
                        synset_info = {
         | 
| 284 | 
            +
                            "name": synset.name(),
         | 
| 285 | 
            +
                            "pos": synset.pos(),
         | 
| 286 | 
            +
                            "definition": synset.definition(),
         | 
| 287 | 
            +
                            "examples": synset.examples()[:2],
         | 
| 288 | 
            +
                            "hypernyms": [h.name() for h in synset.hypernyms()[:2]],
         | 
| 289 | 
            +
                            "synonyms": [l.name().replace('_', ' ') for l in synset.lemmas()[:3]]
         | 
| 290 | 
            +
                        }
         | 
| 291 | 
            +
                        info["synsets"].append(synset_info)
         | 
| 292 | 
            +
                    
         | 
| 293 | 
            +
                    return info
         | 
| 294 | 
            +
             | 
| 295 | 
            +
             | 
| 296 | 
            +
            class IntegratedWordNetCrosswordGenerator:
         | 
| 297 | 
            +
                """
         | 
| 298 | 
            +
                Complete crossword generation system using WordNet clues and thematic word discovery.
         | 
| 299 | 
            +
                """
         | 
| 300 | 
            +
                
         | 
| 301 | 
            +
                def __init__(self, vocab_size_limit: Optional[int] = None, cache_dir: Optional[str] = None):
         | 
| 302 | 
            +
                    """Initialize the integrated WordNet crossword generator.
         | 
| 303 | 
            +
                    
         | 
| 304 | 
            +
                    Args:
         | 
| 305 | 
            +
                        vocab_size_limit: Maximum vocabulary size for thematic generator
         | 
| 306 | 
            +
                        cache_dir: Cache directory for models and data
         | 
| 307 | 
            +
                    """
         | 
| 308 | 
            +
                    self.cache_dir = cache_dir or str(Path(__file__).parent / 'model_cache')
         | 
| 309 | 
            +
                    self.vocab_size_limit = vocab_size_limit or 50000
         | 
| 310 | 
            +
                    
         | 
| 311 | 
            +
                    # Initialize components
         | 
| 312 | 
            +
                    self.thematic_generator = None
         | 
| 313 | 
            +
                    self.clue_generator = None
         | 
| 314 | 
            +
                    self.is_initialized = False
         | 
| 315 | 
            +
                    
         | 
| 316 | 
            +
                    # Stats
         | 
| 317 | 
            +
                    self.stats = {
         | 
| 318 | 
            +
                        'words_discovered': 0,
         | 
| 319 | 
            +
                        'clues_generated': 0,
         | 
| 320 | 
            +
                        'cache_hits': 0,
         | 
| 321 | 
            +
                        'total_time': 0.0
         | 
| 322 | 
            +
                    }
         | 
| 323 | 
            +
                
         | 
| 324 | 
            +
                def initialize(self):
         | 
| 325 | 
            +
                    """Initialize both generators."""
         | 
| 326 | 
            +
                    if self.is_initialized:
         | 
| 327 | 
            +
                        return True
         | 
| 328 | 
            +
                    
         | 
| 329 | 
            +
                    start_time = time.time()
         | 
| 330 | 
            +
                    logger.info("🚀 Initializing Integrated WordNet Crossword Generator...")
         | 
| 331 | 
            +
                    
         | 
| 332 | 
            +
                    success = True
         | 
| 333 | 
            +
                    
         | 
| 334 | 
            +
                    # Initialize WordNet clue generator with consistent cache directory
         | 
| 335 | 
            +
                    logger.info("🔄 Initializing WordNet clue generator...")
         | 
| 336 | 
            +
                    self.clue_generator = WordNetClueGenerator(self.cache_dir)
         | 
| 337 | 
            +
                    if not self.clue_generator.initialize():
         | 
| 338 | 
            +
                        logger.error("❌ Failed to initialize WordNet clue generator")
         | 
| 339 | 
            +
                        success = False
         | 
| 340 | 
            +
                    else:
         | 
| 341 | 
            +
                        logger.info("✅ WordNet clue generator ready")
         | 
| 342 | 
            +
                        logger.info(f"📂 NLTK data stored in: {self.clue_generator.nltk_data_dir}")
         | 
| 343 | 
            +
                    
         | 
| 344 | 
            +
                    # Initialize thematic word generator
         | 
| 345 | 
            +
                    if THEMATIC_AVAILABLE:
         | 
| 346 | 
            +
                        logger.info("🔄 Initializing thematic word generator...")
         | 
| 347 | 
            +
                        try:
         | 
| 348 | 
            +
                            self.thematic_generator = UnifiedThematicWordGenerator(
         | 
| 349 | 
            +
                                cache_dir=self.cache_dir,
         | 
| 350 | 
            +
                                vocab_size_limit=self.vocab_size_limit
         | 
| 351 | 
            +
                            )
         | 
| 352 | 
            +
                            self.thematic_generator.initialize()
         | 
| 353 | 
            +
                            logger.info(f"✅ Thematic generator ready ({self.thematic_generator.get_vocabulary_size():,} words)")
         | 
| 354 | 
            +
                        except Exception as e:
         | 
| 355 | 
            +
                            logger.error(f"❌ Failed to initialize thematic generator: {e}")
         | 
| 356 | 
            +
                            success = False
         | 
| 357 | 
            +
                    else:
         | 
| 358 | 
            +
                        logger.warning("⚠️ Thematic generator not available - limited word discovery")
         | 
| 359 | 
            +
                    
         | 
| 360 | 
            +
                    self.is_initialized = success
         | 
| 361 | 
            +
                    init_time = time.time() - start_time
         | 
| 362 | 
            +
                    logger.info(f"{'✅' if success else '❌'} Initialization {'completed' if success else 'failed'} in {init_time:.2f}s")
         | 
| 363 | 
            +
                    
         | 
| 364 | 
            +
                    return success
         | 
| 365 | 
            +
                
         | 
| 366 | 
            +
                def generate_crossword_entries(self, topic: str, num_words: int = 15, 
         | 
| 367 | 
            +
                                             difficulty: str = "medium", clue_style: str = "auto") -> List[WordNetClueEntry]:
         | 
| 368 | 
            +
                    """Generate complete crossword entries for a topic.
         | 
| 369 | 
            +
                    
         | 
| 370 | 
            +
                    Args:
         | 
| 371 | 
            +
                        topic: Topic for word generation
         | 
| 372 | 
            +
                        num_words: Number of entries to generate
         | 
| 373 | 
            +
                        difficulty: Difficulty level ('easy', 'medium', 'hard')
         | 
| 374 | 
            +
                        clue_style: Clue generation style
         | 
| 375 | 
            +
                        
         | 
| 376 | 
            +
                    Returns:
         | 
| 377 | 
            +
                        List of WordNetClueEntry objects
         | 
| 378 | 
            +
                    """
         | 
| 379 | 
            +
                    if not self.is_initialized:
         | 
| 380 | 
            +
                        if not self.initialize():
         | 
| 381 | 
            +
                            return []
         | 
| 382 | 
            +
                    
         | 
| 383 | 
            +
                    start_time = time.time()
         | 
| 384 | 
            +
                    logger.info(f"🎯 Generating {num_words} crossword entries for '{topic}' (difficulty: {difficulty})")
         | 
| 385 | 
            +
                    
         | 
| 386 | 
            +
                    # Generate thematic words
         | 
| 387 | 
            +
                    if self.thematic_generator:
         | 
| 388 | 
            +
                        try:
         | 
| 389 | 
            +
                            # Get more words than needed for better selection
         | 
| 390 | 
            +
                            word_results = self.thematic_generator.generate_thematic_words(
         | 
| 391 | 
            +
                                inputs=topic,
         | 
| 392 | 
            +
                                num_words=num_words * 2,
         | 
| 393 | 
            +
                                min_similarity=0.2
         | 
| 394 | 
            +
                            )
         | 
| 395 | 
            +
                            self.stats['words_discovered'] += len(word_results)
         | 
| 396 | 
            +
                        except Exception as e:
         | 
| 397 | 
            +
                            logger.error(f"❌ Word generation failed: {e}")
         | 
| 398 | 
            +
                            word_results = []
         | 
| 399 | 
            +
                    else:
         | 
| 400 | 
            +
                        # Fallback: use some common words related to topic
         | 
| 401 | 
            +
                        word_results = [(topic.upper(), 0.9, "tier_5_common")]
         | 
| 402 | 
            +
                    
         | 
| 403 | 
            +
                    if not word_results:
         | 
| 404 | 
            +
                        logger.warning(f"⚠️ No words found for topic '{topic}'")
         | 
| 405 | 
            +
                        return []
         | 
| 406 | 
            +
                    
         | 
| 407 | 
            +
                    # Generate clues for words
         | 
| 408 | 
            +
                    entries = []
         | 
| 409 | 
            +
                    for word, similarity, tier in word_results[:num_words]:
         | 
| 410 | 
            +
                        try:
         | 
| 411 | 
            +
                            clue = self.clue_generator.generate_clue(
         | 
| 412 | 
            +
                                word=word,
         | 
| 413 | 
            +
                                topic=topic,
         | 
| 414 | 
            +
                                clue_style=clue_style,
         | 
| 415 | 
            +
                                difficulty=difficulty
         | 
| 416 | 
            +
                            )
         | 
| 417 | 
            +
                            
         | 
| 418 | 
            +
                            if clue:
         | 
| 419 | 
            +
                                tier_desc = self._get_tier_description(tier)
         | 
| 420 | 
            +
                                entry = WordNetClueEntry(
         | 
| 421 | 
            +
                                    word=word.upper(),
         | 
| 422 | 
            +
                                    clue=clue,
         | 
| 423 | 
            +
                                    topic=topic,
         | 
| 424 | 
            +
                                    similarity_score=similarity,
         | 
| 425 | 
            +
                                    frequency_tier=tier,
         | 
| 426 | 
            +
                                    tier_description=tier_desc,
         | 
| 427 | 
            +
                                    clue_type=clue_style
         | 
| 428 | 
            +
                                )
         | 
| 429 | 
            +
                                entries.append(entry)
         | 
| 430 | 
            +
                                self.stats['clues_generated'] += 1
         | 
| 431 | 
            +
                            
         | 
| 432 | 
            +
                        except Exception as e:
         | 
| 433 | 
            +
                            logger.error(f"❌ Failed to generate clue for '{word}': {e}")
         | 
| 434 | 
            +
                    
         | 
| 435 | 
            +
                    # Sort by similarity and limit results
         | 
| 436 | 
            +
                    entries.sort(key=lambda x: x.similarity_score, reverse=True)
         | 
| 437 | 
            +
                    final_entries = entries[:num_words]
         | 
| 438 | 
            +
                    
         | 
| 439 | 
            +
                    total_time = time.time() - start_time
         | 
| 440 | 
            +
                    self.stats['total_time'] += total_time
         | 
| 441 | 
            +
                    
         | 
| 442 | 
            +
                    logger.info(f"✅ Generated {len(final_entries)} entries in {total_time:.2f}s")
         | 
| 443 | 
            +
                    return final_entries
         | 
| 444 | 
            +
                
         | 
| 445 | 
            +
                def _get_tier_description(self, tier: str) -> str:
         | 
| 446 | 
            +
                    """Get tier description from thematic generator or provide default."""
         | 
| 447 | 
            +
                    if self.thematic_generator and hasattr(self.thematic_generator, 'tier_descriptions'):
         | 
| 448 | 
            +
                        return self.thematic_generator.tier_descriptions.get(tier, tier)
         | 
| 449 | 
            +
                    return tier.replace('_', ' ').title()
         | 
| 450 | 
            +
                
         | 
| 451 | 
            +
                def get_stats(self) -> Dict[str, Any]:
         | 
| 452 | 
            +
                    """Get generation statistics."""
         | 
| 453 | 
            +
                    return {
         | 
| 454 | 
            +
                        **self.stats,
         | 
| 455 | 
            +
                        'thematic_available': self.thematic_generator is not None,
         | 
| 456 | 
            +
                        'wordnet_available': self.clue_generator is not None and self.clue_generator.is_initialized,
         | 
| 457 | 
            +
                        'vocab_size': self.thematic_generator.get_vocabulary_size() if self.thematic_generator else 0
         | 
| 458 | 
            +
                    }
         | 
| 459 | 
            +
             | 
| 460 | 
            +
             | 
| 461 | 
            +
            def main():
         | 
| 462 | 
            +
                """Interactive WordNet crossword generator."""
         | 
| 463 | 
            +
                if not NLTK_AVAILABLE:
         | 
| 464 | 
            +
                    print("❌ NLTK not available. Please install with: pip install nltk")
         | 
| 465 | 
            +
                    return
         | 
| 466 | 
            +
                
         | 
| 467 | 
            +
                print("🚀 WordNet Crossword Generator")
         | 
| 468 | 
            +
                print("=" * 60)
         | 
| 469 | 
            +
                print("Using NLTK WordNet for clue generation + thematic word discovery")
         | 
| 470 | 
            +
                
         | 
| 471 | 
            +
                # Initialize generator
         | 
| 472 | 
            +
                cache_dir = str(Path(__file__).parent / 'model_cache')
         | 
| 473 | 
            +
                generator = IntegratedWordNetCrosswordGenerator(
         | 
| 474 | 
            +
                    vocab_size_limit=50000,
         | 
| 475 | 
            +
                    cache_dir=cache_dir
         | 
| 476 | 
            +
                )
         | 
| 477 | 
            +
                
         | 
| 478 | 
            +
                print("\n🔄 Initializing system...")
         | 
| 479 | 
            +
                if not generator.initialize():
         | 
| 480 | 
            +
                    print("❌ Failed to initialize system")
         | 
| 481 | 
            +
                    return
         | 
| 482 | 
            +
                
         | 
| 483 | 
            +
                stats = generator.get_stats()
         | 
| 484 | 
            +
                print(f"\n📊 System Status:")
         | 
| 485 | 
            +
                print(f"   WordNet clues: {'✅' if stats['wordnet_available'] else '❌'}")
         | 
| 486 | 
            +
                print(f"   Thematic words: {'✅' if stats['thematic_available'] else '❌'}")
         | 
| 487 | 
            +
                if stats['vocab_size'] > 0:
         | 
| 488 | 
            +
                    print(f"   Vocabulary: {stats['vocab_size']:,} words")
         | 
| 489 | 
            +
                
         | 
| 490 | 
            +
                print(f"\n🎮 INTERACTIVE MODE")
         | 
| 491 | 
            +
                print("=" * 60)
         | 
| 492 | 
            +
                print("Commands:")
         | 
| 493 | 
            +
                print("  <topic>                    - Generate words and clues for topic")
         | 
| 494 | 
            +
                print("  <topic> <num_words>        - Generate specific number of entries")
         | 
| 495 | 
            +
                print("  <topic> <num_words> <diff> - Set difficulty (easy/medium/hard)")
         | 
| 496 | 
            +
                print("  <topic> style <style>      - Set clue style (definition/synonym/hypernym/category)")
         | 
| 497 | 
            +
                print("  info <word>                - Show WordNet information for word")
         | 
| 498 | 
            +
                print("  test <word> <topic>        - Test clue generation for specific word")
         | 
| 499 | 
            +
                print("  stats                      - Show generation statistics")
         | 
| 500 | 
            +
                print("  help                       - Show this help")
         | 
| 501 | 
            +
                print("  quit                       - Exit")
         | 
| 502 | 
            +
                print()
         | 
| 503 | 
            +
                print("Examples:")
         | 
| 504 | 
            +
                print("  animals                    - Generate animal-related crossword entries")
         | 
| 505 | 
            +
                print("  technology 10 hard         - 10 hard technology entries")
         | 
| 506 | 
            +
                print("  music style synonym        - Music entries with synonym-style clues")
         | 
| 507 | 
            +
                print("  info elephant              - WordNet info for 'elephant'")
         | 
| 508 | 
            +
                
         | 
| 509 | 
            +
                while True:
         | 
| 510 | 
            +
                    try:
         | 
| 511 | 
            +
                        user_input = input("\n🎯 Enter command: ").strip()
         | 
| 512 | 
            +
                        
         | 
| 513 | 
            +
                        if user_input.lower() in ['quit', 'exit', 'q']:
         | 
| 514 | 
            +
                            break
         | 
| 515 | 
            +
                        
         | 
| 516 | 
            +
                        if not user_input:
         | 
| 517 | 
            +
                            continue
         | 
| 518 | 
            +
                        
         | 
| 519 | 
            +
                        parts = user_input.split()
         | 
| 520 | 
            +
                        
         | 
| 521 | 
            +
                        if user_input.lower() == 'help':
         | 
| 522 | 
            +
                            print("\nCommands:")
         | 
| 523 | 
            +
                            print("  <topic> [num_words] [difficulty] - Generate crossword entries")
         | 
| 524 | 
            +
                            print("  <topic> style <clue_style>        - Generate with specific clue style")
         | 
| 525 | 
            +
                            print("  info <word>                       - Show WordNet info for word")
         | 
| 526 | 
            +
                            print("  test <word> <topic>               - Test clue generation")
         | 
| 527 | 
            +
                            print("  stats                             - Show statistics")
         | 
| 528 | 
            +
                            print("  quit                              - Exit")
         | 
| 529 | 
            +
                            continue
         | 
| 530 | 
            +
                        
         | 
| 531 | 
            +
                        elif user_input.lower() == 'stats':
         | 
| 532 | 
            +
                            stats = generator.get_stats()
         | 
| 533 | 
            +
                            print("\n📊 Generation Statistics:")
         | 
| 534 | 
            +
                            print(f"   Words discovered: {stats['words_discovered']}")
         | 
| 535 | 
            +
                            print(f"   Clues generated: {stats['clues_generated']}")
         | 
| 536 | 
            +
                            print(f"   Total time: {stats['total_time']:.2f}s")
         | 
| 537 | 
            +
                            if stats['clues_generated'] > 0:
         | 
| 538 | 
            +
                                avg_time = stats['total_time'] / stats['clues_generated']
         | 
| 539 | 
            +
                                print(f"   Avg time per clue: {avg_time:.2f}s")
         | 
| 540 | 
            +
                            continue
         | 
| 541 | 
            +
                        
         | 
| 542 | 
            +
                        elif parts[0].lower() == 'info' and len(parts) > 1:
         | 
| 543 | 
            +
                            word = parts[1]
         | 
| 544 | 
            +
                            print(f"\n📝 WordNet Information: '{word}'")
         | 
| 545 | 
            +
                            info = generator.clue_generator.get_clue_info(word)
         | 
| 546 | 
            +
                            
         | 
| 547 | 
            +
                            if 'error' in info:
         | 
| 548 | 
            +
                                print(f"   ❌ {info['error']}")
         | 
| 549 | 
            +
                            else:
         | 
| 550 | 
            +
                                print(f"   Synsets found: {info['synsets_count']}")
         | 
| 551 | 
            +
                                for i, synset in enumerate(info['synsets'], 1):
         | 
| 552 | 
            +
                                    print(f"\n   {i}. {synset['name']} ({synset['pos']})")
         | 
| 553 | 
            +
                                    print(f"      Definition: {synset['definition']}")
         | 
| 554 | 
            +
                                    if synset['examples']:
         | 
| 555 | 
            +
                                        print(f"      Examples: {', '.join(synset['examples'])}")
         | 
| 556 | 
            +
                                    if synset['synonyms']:
         | 
| 557 | 
            +
                                        print(f"      Synonyms: {', '.join(synset['synonyms'])}")
         | 
| 558 | 
            +
                                    if synset['hypernyms']:
         | 
| 559 | 
            +
                                        print(f"      Categories: {', '.join(synset['hypernyms'])}")
         | 
| 560 | 
            +
                            continue
         | 
| 561 | 
            +
                        
         | 
| 562 | 
            +
                        elif parts[0].lower() == 'test' and len(parts) >= 3:
         | 
| 563 | 
            +
                            word = parts[1]
         | 
| 564 | 
            +
                            topic = parts[2]
         | 
| 565 | 
            +
                            print(f"\n🧪 Testing clue generation: '{word}' + '{topic}'")
         | 
| 566 | 
            +
                            
         | 
| 567 | 
            +
                            styles = ['definition', 'synonym', 'hypernym', 'category', 'descriptive']
         | 
| 568 | 
            +
                            for style in styles:
         | 
| 569 | 
            +
                                clue = generator.clue_generator.generate_clue(word, topic, style, 'medium')
         | 
| 570 | 
            +
                                print(f"   {style:12}: {clue if clue else '(no clue generated)'}")
         | 
| 571 | 
            +
                            continue
         | 
| 572 | 
            +
                        
         | 
| 573 | 
            +
                        # Parse generation command
         | 
| 574 | 
            +
                        topic = parts[0]
         | 
| 575 | 
            +
                        num_words = 8
         | 
| 576 | 
            +
                        difficulty = 'medium'
         | 
| 577 | 
            +
                        clue_style = 'auto'
         | 
| 578 | 
            +
                        
         | 
| 579 | 
            +
                        # Parse additional parameters
         | 
| 580 | 
            +
                        i = 1
         | 
| 581 | 
            +
                        while i < len(parts):
         | 
| 582 | 
            +
                            if parts[i].isdigit():
         | 
| 583 | 
            +
                                num_words = int(parts[i])
         | 
| 584 | 
            +
                            elif parts[i].lower() in ['easy', 'medium', 'hard']:
         | 
| 585 | 
            +
                                difficulty = parts[i].lower()
         | 
| 586 | 
            +
                            elif parts[i].lower() == 'style' and i + 1 < len(parts):
         | 
| 587 | 
            +
                                clue_style = parts[i + 1].lower()
         | 
| 588 | 
            +
                                i += 1
         | 
| 589 | 
            +
                            elif parts[i].lower() in ['definition', 'synonym', 'hypernym', 'category', 'descriptive']:
         | 
| 590 | 
            +
                                clue_style = parts[i].lower()
         | 
| 591 | 
            +
                            i += 1
         | 
| 592 | 
            +
                        
         | 
| 593 | 
            +
                        print(f"\n🎯 Generating {num_words} {difficulty} entries for '{topic}'" +
         | 
| 594 | 
            +
                              (f" (style: {clue_style})" if clue_style != 'auto' else ""))
         | 
| 595 | 
            +
                        print("-" * 60)
         | 
| 596 | 
            +
                        
         | 
| 597 | 
            +
                        try:
         | 
| 598 | 
            +
                            start_time = time.time()
         | 
| 599 | 
            +
                            entries = generator.generate_crossword_entries(
         | 
| 600 | 
            +
                                topic=topic,
         | 
| 601 | 
            +
                                num_words=num_words,
         | 
| 602 | 
            +
                                difficulty=difficulty,
         | 
| 603 | 
            +
                                clue_style=clue_style
         | 
| 604 | 
            +
                            )
         | 
| 605 | 
            +
                            generation_time = time.time() - start_time
         | 
| 606 | 
            +
                            
         | 
| 607 | 
            +
                            if entries:
         | 
| 608 | 
            +
                                print(f"✅ Generated {len(entries)} entries in {generation_time:.2f}s:")
         | 
| 609 | 
            +
                                print()
         | 
| 610 | 
            +
                                
         | 
| 611 | 
            +
                                for i, entry in enumerate(entries, 1):
         | 
| 612 | 
            +
                                    tier_short = entry.frequency_tier.split('_')[1] if '_' in entry.frequency_tier else 'unk'
         | 
| 613 | 
            +
                                    print(f"  {i:2}. {entry.word:<12} | {entry.clue}")
         | 
| 614 | 
            +
                                    print(f"      Similarity: {entry.similarity_score:.3f} | Tier: {tier_short} | Type: {entry.clue_type}")
         | 
| 615 | 
            +
                                    print()
         | 
| 616 | 
            +
                            else:
         | 
| 617 | 
            +
                                print("❌ No entries generated. Try a different topic.")
         | 
| 618 | 
            +
                            
         | 
| 619 | 
            +
                        except Exception as e:
         | 
| 620 | 
            +
                            print(f"❌ Error: {e}")
         | 
| 621 | 
            +
                    
         | 
| 622 | 
            +
                    except KeyboardInterrupt:
         | 
| 623 | 
            +
                        print("\n\n👋 Exiting WordNet crossword generator")
         | 
| 624 | 
            +
                        break
         | 
| 625 | 
            +
                    except Exception as e:
         | 
| 626 | 
            +
                        print(f"❌ Error: {e}")
         | 
| 627 | 
            +
                
         | 
| 628 | 
            +
                # Show final stats
         | 
| 629 | 
            +
                final_stats = generator.get_stats()
         | 
| 630 | 
            +
                if final_stats['clues_generated'] > 0:
         | 
| 631 | 
            +
                    print(f"\n📊 Session Summary:")
         | 
| 632 | 
            +
                    print(f"   Entries generated: {final_stats['clues_generated']}")
         | 
| 633 | 
            +
                    print(f"   Total time: {final_stats['total_time']:.2f}s")
         | 
| 634 | 
            +
                    print(f"   Average per entry: {final_stats['total_time']/final_stats['clues_generated']:.2f}s")
         | 
| 635 | 
            +
                
         | 
| 636 | 
            +
                print("\n✅ Thanks for using WordNet Crossword Generator!")
         | 
| 637 | 
            +
             | 
| 638 | 
            +
             | 
| 639 | 
            +
            if __name__ == "__main__":
         | 
| 640 | 
            +
                main()
         | 
    	
        crossword-app/backend-py/test-integration/test_boundary_fix.py
    DELETED
    
    | @@ -1,147 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            import sys
         | 
| 4 | 
            -
            import asyncio
         | 
| 5 | 
            -
            from pathlib import Path
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            # Add project root to path
         | 
| 8 | 
            -
            project_root = Path(__file__).parent.parent  # Go up from test-integration to backend-py
         | 
| 9 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 10 | 
            -
             | 
| 11 | 
            -
            from src.services.crossword_generator import CrosswordGenerator
         | 
| 12 | 
            -
             | 
| 13 | 
            -
            async def test_boundary_fix():
         | 
| 14 | 
            -
                """Test that the boundary fix works correctly."""
         | 
| 15 | 
            -
                
         | 
| 16 | 
            -
                # Sample words that are known to cause boundary issues
         | 
| 17 | 
            -
                test_words = [
         | 
| 18 | 
            -
                    {"word": "COMPUTER", "clue": "Electronic device"},
         | 
| 19 | 
            -
                    {"word": "MACHINE", "clue": "Device with moving parts"},
         | 
| 20 | 
            -
                    {"word": "SCIENCE", "clue": "Systematic study"},
         | 
| 21 | 
            -
                    {"word": "EXPERT", "clue": "Specialist"},
         | 
| 22 | 
            -
                    {"word": "CODE", "clue": "Programming text"},
         | 
| 23 | 
            -
                    {"word": "DATA", "clue": "Information"}
         | 
| 24 | 
            -
                ]
         | 
| 25 | 
            -
                
         | 
| 26 | 
            -
                generator = CrosswordGenerator()
         | 
| 27 | 
            -
                
         | 
| 28 | 
            -
                print("🧪 Testing Boundary Fix")
         | 
| 29 | 
            -
                print("=" * 50)
         | 
| 30 | 
            -
                
         | 
| 31 | 
            -
                # Generate a crossword
         | 
| 32 | 
            -
                result = generator._create_grid(test_words)
         | 
| 33 | 
            -
                
         | 
| 34 | 
            -
                if not result:
         | 
| 35 | 
            -
                    print("❌ Grid generation failed")
         | 
| 36 | 
            -
                    return False
         | 
| 37 | 
            -
                
         | 
| 38 | 
            -
                grid = result["grid"]
         | 
| 39 | 
            -
                placed_words = result["placed_words"]
         | 
| 40 | 
            -
                
         | 
| 41 | 
            -
                print(f"✅ Generated grid with {len(placed_words)} words")
         | 
| 42 | 
            -
                print(f"Grid size: {len(grid)}x{len(grid[0])}")
         | 
| 43 | 
            -
                
         | 
| 44 | 
            -
                # Display the grid
         | 
| 45 | 
            -
                print("\nGenerated Grid:")
         | 
| 46 | 
            -
                for i, row in enumerate(grid):
         | 
| 47 | 
            -
                    row_str = " ".join(cell if cell != "." else " " for cell in row)
         | 
| 48 | 
            -
                    print(f"{i:2d} | {row_str}")
         | 
| 49 | 
            -
                
         | 
| 50 | 
            -
                print(f"\nPlaced Words:")
         | 
| 51 | 
            -
                for word in placed_words:
         | 
| 52 | 
            -
                    print(f"  {word['word']} at ({word['row']},{word['col']}) {word['direction']}")
         | 
| 53 | 
            -
                
         | 
| 54 | 
            -
                # Analyze for boundary violations
         | 
| 55 | 
            -
                print(f"\n🔍 Analyzing for boundary violations...")
         | 
| 56 | 
            -
                
         | 
| 57 | 
            -
                violations = []
         | 
| 58 | 
            -
                
         | 
| 59 | 
            -
                # Check horizontal words
         | 
| 60 | 
            -
                for r in range(len(grid)):
         | 
| 61 | 
            -
                    current_word = ""
         | 
| 62 | 
            -
                    word_start = -1
         | 
| 63 | 
            -
                    
         | 
| 64 | 
            -
                    for c in range(len(grid[r])):
         | 
| 65 | 
            -
                        if grid[r][c] != ".":
         | 
| 66 | 
            -
                            if current_word == "":
         | 
| 67 | 
            -
                                word_start = c
         | 
| 68 | 
            -
                            current_word += grid[r][c]
         | 
| 69 | 
            -
                        else:
         | 
| 70 | 
            -
                            if current_word:
         | 
| 71 | 
            -
                                # Word ended - check if it's a valid placed word
         | 
| 72 | 
            -
                                is_valid_word = any(
         | 
| 73 | 
            -
                                    placed['word'] == current_word and 
         | 
| 74 | 
            -
                                    placed['row'] == r and 
         | 
| 75 | 
            -
                                    placed['col'] == word_start and 
         | 
| 76 | 
            -
                                    placed['direction'] == 'horizontal'
         | 
| 77 | 
            -
                                    for placed in placed_words
         | 
| 78 | 
            -
                                )
         | 
| 79 | 
            -
                                if not is_valid_word and len(current_word) > 1:
         | 
| 80 | 
            -
                                    violations.append(f"Invalid horizontal word '{current_word}' at ({r},{word_start})")
         | 
| 81 | 
            -
                                current_word = ""
         | 
| 82 | 
            -
                    
         | 
| 83 | 
            -
                    # Check word at end of row
         | 
| 84 | 
            -
                    if current_word:
         | 
| 85 | 
            -
                        is_valid_word = any(
         | 
| 86 | 
            -
                            placed['word'] == current_word and 
         | 
| 87 | 
            -
                            placed['row'] == r and 
         | 
| 88 | 
            -
                            placed['col'] == word_start and 
         | 
| 89 | 
            -
                            placed['direction'] == 'horizontal'
         | 
| 90 | 
            -
                            for placed in placed_words
         | 
| 91 | 
            -
                        )
         | 
| 92 | 
            -
                        if not is_valid_word and len(current_word) > 1:
         | 
| 93 | 
            -
                            violations.append(f"Invalid horizontal word '{current_word}' at ({r},{word_start})")
         | 
| 94 | 
            -
                
         | 
| 95 | 
            -
                # Check vertical words
         | 
| 96 | 
            -
                for c in range(len(grid[0])):
         | 
| 97 | 
            -
                    current_word = ""
         | 
| 98 | 
            -
                    word_start = -1
         | 
| 99 | 
            -
                    
         | 
| 100 | 
            -
                    for r in range(len(grid)):
         | 
| 101 | 
            -
                        if grid[r][c] != ".":
         | 
| 102 | 
            -
                            if current_word == "":
         | 
| 103 | 
            -
                                word_start = r
         | 
| 104 | 
            -
                            current_word += grid[r][c]
         | 
| 105 | 
            -
                        else:
         | 
| 106 | 
            -
                            if current_word:
         | 
| 107 | 
            -
                                # Word ended - check if it's a valid placed word
         | 
| 108 | 
            -
                                is_valid_word = any(
         | 
| 109 | 
            -
                                    placed['word'] == current_word and 
         | 
| 110 | 
            -
                                    placed['row'] == word_start and 
         | 
| 111 | 
            -
                                    placed['col'] == c and 
         | 
| 112 | 
            -
                                    placed['direction'] == 'vertical'
         | 
| 113 | 
            -
                                    for placed in placed_words
         | 
| 114 | 
            -
                                )
         | 
| 115 | 
            -
                                if not is_valid_word and len(current_word) > 1:
         | 
| 116 | 
            -
                                    violations.append(f"Invalid vertical word '{current_word}' at ({word_start},{c})")
         | 
| 117 | 
            -
                                current_word = ""
         | 
| 118 | 
            -
                    
         | 
| 119 | 
            -
                    # Check word at end of column
         | 
| 120 | 
            -
                    if current_word:
         | 
| 121 | 
            -
                        is_valid_word = any(
         | 
| 122 | 
            -
                            placed['word'] == current_word and 
         | 
| 123 | 
            -
                            placed['row'] == word_start and 
         | 
| 124 | 
            -
                            placed['col'] == c and 
         | 
| 125 | 
            -
                            placed['direction'] == 'vertical'
         | 
| 126 | 
            -
                            for placed in placed_words
         | 
| 127 | 
            -
                        )
         | 
| 128 | 
            -
                        if not is_valid_word and len(current_word) > 1:
         | 
| 129 | 
            -
                            violations.append(f"Invalid vertical word '{current_word}' at ({word_start},{c})")
         | 
| 130 | 
            -
                
         | 
| 131 | 
            -
                # Report results
         | 
| 132 | 
            -
                if violations:
         | 
| 133 | 
            -
                    print(f"❌ Found {len(violations)} boundary violations:")
         | 
| 134 | 
            -
                    for violation in violations:
         | 
| 135 | 
            -
                        print(f"  - {violation}")
         | 
| 136 | 
            -
                    return False
         | 
| 137 | 
            -
                else:
         | 
| 138 | 
            -
                    print(f"✅ No boundary violations found!")
         | 
| 139 | 
            -
                    print(f"✅ All words in grid are properly placed and bounded")
         | 
| 140 | 
            -
                    return True
         | 
| 141 | 
            -
             | 
| 142 | 
            -
            if __name__ == "__main__":
         | 
| 143 | 
            -
                success = asyncio.run(test_boundary_fix())
         | 
| 144 | 
            -
                if success:
         | 
| 145 | 
            -
                    print(f"\n🎉 Boundary fix is working correctly!")
         | 
| 146 | 
            -
                else:
         | 
| 147 | 
            -
                    print(f"\n💥 Boundary fix needs more work!")
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/test-integration/test_bounds_comprehensive.py
    DELETED
    
    | @@ -1,266 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
            """
         | 
| 3 | 
            -
            Comprehensive test for bounds checking fixes in crossword generator.
         | 
| 4 | 
            -
            """
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            import asyncio
         | 
| 7 | 
            -
            import sys
         | 
| 8 | 
            -
            import pytest
         | 
| 9 | 
            -
            from pathlib import Path
         | 
| 10 | 
            -
             | 
| 11 | 
            -
            # Add project root to path
         | 
| 12 | 
            -
            project_root = Path(__file__).parent.parent  # Go up from test-integration to backend-py
         | 
| 13 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
         | 
| 16 | 
            -
             | 
| 17 | 
            -
            class TestBoundsChecking:
         | 
| 18 | 
            -
                """Test all bounds checking in crossword generator."""
         | 
| 19 | 
            -
                
         | 
| 20 | 
            -
                def setup_method(self):
         | 
| 21 | 
            -
                    """Setup test instance."""
         | 
| 22 | 
            -
                    self.generator = CrosswordGeneratorFixed(vector_service=None)
         | 
| 23 | 
            -
                
         | 
| 24 | 
            -
                def test_can_place_word_bounds_horizontal(self):
         | 
| 25 | 
            -
                    """Test _can_place_word bounds checking for horizontal placement."""
         | 
| 26 | 
            -
                    # Create small grid
         | 
| 27 | 
            -
                    grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 28 | 
            -
                    
         | 
| 29 | 
            -
                    # Test cases that should fail bounds checking
         | 
| 30 | 
            -
                    assert not self.generator._can_place_word(grid, "TOOLONG", 2, 1, "horizontal")  # Word too long
         | 
| 31 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", -1, 1, "horizontal")    # Negative row
         | 
| 32 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 1, -1, "horizontal")    # Negative col
         | 
| 33 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 5, 1, "horizontal")     # Row >= size
         | 
| 34 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 1, 5, "horizontal")     # Col >= size
         | 
| 35 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 1, 3, "horizontal")     # Word extends beyond grid
         | 
| 36 | 
            -
                    
         | 
| 37 | 
            -
                    # Test cases that should pass
         | 
| 38 | 
            -
                    assert self.generator._can_place_word(grid, "TEST", 2, 1, "horizontal")         # Valid placement
         | 
| 39 | 
            -
                    assert self.generator._can_place_word(grid, "A", 0, 0, "horizontal")            # Single letter
         | 
| 40 | 
            -
                    
         | 
| 41 | 
            -
                def test_can_place_word_bounds_vertical(self):
         | 
| 42 | 
            -
                    """Test _can_place_word bounds checking for vertical placement."""
         | 
| 43 | 
            -
                    # Create small grid
         | 
| 44 | 
            -
                    grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 45 | 
            -
                    
         | 
| 46 | 
            -
                    # Test cases that should fail bounds checking
         | 
| 47 | 
            -
                    assert not self.generator._can_place_word(grid, "TOOLONG", 1, 2, "vertical")    # Word too long
         | 
| 48 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", -1, 1, "vertical")      # Negative row
         | 
| 49 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 1, -1, "vertical")      # Negative col
         | 
| 50 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 5, 1, "vertical")       # Row >= size
         | 
| 51 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 1, 5, "vertical")       # Col >= size
         | 
| 52 | 
            -
                    assert not self.generator._can_place_word(grid, "TEST", 3, 1, "vertical")       # Word extends beyond grid
         | 
| 53 | 
            -
                    
         | 
| 54 | 
            -
                    # Test cases that should pass
         | 
| 55 | 
            -
                    assert self.generator._can_place_word(grid, "TEST", 1, 2, "vertical")           # Valid placement
         | 
| 56 | 
            -
                    assert self.generator._can_place_word(grid, "A", 0, 0, "vertical")              # Single letter
         | 
| 57 | 
            -
                
         | 
| 58 | 
            -
                def test_place_word_bounds_horizontal(self):
         | 
| 59 | 
            -
                    """Test _place_word bounds checking for horizontal placement."""
         | 
| 60 | 
            -
                    grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 61 | 
            -
                    
         | 
| 62 | 
            -
                    # Valid placement should work
         | 
| 63 | 
            -
                    original_state = self.generator._place_word(grid, "TEST", 2, 1, "horizontal")
         | 
| 64 | 
            -
                    assert len(original_state) == 4
         | 
| 65 | 
            -
                    assert grid[2][1] == "T"
         | 
| 66 | 
            -
                    assert grid[2][4] == "T"
         | 
| 67 | 
            -
                    
         | 
| 68 | 
            -
                    # Test out-of-bounds placement should raise IndexError
         | 
| 69 | 
            -
                    with pytest.raises(IndexError):
         | 
| 70 | 
            -
                        self.generator._place_word(grid, "TOOLONG", 2, 1, "horizontal")
         | 
| 71 | 
            -
                    
         | 
| 72 | 
            -
                    with pytest.raises(IndexError):
         | 
| 73 | 
            -
                        self.generator._place_word(grid, "TEST", -1, 1, "horizontal")
         | 
| 74 | 
            -
                    
         | 
| 75 | 
            -
                    with pytest.raises(IndexError):
         | 
| 76 | 
            -
                        self.generator._place_word(grid, "TEST", 5, 1, "horizontal")
         | 
| 77 | 
            -
                        
         | 
| 78 | 
            -
                    with pytest.raises(IndexError):
         | 
| 79 | 
            -
                        self.generator._place_word(grid, "TEST", 1, 5, "horizontal")
         | 
| 80 | 
            -
                
         | 
| 81 | 
            -
                def test_place_word_bounds_vertical(self):
         | 
| 82 | 
            -
                    """Test _place_word bounds checking for vertical placement."""
         | 
| 83 | 
            -
                    grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 84 | 
            -
                    
         | 
| 85 | 
            -
                    # Valid placement should work
         | 
| 86 | 
            -
                    original_state = self.generator._place_word(grid, "TEST", 1, 2, "vertical")
         | 
| 87 | 
            -
                    assert len(original_state) == 4
         | 
| 88 | 
            -
                    assert grid[1][2] == "T"
         | 
| 89 | 
            -
                    assert grid[4][2] == "T"
         | 
| 90 | 
            -
                    
         | 
| 91 | 
            -
                    # Test out-of-bounds placement should raise IndexError
         | 
| 92 | 
            -
                    with pytest.raises(IndexError):
         | 
| 93 | 
            -
                        self.generator._place_word(grid, "TOOLONG", 1, 2, "vertical")
         | 
| 94 | 
            -
                        
         | 
| 95 | 
            -
                    with pytest.raises(IndexError):
         | 
| 96 | 
            -
                        self.generator._place_word(grid, "TEST", -1, 2, "vertical")
         | 
| 97 | 
            -
                        
         | 
| 98 | 
            -
                    with pytest.raises(IndexError):
         | 
| 99 | 
            -
                        self.generator._place_word(grid, "TEST", 5, 2, "vertical")
         | 
| 100 | 
            -
                        
         | 
| 101 | 
            -
                    with pytest.raises(IndexError):
         | 
| 102 | 
            -
                        self.generator._place_word(grid, "TEST", 2, 5, "vertical")
         | 
| 103 | 
            -
                
         | 
| 104 | 
            -
                def test_remove_word_bounds(self):
         | 
| 105 | 
            -
                    """Test _remove_word bounds checking."""
         | 
| 106 | 
            -
                    grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 107 | 
            -
                    
         | 
| 108 | 
            -
                    # Place a word first
         | 
| 109 | 
            -
                    original_state = self.generator._place_word(grid, "TEST", 2, 1, "horizontal")
         | 
| 110 | 
            -
                    
         | 
| 111 | 
            -
                    # Normal removal should work
         | 
| 112 | 
            -
                    self.generator._remove_word(grid, original_state)
         | 
| 113 | 
            -
                    assert grid[2][1] == "."
         | 
| 114 | 
            -
                    
         | 
| 115 | 
            -
                    # Test invalid original state should raise IndexError
         | 
| 116 | 
            -
                    bad_state = [{"row": -1, "col": 1, "value": "."}]
         | 
| 117 | 
            -
                    with pytest.raises(IndexError):
         | 
| 118 | 
            -
                        self.generator._remove_word(grid, bad_state)
         | 
| 119 | 
            -
                        
         | 
| 120 | 
            -
                    bad_state = [{"row": 5, "col": 1, "value": "."}]
         | 
| 121 | 
            -
                    with pytest.raises(IndexError):
         | 
| 122 | 
            -
                        self.generator._remove_word(grid, bad_state)
         | 
| 123 | 
            -
                        
         | 
| 124 | 
            -
                    bad_state = [{"row": 1, "col": -1, "value": "."}]
         | 
| 125 | 
            -
                    with pytest.raises(IndexError):
         | 
| 126 | 
            -
                        self.generator._remove_word(grid, bad_state)
         | 
| 127 | 
            -
                        
         | 
| 128 | 
            -
                    bad_state = [{"row": 1, "col": 5, "value": "."}]
         | 
| 129 | 
            -
                    with pytest.raises(IndexError):
         | 
| 130 | 
            -
                        self.generator._remove_word(grid, bad_state)
         | 
| 131 | 
            -
                
         | 
| 132 | 
            -
                def test_create_simple_cross_bounds(self):
         | 
| 133 | 
            -
                    """Test _create_simple_cross bounds checking."""
         | 
| 134 | 
            -
                    # Test with words that have intersections
         | 
| 135 | 
            -
                    word_list = ["CAT", "TOY"]  # 'T' intersection
         | 
| 136 | 
            -
                    word_objs = [{"word": w, "clue": f"Clue for {w}"} for w in word_list]
         | 
| 137 | 
            -
                    
         | 
| 138 | 
            -
                    # This should work without bounds errors
         | 
| 139 | 
            -
                    result = self.generator._create_simple_cross(word_list, word_objs)
         | 
| 140 | 
            -
                    assert result is not None
         | 
| 141 | 
            -
                    assert len(result["placed_words"]) == 2
         | 
| 142 | 
            -
                    
         | 
| 143 | 
            -
                    # Test with words that might cause issues
         | 
| 144 | 
            -
                    word_list = ["A", "A"]  # Same single letter
         | 
| 145 | 
            -
                    word_objs = [{"word": w, "clue": f"Clue for {w}"} for w in word_list]
         | 
| 146 | 
            -
                    
         | 
| 147 | 
            -
                    # This should not crash with bounds errors
         | 
| 148 | 
            -
                    result = self.generator._create_simple_cross(word_list, word_objs)
         | 
| 149 | 
            -
                    # May return None due to placement issues, but should not crash
         | 
| 150 | 
            -
                
         | 
| 151 | 
            -
                def test_trim_grid_bounds(self):
         | 
| 152 | 
            -
                    """Test _trim_grid bounds checking."""
         | 
| 153 | 
            -
                    # Create a grid with words placed
         | 
| 154 | 
            -
                    grid = [["." for _ in range(10)] for _ in range(10)]
         | 
| 155 | 
            -
                    
         | 
| 156 | 
            -
                    # Place some letters
         | 
| 157 | 
            -
                    grid[5][3] = "T"
         | 
| 158 | 
            -
                    grid[5][4] = "E"
         | 
| 159 | 
            -
                    grid[5][5] = "S"
         | 
| 160 | 
            -
                    grid[5][6] = "T"
         | 
| 161 | 
            -
                    
         | 
| 162 | 
            -
                    placed_words = [{
         | 
| 163 | 
            -
                        "word": "TEST",
         | 
| 164 | 
            -
                        "row": 5,
         | 
| 165 | 
            -
                        "col": 3,
         | 
| 166 | 
            -
                        "direction": "horizontal",
         | 
| 167 | 
            -
                        "number": 1
         | 
| 168 | 
            -
                    }]
         | 
| 169 | 
            -
                    
         | 
| 170 | 
            -
                    # This should work without bounds errors
         | 
| 171 | 
            -
                    result = self.generator._trim_grid(grid, placed_words)
         | 
| 172 | 
            -
                    assert result is not None
         | 
| 173 | 
            -
                    assert "grid" in result
         | 
| 174 | 
            -
                    assert "placed_words" in result
         | 
| 175 | 
            -
                    
         | 
| 176 | 
            -
                    # Test with edge case placements
         | 
| 177 | 
            -
                    placed_words = [{
         | 
| 178 | 
            -
                        "word": "A",
         | 
| 179 | 
            -
                        "row": 0,
         | 
| 180 | 
            -
                        "col": 0,
         | 
| 181 | 
            -
                        "direction": "horizontal", 
         | 
| 182 | 
            -
                        "number": 1
         | 
| 183 | 
            -
                    }]
         | 
| 184 | 
            -
                    
         | 
| 185 | 
            -
                    grid[0][0] = "A"
         | 
| 186 | 
            -
                    result = self.generator._trim_grid(grid, placed_words)
         | 
| 187 | 
            -
                    assert result is not None
         | 
| 188 | 
            -
                
         | 
| 189 | 
            -
                def test_calculation_placement_score_bounds(self):
         | 
| 190 | 
            -
                    """Test _calculate_placement_score bounds checking."""
         | 
| 191 | 
            -
                    grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 192 | 
            -
                    
         | 
| 193 | 
            -
                    # Place some letters for intersection testing
         | 
| 194 | 
            -
                    grid[2][2] = "T"
         | 
| 195 | 
            -
                    grid[2][3] = "E"
         | 
| 196 | 
            -
                    
         | 
| 197 | 
            -
                    placement = {"row": 2, "col": 2, "direction": "horizontal"}
         | 
| 198 | 
            -
                    placed_words = []
         | 
| 199 | 
            -
                    
         | 
| 200 | 
            -
                    # This should work without bounds errors
         | 
| 201 | 
            -
                    score = self.generator._calculate_placement_score(grid, "TEST", placement, placed_words)
         | 
| 202 | 
            -
                    assert isinstance(score, int)
         | 
| 203 | 
            -
                    
         | 
| 204 | 
            -
                    # Test with out-of-bounds placement (should handle gracefully)
         | 
| 205 | 
            -
                    placement = {"row": 4, "col": 3, "direction": "horizontal"}  # Would extend beyond grid
         | 
| 206 | 
            -
                    score = self.generator._calculate_placement_score(grid, "TEST", placement, placed_words)
         | 
| 207 | 
            -
                    assert isinstance(score, int)
         | 
| 208 | 
            -
                    
         | 
| 209 | 
            -
                    # Test with negative placement (should handle gracefully)
         | 
| 210 | 
            -
                    placement = {"row": -1, "col": 0, "direction": "horizontal"}
         | 
| 211 | 
            -
                    score = self.generator._calculate_placement_score(grid, "TEST", placement, placed_words)
         | 
| 212 | 
            -
                    assert isinstance(score, int)
         | 
| 213 | 
            -
             | 
| 214 | 
            -
            async def test_full_generation_stress():
         | 
| 215 | 
            -
                """Stress test full generation to catch index errors."""
         | 
| 216 | 
            -
                generator = CrosswordGeneratorFixed(vector_service=None)
         | 
| 217 | 
            -
                
         | 
| 218 | 
            -
                # Mock word selection to return test words
         | 
| 219 | 
            -
                test_words = [
         | 
| 220 | 
            -
                    {"word": "CAT", "clue": "Feline pet"},
         | 
| 221 | 
            -
                    {"word": "DOG", "clue": "Man's best friend"}, 
         | 
| 222 | 
            -
                    {"word": "BIRD", "clue": "Flying animal"},
         | 
| 223 | 
            -
                    {"word": "FISH", "clue": "Aquatic animal"},
         | 
| 224 | 
            -
                    {"word": "ELEPHANT", "clue": "Large mammal"},
         | 
| 225 | 
            -
                    {"word": "TIGER", "clue": "Striped cat"},
         | 
| 226 | 
            -
                    {"word": "HORSE", "clue": "Riding animal"},
         | 
| 227 | 
            -
                    {"word": "BEAR", "clue": "Large carnivore"},
         | 
| 228 | 
            -
                    {"word": "WOLF", "clue": "Pack animal"},
         | 
| 229 | 
            -
                    {"word": "LION", "clue": "King of jungle"}
         | 
| 230 | 
            -
                ]
         | 
| 231 | 
            -
                
         | 
| 232 | 
            -
                generator._select_words = lambda topics, difficulty, use_ai: test_words
         | 
| 233 | 
            -
                
         | 
| 234 | 
            -
                # Run multiple generation attempts
         | 
| 235 | 
            -
                for i in range(20):
         | 
| 236 | 
            -
                    try:
         | 
| 237 | 
            -
                        result = await generator.generate_puzzle(["animals"], "medium", use_ai=False)
         | 
| 238 | 
            -
                        if result:
         | 
| 239 | 
            -
                            print(f"✅ Generation {i+1} succeeded")
         | 
| 240 | 
            -
                        else:
         | 
| 241 | 
            -
                            print(f"⚠️ Generation {i+1} returned None")
         | 
| 242 | 
            -
                    except IndexError as e:
         | 
| 243 | 
            -
                        print(f"❌ Index error in generation {i+1}: {e}")
         | 
| 244 | 
            -
                        raise
         | 
| 245 | 
            -
                    except Exception as e:
         | 
| 246 | 
            -
                        print(f"⚠️ Other error in generation {i+1}: {e}")
         | 
| 247 | 
            -
                        # Don't raise for other errors, just continue
         | 
| 248 | 
            -
                
         | 
| 249 | 
            -
                print("✅ All stress test generations completed without index errors!")
         | 
| 250 | 
            -
             | 
| 251 | 
            -
            if __name__ == "__main__":
         | 
| 252 | 
            -
                # Run tests
         | 
| 253 | 
            -
                print("🧪 Running comprehensive bounds checking tests...")
         | 
| 254 | 
            -
                
         | 
| 255 | 
            -
                # Run pytest on this file
         | 
| 256 | 
            -
                import subprocess
         | 
| 257 | 
            -
                result = subprocess.run([sys.executable, "-m", "pytest", __file__, "-v"], 
         | 
| 258 | 
            -
                                      capture_output=True, text=True)
         | 
| 259 | 
            -
                
         | 
| 260 | 
            -
                print("STDOUT:", result.stdout)
         | 
| 261 | 
            -
                if result.stderr:
         | 
| 262 | 
            -
                    print("STDERR:", result.stderr)
         | 
| 263 | 
            -
                
         | 
| 264 | 
            -
                # Run stress test
         | 
| 265 | 
            -
                print("\n🏋️ Running stress test...")
         | 
| 266 | 
            -
                asyncio.run(test_full_generation_stress())
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        crossword-app/backend-py/test-integration/test_bounds_fix.py
    DELETED
    
    | @@ -1,90 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env python3
         | 
| 2 | 
            -
            """
         | 
| 3 | 
            -
            Quick test to verify the bounds checking fix.
         | 
| 4 | 
            -
            """
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            import sys
         | 
| 7 | 
            -
            from pathlib import Path
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            # Add project root to path
         | 
| 10 | 
            -
            project_root = Path(__file__).parent.parent  # Go up from test-integration to backend-py
         | 
| 11 | 
            -
            sys.path.insert(0, str(project_root))
         | 
| 12 | 
            -
             | 
| 13 | 
            -
            from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            def test_bounds_checking():
         | 
| 16 | 
            -
                """Test that placement score calculation doesn't crash with out-of-bounds access."""
         | 
| 17 | 
            -
                print("🧪 Testing bounds checking fix...")
         | 
| 18 | 
            -
                
         | 
| 19 | 
            -
                generator = CrosswordGeneratorFixed()
         | 
| 20 | 
            -
                
         | 
| 21 | 
            -
                # Create a small grid
         | 
| 22 | 
            -
                grid = [["." for _ in range(5)] for _ in range(5)]
         | 
| 23 | 
            -
                
         | 
| 24 | 
            -
                # Test placement that would go out of bounds
         | 
| 25 | 
            -
                placement = {
         | 
| 26 | 
            -
                    "row": 3,       # Starting at row 3
         | 
| 27 | 
            -
                    "col": 2,       # Starting at col 2  
         | 
| 28 | 
            -
                    "direction": "vertical"
         | 
| 29 | 
            -
                }
         | 
| 30 | 
            -
                
         | 
| 31 | 
            -
                # Word that would extend beyond grid (3+8=11 > 5)
         | 
| 32 | 
            -
                word = "ELEPHANT"  # 8 letters, would go from row 3 to row 10 (out of bounds)
         | 
| 33 | 
            -
                
         | 
| 34 | 
            -
                try:
         | 
| 35 | 
            -
                    # This should NOT crash with bounds checking
         | 
| 36 | 
            -
                    score = generator._calculate_placement_score(grid, word, placement, [])
         | 
| 37 | 
            -
                    print(f"✅ Success! Placement score calculated: {score}")
         | 
| 38 | 
            -
                    print("✅ Bounds checking is working correctly")
         | 
| 39 | 
            -
                    return True
         | 
| 40 | 
            -
                except IndexError as e:
         | 
| 41 | 
            -
                    print(f"❌ IndexError still occurs: {e}")
         | 
| 42 | 
            -
                    return False
         | 
| 43 | 
            -
                except Exception as e:
         | 
| 44 | 
            -
                    print(f"❌ Other error: {e}")
         | 
| 45 | 
            -
                    return False
         | 
| 46 | 
            -
             | 
| 47 | 
            -
            def test_valid_placement():
         | 
| 48 | 
            -
                """Test that valid placements still work correctly."""
         | 
| 49 | 
            -
                print("\n🧪 Testing valid placement scoring...")
         | 
| 50 | 
            -
                
         | 
| 51 | 
            -
                generator = CrosswordGeneratorFixed()
         | 
| 52 | 
            -
                
         | 
| 53 | 
            -
                # Create a grid with some letters
         | 
| 54 | 
            -
                grid = [["." for _ in range(8)] for _ in range(8)]
         | 
| 55 | 
            -
                grid[2][2] = "A"  # Place an 'A' at position (2,2)
         | 
| 56 | 
            -
                
         | 
| 57 | 
            -
                # Test placement that intersects properly
         | 
| 58 | 
            -
                placement = {
         | 
| 59 | 
            -
                    "row": 2,
         | 
| 60 | 
            -
                    "col": 1,
         | 
| 61 | 
            -
                    "direction": "horizontal"
         | 
| 62 | 
            -
                }
         | 
| 63 | 
            -
                
         | 
| 64 | 
            -
                word = "CAT"  # Should intersect at the 'A'
         | 
| 65 | 
            -
                
         | 
| 66 | 
            -
                try:
         | 
| 67 | 
            -
                    score = generator._calculate_placement_score(grid, word, placement, [])
         | 
| 68 | 
            -
                    print(f"✅ Valid placement score: {score}")
         | 
| 69 | 
            -
                    
         | 
| 70 | 
            -
                    # Should have intersection bonus (score > 100)
         | 
| 71 | 
            -
                    if score > 300:  # Base 100 + intersection 200
         | 
| 72 | 
            -
                        print("✅ Intersection detection working")
         | 
| 73 | 
            -
                    else:
         | 
| 74 | 
            -
                        print(f"⚠️ Expected intersection bonus, got score {score}")
         | 
| 75 | 
            -
                    
         | 
| 76 | 
            -
                    return True
         | 
| 77 | 
            -
                except Exception as e:
         | 
| 78 | 
            -
                    print(f"❌ Error with valid placement: {e}")
         | 
| 79 | 
            -
                    return False
         | 
| 80 | 
            -
             | 
| 81 | 
            -
            if __name__ == "__main__":
         | 
| 82 | 
            -
                print("🔧 Testing crossword generator bounds fix\n")
         | 
| 83 | 
            -
                
         | 
| 84 | 
            -
                test1_pass = test_bounds_checking()
         | 
| 85 | 
            -
                test2_pass = test_valid_placement()
         | 
| 86 | 
            -
                
         | 
| 87 | 
            -
                if test1_pass and test2_pass:
         | 
| 88 | 
            -
                    print("\n✅ All tests passed! The bounds checking fix is working.")
         | 
| 89 | 
            -
                else:
         | 
| 90 | 
            -
                    print("\n❌ Some tests failed. More work needed.")
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  |