PageSourceSearch

https://dynamic-salmiakki-095693.netlify.app/js/images/image-gallery.js

js dynamic-salmiakki-095693.netlify.app collected 2026-10-03 10:40:39 UTC 32,777 bytes, 710 lines download raw bytes

1// Image Processing & Gallery Module
2// Extracted from main.js for better modularity
3
4/**
5 * Wikipedia Image Processing & Gallery Manager
6 * Handles image fetching, gallery creation, and image display functionality
7 */
8class WikipediaImageGallery {
9    constructor() {
10        this.imageCache = new Map(); // Cache for fetched images
11        this.galleryCount = 0; // Counter for unique gallery IDs
12    }
13
14    /**
15     * Fetch Wikipedia images for multiple entities (Stage 2/3 progressive loading)
16     * @param {Array} entities - Array of entity objects with title/name properties
17     * @param {string} mainQuery - Main search query for context
18     * @returns {Promise<void>}
19     */
20    async fetchWikipediaImagesForEntities(entities, mainQuery) {
21        if (!entities || entities.length === 0) {
22            console.log('[Stage 2/3 Images] No entities provided for image fetching');
23            return;
24        }
25
26        console.log(`[Stage 2/3 Images] 🚀 Starting image fetch for ${entities.length} entities`);
27        console.log('[Stage 2/3 Images] Entities to process:', entities.map(e => e.title || e.name || e.page_title || e));
28
29        // Initialize global storage if not exists
30        if (!window.fetchedGalleryImages) {
31            window.fetchedGalleryImages = {};
32        }
33
34        // Process entities in smaller batches to avoid overwhelming the API
35        const batchSize = 3; // Process 3 entities at a time
36        const batches = [];
37        for (let i = 0; i < entities.length; i += batchSize) {
38            batches.push(entities.slice(i, i + batchSize));
39        }
40
41        console.log(`[Stage 2/3 Images] Processing ${batches.length} batches of ${batchSize} entities each`);
42
43        for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
44            const batch = batches[batchIndex];
45            console.log(`[Stage 2/3 Images] 📦 Processing batch ${batchIndex + 1}/${batches.length}:`, batch.map(e => e.title || e.name || e.page_title || e));
46
47            // Process entities in parallel within each batch
48            const batchPromises = batch.map(async (entity, entityIndex) => {
49                const entityTitle = entity.title || entity.name || entity.page_title || entity;
50                if (!entityTitle || typeof entityTitle !== 'string') {
51                    console.warn('[Stage 2/3 Images] Invalid entity title:', entity);
52                    return;
53                }
54
55                // Skip if we already have images for this entity
56                if (window.fetchedGalleryImages[entityTitle]) {
57                    console.log(`[Stage 2/3 Images] ⏭️ Skipping ${entityTitle} - already have ${window.fetchedGalleryImages[entityTitle].length} images`);
58                    return;
59                }
60
61                try {
62                    console.log(`[Stage 2/3 Images] 🔍 Fetching images for: ${entityTitle}`);
63                    await this.fetchWikipediaImageForQuery(entityTitle);
64                    console.log(`[Stage 2/3 Images] ✅ Completed image fetch for: ${entityTitle}`);
65                } catch (error) {
66                    console.error(`[Stage 2/3 Images] ❌ Failed to fetch images for ${entityTitle}:`, error);
67                }
68            });
69
70            // Wait for current batch to complete
71            await Promise.allSettled(batchPromises);
72
73            // Add delay between batches to be respectful to Wikipedia API
74            if (batchIndex < batches.length - 1) {
75                const delay = 2000; // 2 second delay between batches
76                console.log(`[Stage 2/3 Images] ⏳ Waiting ${delay}ms before next batch...`);
77                await new Promise(resolve => setTimeout(resolve, delay));
78            }
79        }
80
81        console.log('[Stage 2/3 Images] 🎉 Completed image fetching for all entities');
82        console.log('[Stage 2/3 Images] Final global storage:', Object.keys(window.fetchedGalleryImages).map(key => `${key}: ${window.fetchedGalleryImages[key].length} images`));
83
84        // Trigger gallery refresh if fancy UI is available
85        if (typeof window.createFancyUIPreview === 'function' && window.wikipediaMetadata) {
86            console.log('[Stage 2/3 Images] 🔄 Triggering gallery refresh after Stage 2/3 image fetch');
87            setTimeout(() => {
88                try {
89                    const refreshData = {
90                        wikipediaData: window.wikipediaMetadata?.wikipediaData || [],
91                        wikipediaImages: window.wikipediaMetadata?.wikipediaImages || [],
92                        realtimeFacts: window.wikipediaMetadata?.realtimeFacts || [],
93                        factsByEntity: window.wikipediaMetadata?.factsByEntity || {},
94                        fullResponse: window.lastFullResponse || ''
95                    };
96                    window.createFancyUIPreview(refreshData, document.querySelector('.chat-container'));
97                } catch (error) {
98                    console.error('[Stage 2/3 Images] Error refreshing gallery:', error);
99                }
100            }, 1000);
101        }
102    }
103
104    /**
105     * Fetch Wikipedia image for query using improved topic extraction
106     * @param {string} query - Search query
107     * @returns {Promise<void>}
108     */
109    async fetchWikipediaImageForQuery(query) {
110        // Enhanced fetch with retry logic and rate limiting
111        const fetchWithRetry = async (url, retries = 3) => {
112            for (let attempt = 1; attempt <= retries; attempt++) {
113                try {
114                    // Add delay between requests to prevent rate limiting
115                    if (attempt > 1) {
116                        const delay = Math.min(1000 * Math.pow(2, attempt - 2), 5000); // 1s, 2s, 4s max
117                        console.log(`[Portal Image] Retry ${attempt}/${retries} after ${delay}ms delay`);
118                        await new Promise(resolve => setTimeout(resolve, delay));
119                    }
120                    
121                    const response = await fetch(url, {
122                        headers: {
123                            'User-Agent': 'WikiLive/1.0 (Educational Tool)'
124                        }
125                    });
126                    
127                    // Handle specific HTTP status codes
128                    if (response.status === 502 || response.status === 503 || response.status === 504) {
129                        throw new Error(`HTTP ${response.status}: Server temporarily unavailable`);
130                    }
131                    
132                    if (response.status === 429) {
133                        // Rate limited - wait longer
134                        const retryAfter = response.headers.get('Retry-After') || 10;
135                        console.warn(`[Portal Image] Rate limited, waiting ${retryAfter}s`);
136                        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
137                        throw new Error('Rate limited, retrying');
138                    }
139                    
140                    if (!response.ok) {
141                        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
142                    }
143                    
144                    return response;
145                } catch (error) {
146                    console.warn(`[Portal Image] Attempt ${attempt}/${retries} failed:`, error.message);
147                    
148                    // Check if this is a retryable error
149                    const isRetryableError = (
150                        error.message.includes('502') ||
151                        error.message.includes('503') ||
152                        error.message.includes('504') ||
153                        error.message.includes('429') ||
154                        error.message.includes('Rate limited') ||
155                        error.message.includes('ERR_CONNECTION_CLOSED') ||
156                        error.message.includes('ERR_NETWORK') ||
157                        error.message.includes('ERR_INTERNET_DISCONNECTED') ||
158                        error.message.includes('network error') ||
159                        error.message.includes('Failed to fetch') ||
160                        error.name === 'TypeError' && error.message.includes('fetch')
161                    );
162                    
163                    if (attempt === retries) {
164                        if (isRetryableError) {
165                            console.error(`[Portal Image] Max retries (${retries}) reached for retryable error:`, error.message);
166                        }
167                        throw error;
168                    }
169                    
170                    // Only retry if it's a retryable error
171                    if (!isRetryableError) {
172                        console.error(`[Portal Image] Non-retryable error, failing immediately:`, error.message);
173                        throw error;
174                    }
175                    
176                    // Don't retry on non-retryable errors
177                    if (error.message.includes('400') || error.message.includes('401') || error.message.includes('403')) {
178                        throw error;
179                    }
180                }
181            }
182        };
183        
184        try {
185            console.log('[Portal Image] Fetching images for query:', query);
186            
187            // Step 1: Use Wikipedia's search API to resolve natural language query to page title
188            const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*`;
189            
190            if (window.brainLoader) {
191                window.brainLoader.updateProcessingStep('Searching Wikipedia...', '🔍');
192            }
193            
194            const searchResponse = await fetch(searchUrl);
195            
196            if (!searchResponse.ok) {
197                throw new Error(`Search API failed: ${searchResponse.status}`);
198            }
199            
200            const searchData = await searchResponse.json();
201            if (!searchData.query?.search?.length) {
202                console.log('[Portal Image] No Wikipedia pages found for query:', query);
203                if (window.brainLoader) {
204                    window.brainLoader.updateProcessingStep('No Wikipedia content found', '❌');
205                }
206                return;
207            }
208            
209            const pageTitle = searchData.query.search[0].title;
210            console.log('[Portal Image] Resolved page title:', pageTitle);
211            
212            // Step 2: First fetch the main page image for later use
213            if (window.brainLoader) {
214                window.brainLoader.updateProcessingStep('Fetching main image...', '🖼️');
215            }
216            
217            let mainImageUrl = null;
218            try {
219                const mainImageApiUrl = `https://en.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(pageTitle)}&prop=pageimages&format=json&pithumbsize=300&origin=*`;
220                const mainImageResponse = await fetchWithRetry(mainImageApiUrl);
221                
222                if (mainImageResponse.ok) {
223                    const mainImageData = await mainImageResponse.json();
224                    if (mainImageData.query && mainImageData.query.pages) {
225                        const pages = Object.values(mainImageData.query.pages);
226                        if (pages[0] && pages[0].thumbnail && pages[0].thumbnail.source) {
227                            mainImageUrl = pages[0].thumbnail.source;
228                            console.log('[Portal Image] Found main image:', mainImageUrl);
229                        }
230                    }
231                }
232            } catch (error) {
233                console.warn('[Portal Image] Failed to fetch main image:', error);
234            }
235            
236            // Step 3: Fetch ALL images from this page using generator=images API
237            if (window.brainLoader) {
238                window.brainLoader.updateProcessingStep('Fetching all images...', '🖼️');
239            }
240            
241            // Fetch ALL images for the page using generator=images with continuation
242            const allImagesUrl = `https://en.wikipedia.org/w/api.php?action=query&generator=images&titles=${encodeURIComponent(pageTitle)}&prop=imageinfo&iiprop=url&format=json&origin=*`;
243            
244            let allImageUrls = [];
245            let continueToken = null;
246            let fetchCount = 0;
247            const maxFetches = 5; // Limit to prevent infinite loops
248            
249
250            
251            // Fetch all image batches using continuation
252            do {
253                let currentUrl = allImagesUrl;
254                if (continueToken) {
255                    currentUrl += `&gimcontinue=${encodeURIComponent(continueToken)}`;
256                }
257                
258                console.log(`[Portal Image] Fetching image batch ${fetchCount + 1}:`, currentUrl);
259                
260                try {
261                    const allImagesResponse = await fetchWithRetry(currentUrl);
262                    
263                    if (allImagesResponse.ok) {
264                        const allImagesData = await allImagesResponse.json();
265                        console.log(`[Portal Image] Batch ${fetchCount + 1} API response:`, allImagesData);
266                        
267                        // Extract image URLs from the generator response
268                        if (allImagesData.query && allImagesData.query.pages) {
269                            Object.values(allImagesData.query.pages).forEach(page => {
270                                if (page.imageinfo && page.imageinfo[0] && page.imageinfo[0].url) {
271                                    allImageUrls.push(page.imageinfo[0].url);
272                                }
273                            });
274                        }
275                        
276                        // Check for continuation
277                        continueToken = allImagesData.continue ? allImagesData.continue.gimcontinue : null;
278                        fetchCount++;
279                        
280                        console.log(`[Portal Image] Batch ${fetchCount} complete. Images so far: ${allImageUrls.length}, Continue: ${continueToken ? 'Yes' : 'No'}`);
281                    } else {
282                        console.error('[Portal Image] Failed to fetch image batch:', allImagesResponse.status);
283                        break;
284                    }
285                } catch (error) {
286                    console.error(`[Portal Image] Connection error in batch ${fetchCount + 1}:`, error.message);
287                    
288                    // Check if we have any images already - if so, continue with what we have
289                    if (allImageUrls.length > 0) {
290                        console.log(`[Portal Image] Continuing with ${allImageUrls.length} images already fetched`);
291                        if (window.brainLoader) {
292                            window.brainLoader.updateProcessingStep(`Connection issue, using ${allImageUrls.length} images`, '⚠️');
293                        }
294                        break; // Exit the loop but continue processing with existing images
295                    } else {
296                        // No images yet, this is a more serious failure
297                        console.error('[Portal Image] No images fetched and connection failed');
298                        if (window.brainLoader) {
299                            window.brainLoader.updateProcessingStep('Connection failed, no images available', '❌');
300                        }
301                        return; // Exit the function entirely
302                    }
303                }
304            } while (continueToken && fetchCount < maxFetches);
305            
306            console.log(`[Portal Image] Total images fetched: ${allImageUrls.length}`);
307            
308            // Filter out unwanted images (logos, icons, etc.) and keep only photos
309            let imageUrls = allImageUrls.filter(url => {
310                const urlLower = url.toLowerCase();
311                return url && 
312                       url.startsWith('http') && 
313                       (url.includes('.jpg') || url.includes('.png') || url.includes('.jpeg')) &&
314                       !urlLower.includes('commons-logo') &&
315                       !urlLower.includes('wikimedia-logo') &&
316                       !urlLower.includes('edit-icon') &&
317                       !urlLower.includes('flag') &&
318                       !urlLower.includes('.svg') &&
319                       !urlLower.includes('symbol') &&
320                       !urlLower.includes('icon') &&
321                       !urlLower.includes('protection') &&
322                       !urlLower.includes('vip.svg') &&
323                       !urlLower.includes('shackle');
324            });
325            
326            // Remove duplicates
327            imageUrls = [...new Set(imageUrls)];
328            
329            console.log('[Portal Image] Filtered image URLs:', imageUrls);
330            
331            if (imageUrls.length === 0) {
332                console.log('[Portal Image] No valid images found, falling back to main image API');
333                
334                // Fallback: Get main page image if no gallery images found
335                const mainImageUrlFallback = `https://en.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(pageTitle)}&prop=pageimages&format=json&pithumbsize=600&origin=*`;
336                const mainImageResponseFallback = await fetchWithRetry(mainImageUrlFallback);
337                
338                if (mainImageResponseFallback.ok) {
339                    const mainImageDataFallback = await mainImageResponseFallback.json();
340                    if (mainImageDataFallback.query && mainImageDataFallback.query.pages) {
341                        const pagesFallback = Object.values(mainImageDataFallback.query.pages);
342                        if (pagesFallback[0] && pagesFallback[0].thumbnail && pagesFallback[0].thumbnail.source) {
343                            imageUrls.push(pagesFallback[0].thumbnail.source);
344                            console.log('[Portal Image] Found main image as fallback:', pagesFallback[0].thumbnail.source);
345                        }
346                    }
347                }
348            }
349            
350            if (imageUrls.length === 0) {
351                console.log('[Portal Image] No valid images found for:', pageTitle);
352                if (window.brainLoader) {
353                    window.brainLoader.updateProcessingStep('No images found', '❌');
354                }
355                return;
356            }
357            
358            // Step 4: Cache the images
359            this.imageCache.set(pageTitle, {
360                pageTitle: pageTitle,
361                imageUrls: imageUrls,
362                timestamp: Date.now()
363            });
364            
365            // Step 5: Store images globally for fancy UI gallery access
366            if (!window.fetchedGalleryImages) {
367                window.fetchedGalleryImages = {};
368            }
369            window.fetchedGalleryImages[pageTitle] = {
370                pageTitle: pageTitle,
371                imageUrls: imageUrls,
372                mainImageUrl: mainImageUrl,
373                timestamp: Date.now(),
374                totalImages: imageUrls.length
375            };
376            console.log(`[Portal Image] 🎯 Stored ${imageUrls.length} images globally for gallery:`, pageTitle);
377            
378            // Step 6: Start the image slideshow in brain portal
379            if (window.brainLoader) {
380                window.brainLoader.updateProcessingStep(`Found ${imageUrls.length} images!`, '✅');
381                window.brainLoader.startImageSlideshow(imageUrls, mainImageUrl);
382            }
383            
384            console.log('[Portal Image] Successfully started image slideshow for:', pageTitle, 'with', imageUrls.length, 'images');
385            
386        } catch (error) {
387            console.error('[Portal Image] Error fetching Wikipedia images for query:', error);
388            if (window.brainLoader) {
389                window.brainLoader.updateProcessingStep('Processing query...', '🔍');
390            }
391        }
392    }
393
394    /**
395     * Create gallery with AI response images
396     * @param {string} aiResponseText - AI response text containing Wikipedia URLs
397     * @param {HTMLElement} botDiv - Bot message container
398     * @returns {Promise<void>}
399     */
400    async createGalleryWithAIResponseImages(aiResponseText, botDiv) {
401        console.log('🎨 Creating new gallery for AI response URL images');
402        
403        const wikipediaUrls = window.URLHelpers ? 
404            window.URLHelpers.extractWikipediaUrlsFromText(aiResponseText) :
405            this.extractWikipediaUrlsFromText(aiResponseText);
406        
407        if (wikipediaUrls.length === 0) {
408            console.log('📭 No Wikipedia URLs found in AI response for new gallery');
409            return;
410        }
411        
412        // Create gallery div
413        const galleryDiv = document.createElement('div');
414        galleryDiv.className = 'wikipedia-image-gallery';
415        galleryDiv.id = `gallery-${++this.galleryCount}`;
416        galleryDiv.style.cssText = `
417            display: grid;
418            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
419            gap: 15px;
420            margin: 20px 0;
421            padding: 15px;
422            background: #f8f9fa;
423            border-radius: 8px;
424            border: 1px solid #e9ecef;
425        `;
426        
427        // Add gallery header
428        const headerDiv = document.createElement('div');
429        headerDiv.style.cssText = `
430            grid-column: 1 / -1;
431            font-weight: bold;
432            color: #495057;
433            margin-bottom: 10px;
434            font-size: 0.9em;
435        `;
436        headerDiv.textContent = `📸 Wikipedia Images (${wikipediaUrls.length} sources)`;
437        galleryDiv.appendChild(headerDiv);
438        
439        // Process each URL for images
440        const imagePromises = wikipediaUrls.map(async (url, index) => {
441            try {
442                // Extract page title from URL
443                const titleMatch = url.match(/\/wiki\/(.+)$/);
444                if (!titleMatch) return null;
445                
446                const pageTitle = decodeURIComponent(titleMatch[1]);
447                console.log(`🔍 Gallery: Fetching image ${index + 1} for: ${pageTitle}`);
448                
449                // Check cache first
450                if (this.imageCache.has(pageTitle)) {
451                    const cached = this.imageCache.get(pageTitle);
452                    return { pageTitle, imageUrl: cached.imageUrls[0], pageUrl: url };
453                }
454                
455                // Fetch image from API
456                const imageResponse = await fetch(`/.netlify/functions/fetch-wikipedia-image?title=${encodeURIComponent(pageTitle)}`);
457                
458                if (imageResponse.ok) {
459                    const imageData = await imageResponse.json();
460                    if (imageData.imageUrl) {
461                        // Cache the result
462                        this.imageCache.set(pageTitle, {
463                            pageTitle: pageTitle,
464                            imageUrls: [imageData.imageUrl],
465                            timestamp: Date.now()
466                        });
467                        
468                        return {
469                            pageTitle: pageTitle,
470                            imageUrl: imageData.imageUrl,
471                            pageUrl: url
472                        };
473                    }
474                }
475                
476                console.log(`❌ Gallery: No image found for ${pageTitle}`);
477                return null;
478                
479            } catch (error) {
480                console.error(`❌ Gallery: Error fetching image ${index + 1}:`, error);
481                return null;
482            }
483        });
484        
485        // Wait for all image fetches to complete
486        const imageResults = await Promise.all(imagePromises);
487        const validImages = imageResults.filter(result => result !== null);
488        
489        console.log(`✅ Gallery: Successfully fetched ${validImages.length}/${wikipediaUrls.length} images`);
490        
491        // Create image elements
492        validImages.forEach((imageData, index) => {
493            const imageContainer = document.createElement('div');
494            imageContainer.style.cssText = `
495                position: relative;
496                border-radius: 6px;
497                overflow: hidden;
498                box-shadow: 0 2px 8px rgba(0,0,0,0.1);
499                transition: transform 0.2s ease;
500                cursor: pointer;
501                background: white;
502            `;
503            
504            // Add hover effect
505            imageContainer.addEventListener('mouseenter', () => {
506                imageContainer.style.transform = 'scale(1.02)';
507            });
508            imageContainer.addEventListener('mouseleave', () => {
509                imageContainer.style.transform = 'scale(1)';
510            });
511            
512            const img = document.createElement('img');
513            img.src = imageData.imageUrl;
514            img.alt = imageData.pageTitle;
515            img.style.cssText = `
516                width: 100%;
517                height: 150px;
518                object-fit: cover;
519                display: block;
520            `;
521            
522            // Add click handler to open Wikipedia page
523            imageContainer.addEventListener('click', () => {
524                window.open(imageData.pageUrl, '_blank');
525            });
526            
527            // Hide container if image fails to load
528            img.addEventListener('error', () => {
529                console.error(`❌ Failed to load gallery image: ${imageData.pageTitle}`);
530                imageContainer.style.display = 'none';
531            });
532            
533            // Add loading and success handlers
534            img.addEventListener('load', () => {
535                console.log(`✅ Gallery image ${index + 1} loaded successfully: ${imageData.pageTitle}`);
536            });
537            
538            // Add image title overlay
539            const titleOverlay = document.createElement('div');
540            titleOverlay.style.cssText = `
541                position: absolute;
542                bottom: 0;
543                left: 0;
544                right: 0;
545                background: linear-gradient(transparent, rgba(0,0,0,0.7));
546                color: white;
547                padding: 8px;
548                font-size: 0.8em;
549                font-weight: 500;
550                text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
551            `;
552            titleOverlay.textContent = imageData.pageTitle.replace(/_/g, ' ');
553            
554            imageContainer.appendChild(img);
555            imageContainer.appendChild(titleOverlay);
556            galleryDiv.appendChild(imageContainer);
557        });
558        
559        // Only add gallery if we have images and a valid container
560        if (validImages.length > 0) {
561            if (botDiv) {
562                botDiv.appendChild(galleryDiv);
563                console.log(`📸 Wikipedia image gallery added to bot message with ${validImages.length} images`);
564            } else {
565                console.warn('⚠️ botDiv is null, cannot append gallery');
566            }
567        } else {
568            console.log('📭 No valid images found, gallery not added');
569        }
570    }
571
572    /**
573     * Fallback function to extract Wikipedia URLs from text
574     * @param {string} text - Text to search for URLs
575     * @returns {Array<string>} - Array of Wikipedia URLs
576     */
577    extractWikipediaUrlsFromText(text) {
578        if (!text || typeof text !== 'string') {
579            return [];
580        }
581        
582        const wikipediaUrlRegex = /https?:\/\/[a-z]{2,3}\.wikipedia\.org\/wiki\/[^\s\)\]\"'\<\>]+/gi;
583        const urls = text.match(wikipediaUrlRegex) || [];
584        
585        // Clean and deduplicate URLs
586        const cleanUrls = urls
587            .map(url => url.replace(/\/wiki\/([^:]+):(.*)$/, '/wiki/$1')) // Fix malformed URLs
588            .filter(url => url.length > 0)
589            .filter((url, index, arr) => arr.indexOf(url) === index); // Remove duplicates
590        
591        return cleanUrls;
592    }
593
594    /**
595     * Fetch Wikipedia image from extracted URLs (enhanced version)
596     * @param {Array<string>} extractedUrls - Array of Wikipedia URLs
597     * @returns {Promise<void>}
598     */
599    async fetchWikipediaImageFromExtractedUrls(extractedUrls) {
600        if (!extractedUrls || !Array.isArray(extractedUrls) || extractedUrls.length === 0) {
601            console.log('[Portal Image] No extracted Wikipedia URLs available');
602            return;
603        }
604
605        try {
606            // Smart URL selection: prioritize person names and main 
606subjects
607            let selectedUrl = extractedUrls[0];
608            
609            // Look for a main page (non-disambiguation) if available
610            for (const url of extractedUrls) {
611                const title = url.split('/wiki/')[1];
612                if (title && !title.includes('(disambiguation)') && !title.includes('List_of')) {
613                    selectedUrl = url;
614                    break;
615                }
616            }
617            
618            // Extract page title
619            const titleMatch = selectedUrl.match(/\/wiki\/(.+)$/);
620            if (!titleMatch) {
621                console.log('[Portal Image] Could not extract title from URL:', selectedUrl);
622                return;
623            }
624            
625            const pageTitle = decodeURIComponent(titleMatch[1]);
626            console.log('[Portal Image] Fetching image for extracted URL:', pageTitle);
627            
628            if (window.brainLoader) {
629                window.brainLoader.updateProcessingStep('Fetching main image...', '🖼️');
630            }
631            
632            const imageResponse = await fetch(`/.netlify/functions/fetch-wikipedia-image?title=${encodeURIComponent(pageTitle)}`);
633            
634            if (imageResponse.ok) {
635                const imageData = await imageResponse.json();
636                if (imageData.imageUrl) {
637                    // Cache the image
638                    this.imageCache.set(pageTitle, {
639                        pageTitle: pageTitle,
640                        imageUrls: [imageData.imageUrl],
641                        timestamp: Date.now()
642                    });
643                    
644                    if (window.brainLoader) {
645                        window.brainLoader.updateProcessingStep('Main image loaded!', '✅');
646                    }
647                    console.log('[Portal Image] Successfully loaded image from OpenAI-extracted URL');
648                } else {
649                    console.log('[Portal Image] No image found for page:', pageTitle);
650                }
651            } else {
652                console.log('[Portal Image] Failed to fetch image, status:', imageResponse.status);
653            }
654        } catch (error) {
655            console.error('[Portal Image] Error fetching Wikipedia image from extracted URLs:', error);
656        }
657    }
658
659    /**
660     * Clear image cache
661     */
662    clearCache() {
663        this.imageCache.clear();
664        console.log('[Image Gallery] Cache cleared');
665    }
666
667    /**
668     * Get cached image data
669     * @param {string} pageTitle - Wikipedia page title
670     * @returns {Object|null} - Cached image data or null
671     */
672    getCachedImage(pageTitle) {
673        return this.imageCache.get(pageTitle) || null;
674    }
675
676    /**
677     * Get cache statistics
678     * @returns {Object} - Cache statistics
679     */
680    getCacheStats() {
681        return {
682            size: this.imageCache.size,
683            galleries: this.galleryCount
684        };
685    }
686}
687
688// Export for use in other modules
689if (typeof module !== 'undefined' && module.exports) {
690    // Node.js environment
691    module.exports = {
692        WikipediaImageGallery
693    };
694} else {
695    // Browser environment - attach to window
696    window.WikipediaImageGallery = WikipediaImageGallery;
697    
698    // Create global instance for backward compatibility
699    window.imageGallery = new WikipediaImageGallery();
700    
701    // Export functions to global scope for backward compatibility
702    window.fetchWikipediaImageForQuery = (query) => window.imageGallery.fetchWikipediaImageForQuery(query);
703    window.fetchWikipediaImagesForEntities = (entities, mainQuery) => window.imageGallery.fetchWikipediaImagesForEntities(entities, mainQuery);
704    window.createGalleryWithAIResponseImages = (aiResponseText, botDiv) => 
705        window.imageGallery.createGalleryWithAIResponseImages(aiResponseText, botDiv);
706    window.fetchWikipediaImageFromExtractedUrls = (extractedUrls) => 
707        window.imageGallery.fetchWikipediaImageFromExtractedUrls(extractedUrls);
708}
709
710console.log('Wikipedia Image Processing & Gallery Module - Loaded and Ready');

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.