1// ð¯ Fancy UI Preview Component for WikiLive Infobox Visualization 2// Creates beautiful visual layouts with search term in center and infoboxes arranged around it 3 4// Function to create a fancy UI preview using the extracted JSON data 5function createFancyUIPreview(data, chatContainer) { 6 console.log('[FancyUI] ð¯ createFancyUIPreview called with data:', data); 7 8 // ð DEBUG: Show complete input data structure 9 console.log('ð¯ [Gallery JSON for carousel] Input data to createFancyUIPreview:'); 10 console.log('ð Complete Input JSON:', JSON.stringify({ 11 hasWikipediaImages: !!(data?.wikipediaImages), 12 wikipediaImagesCount: data?.wikipediaImages?.length || 0, 13 hasWikipediaData: !!(data?.wikipediaData), 14 wikipediaDataCount: data?.wikipediaData?.length || 0, 15 hasInfoboxes: !!(data?.infoboxes), 16 infoboxesCount: data?.infoboxes?.length || 0, 17 hasRealtimeFacts: !!(data?.realtime_facts), 18 realtimeFactsCount: data?.realtime_facts?.length || 0, 19 hasFactsByEntity: !!(data?.factsByEntity), 20 factsByEntityKeys: data?.factsByEntity ? Object.keys(data.factsByEntity) : [], 21 sampleWikipediaImage: data?.wikipediaImages?.[0] || null, 22 sampleWikipediaData: data?.wikipediaData?.[0] || null 23 }, null, 2)); 24 25 if (data?.wikipediaImages?.length > 0) { 26 console.log('ð¯ [Gallery JSON for carousel] Wikipedia Images Sample:'); 27 data.wikipediaImages.forEach((img, index) => { 28 console.log(`Image ${index + 1}:`, JSON.stringify(img, null, 2)); 29 }); 30 } 31 32 if (!data) { 33 console.error('[FancyUI] â No data provided to createFancyUIPreview'); 34 return; 35 } 36 37 try { 38 console.log("ð¯ [createFancyUIPreview] FUNCTION CALLED - Starting fancy UI visualization..."); 39 40 if (!data) { 41 console.error("ð¯ [createFancyUIPreview] ERROR: No data provided"); 42 return; 43 } 44 45 // ðºï¸ CRITICAL FIX: Process and classify entities before storing for relationship map 46 const processedData = { 47 ...data, 48 timestamp: new Date().toISOString(), 49 totalResults: (data.infoboxes?.length || 0) + (data.wikipediaData?.length || 0) 50 }; 51 52 // ð¯ ENHANCED: Apply entity type classification to all data sources 53 if (processedData.infoboxes && Array.isArray(processedData.infoboxes)) { 54 processedData.infoboxes = processedData.infoboxes.map(entity => { 55 const entityType = classifyEntityType(entity, entity.title, entity.summary); 56 const relevanceScore = calculateEntityRelevance(entity, entity.title, entity.summary, entity.imageUrl, 'infoboxes'); 57 return { 58 ...entity, 59 type: entityType, 60 relevance: relevanceScore 61 }; 62 }); 63 } 64 65 if (processedData.wikipediaData && Array.isArray(processedData.wikipediaData)) { 66 processedData.wikipediaData = processedData.wikipediaData.map(entity => { 67 const entityType = classifyEntityType(entity, entity.title, entity.summary); 68 const relevanceScore = calculateEntityRelevance(entity, entity.title, entity.summary, entity.imageUrl, 'wikipediaData'); 69 return { 70 ...entity, 71 type: entityType, 72 relevance: relevanceScore 73 }; 74 }); 75 } 76 77 if (processedData.factsByEntity && typeof processedData.factsByEntity === 'object') { 78 Object.keys(processedData.factsByEntity).forEach(entityKey => { 79 if (Array.isArray(processedData.factsByEntity[entityKey])) { 80 processedData.factsByEntity[entityKey] = processedData.factsByEntity[entityKey].map(entity => { 81 const entityType = classifyEntityType(entity, entity.title, entity.summary); 82 const relevanceScore = calculateEntityRelevance(entity, entity.title, entity.summary, entity.imageUrl, 'factsByEntity'); 83 return { 84 ...entity, 85 type: entityType, 86 relevance: relevanceScore 87 }; 88 }); 89 } 90 }); 91 } 92 93 // Store the processed data with classified entity types 94 window.currentFancyData = processedData; 95 96 console.log('ðºï¸ [FancyUI] Data stored for relationship map:', { 97 hasInfoboxes: !!(data.infoboxes && data.infoboxes.length > 0), 98 infoboxCount: data.infoboxes?.length || 0, 99 hasFactsByEntity: !!(data.factsByEntity), 100 factsByEntityKeys: data.factsByEntity ? Object.keys(data.factsByEntity).length : 0, 101 searchTerm: data.searchTerm, 102 timestamp: window.currentFancyData.timestamp 103 }); 104 105 if (!chatContainer) { 106 console.error("ð¯ [createFancyUIPreview] ERROR: No chatContainer provided"); 107 return; 108 } 109 110 // Remove existing preview if present 111 const existingPreview = chatContainer.querySelector('.fancy-ui-preview'); 112 if (existingPreview) { 113 existingPreview.remove(); 114 } 115 116 // Create main preview container 117 const previewContainer = document.createElement('div'); 118 previewContainer.className = 'fancy-ui-preview'; 119 previewContainer.style.cssText = `
120 margin: 20px 0; 121 padding: 30px; 122 background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); 123 border-radius: 20px; 124 box-shadow: 0 20px 40px rgba(0,0,0,0.1); 125 color: white; 126 position: relative; 127 overflow: hidden; 128 `; 129 130 // Create header with controls 131 const header = document.createElement('div'); 132 header.style.cssText = ` 133 display: flex; 134 justify-content: space-between; 135 align-items: center; 136 margin-bottom: 20px; 137 padding: 0 10px; 138 `; 139 140 const title = document.createElement('h2'); 141 title.textContent = '⨠WikiLive'; 142 title.style.cssText = ` 143 margin: 0; 144 color: white; 145 font-size: 24px; 146 text-shadow: 2px 2px 4px rgba(0,0,0,0.5); 147 `; 148 149 const closeBtn = document.createElement('button'); 150 closeBtn.innerHTML = 'â'; 151 closeBtn.style.cssText = ` 152 padding: 10px 15px; 153 background: rgba(255,255,255,0.2); 154 color: white; 155 border: none; 156 border-radius: 50%; 157 cursor: pointer; 158 font-size: 18px; 159 font-weight: bold; 160 transition: all 0.3s ease; 161 `; 162 163 closeBtn.addEventListener('click', () => { 164 previewContainer.remove(); 165 }); 166 167 header.appendChild(title); 168 header.appendChild(closeBtn); 169 previewContainer.appendChild(header); 170 171 // Create central search term 172 const centralTerm = document.createElement('div'); 173 centralTerm.style.cssText = ` 174 position: relative; 175 z-index: 1; 176 text-align: center; 177 margin: 40px auto; 178 padding: 20px 30px; 179 background: rgba(255,255,255,0.15); 180 border: 2px solid rgba(255,255,255,0.3); 181 border-radius: 50px; 182 font-size: 1.5em; 183 font-weight: bold; 184 backdrop-filter: blur(10px); 185 max-width: 400px; 186 box-shadow: 0 8px 32px rgba(0,0,0,0.1); 187 `; 188 centralTerm.textContent = data.searchTerm || 'Search Results'; 189 previewContainer.appendChild(centralTerm); 190 191 // Unify data structure - Fix parameter mapping 192 const unifiedData = unifyDataForFancyUI( 193 data.wikipediaData || data.infoboxes || [], 194 data.realtimeFacts || data.realtime_facts || [], 195 data.factsByEntity || {}, 196 data 197 ); 198 199 // ð¥ ENHANCED: Image enrichment with multiple data sources and comprehensive logging 200 console.log('[FancyUI] ð¼ï¸ Starting comprehensive image enrichment process'); 201 console.log('[FancyUI] ð Data sources available:', { 202 hasInputData: !!data, 203 hasWindowMetadata: !!(window.wikipediaMetadata), 204 inputDataKeys: data ? Object.keys(data) : [], 205 windowMetadataKeys: window.wikipediaMetadata ? Object.keys(window.wikipediaMetadata) : [] 206 }); 207 208 // ð¥ CRITICAL DEBUG: Log the actual structure of Wikipedia images 209 console.log('[FancyUI] ð DETAILED IMAGE DATA ANALYSIS:'); 210 const inputImages = data && data.wikipediaImages; 211 if (inputImages) { 212 console.log('[FancyUI] Input data.wikipediaImages:', inputImages); 213 console.log('[FancyUI] Input data.wikipediaImages type:', typeof inputImages); 214 console.log('[FancyUI] Input data.wikipediaImages length:', inputImages.length); 215 if (inputImages.length > 0) { 216 console.log('[FancyUI] First image sample:', inputImages[0]); 217 console.log('[FancyUI] First image keys:', Object.keys(inputImages[0])); 218 } 219 } 220 221 if (window.wikipediaMetadata?.wikipediaImages) { 222 console.log('[FancyUI] Window metadata images:', window.wikipediaMetadata.wikipediaImages); 223 console.log('[FancyUI] Window metadata images type:', typeof window.wikipediaMetadata.wikipediaImages); 224 console.log('[FancyUI] Window metadata images length:', window.wikipediaMetadata.wikipediaImages.length); 225 if (window.wikipediaMetadata.wikipediaImages.length > 0) { 226 console.log('[FancyUI] First window image sample:', window.wikipediaMetadata.wikipediaImages[0]); 227 console.log('[FancyUI] First window image keys:', Object.keys(window.wikipediaMetadata.wikipediaImages[0])); 228 } 229 } 230 231 // ð¥ DEBUG: Log unified data structure before enrichment 232 console.log('[FancyUI] ð Unified data before enrichment:', { 233 count: unifiedData.length, 234 entities: unifiedData.map(e => ({ title: e.title, hasImage: !!e.imageUrl })) 235 }); 236 237 // 1. Process images from input data (if available) 238 const dataImages = data && data.wikipediaImages; 239 if (dataImages && Array.isArray(dataImages)) { 240 console.log(`[FancyUI] Processing ${dataImages.length} images from input data`);
241 dataImages.forEach((imageData, index) => { 242 console.log(`[FancyUI] ð Processing input image[${index}]:`, { 243 imageData: imageData, 244 pageTitle: imageData.page_title || imageData.pageTitle || imageData.title, 245 imageUrl: imageData.imageUrl || imageData.image_url || imageData.url, 246 hasImageUrl: !!(imageData.imageUrl || imageData.image_url || imageData.url), 247 imageDataKeys: Object.keys(imageData) 248 }); 249 250 // ð¥ ENHANCED: Try ALL possible title field combinations 251 const pageTitle = imageData.page_title || 252 imageData.pageTitle || 253 imageData.title || 254 imageData.name || 255 imageData.entity || 256 imageData.wikipedia_title || 257 imageData.article_title; 258 259 // ð¥ ENHANCED: Try ALL possible image URL field combinations 260 const imageUrl = imageData.imageUrl || 261 imageData.image_url || 262 imageData.url || 263 imageData.src || 264 imageData.image || 265 imageData.thumbnail || 266 imageData.thumb || 267 imageData.main_image || 268 imageData.main_image_url || 269 imageData.wikipedia_image || 270 imageData.photo || 271 imageData.picture; 272 273 console.log(`[FancyUI] ð Enhanced field extraction for image[${index}]:`, { 274 extractedPageTitle: pageTitle, 275 extractedImageUrl: imageUrl, 276 allFields: Object.keys(imageData), 277 titleFields: Object.keys(imageData).filter(k => k.toLowerCase().includes('title') || k.toLowerCase().includes('name')), 278 imageFields: Object.keys(imageData).filter(k => k.toLowerCase().includes('image') || k.toLowerCase().includes('url') || k.toLowerCase().includes('src') || k.toLowerCase().includes('thumb')) 279 }); 280 281 if (pageTitle && imageUrl) { 282 // Find matching entity with multiple title variations 283 const matchingEntity = unifiedData.find(entity => { 284 const matches = [ 285 entity.title === pageTitle, 286 entity.page_title === pageTitle, 287 entity.title === pageTitle.replace(/_/g, ' '), 288 entity.title.toLowerCase() === pageTitle.toLowerCase(), 289 entity.title.toLowerCase() === pageTitle.replace(/_/g, ' ').toLowerCase(), 290 // Additional fuzzy matching 291 entity.title.includes(pageTitle) || pageTitle.includes(entity.title), 292 entity.title.toLowerCase().includes(pageTitle.toLowerCase()) || pageTitle.toLowerCase().includes(entity.title.toLowerCase()), 293 // Even more flexible matching 294 entity.title.replace(/[^a-zA-Z0-9]/g, '').toLowerCase() === pageTitle.replace(/[^a-zA-Z0-9]/g, '').toLowerCase() 295 ]; 296 return matches.some(match => match); 297 }); 298 299 if (matchingEntity) { 300 matchingEntity.main_image_url = imageUrl; 301 matchingEntity.imageUrl = imageUrl; 302 matchingEntity.image_url = imageUrl; 303 matchingEntity.thumbnail = imageUrl; 304 matchingEntity.wikipediaImage = imageUrl; 305 console.log(`[FancyUI] â INPUT DATA: Applied image to entity "${matchingEntity.title}": ${imageUrl}`); 306 } else { 307 console.log(`[FancyUI] â ï¸ INPUT DATA: No matching entity found for "${pageTitle}"`); 308 console.log(`[FancyUI] Available entity titles:`, unifiedData.map(e => e.title)); 309 } 310 } else { 311 console.log(`[FancyUI] â ï¸ INPUT DATA: Missing pageTitle or imageUrl for image[${index}]`, { 312 pageTitle: pageTitle, 313 imageUrl: imageUrl, 314 availableFields: Object.keys(imageData) 315 }); 316 } 317 }); 318 } else { 319 console.log('[FancyUI] â ï¸ No wikipediaImages found in input data'); 320 } 321 322 // Check window metadata as fallback (SECONDARY SOURCE) 323 if (window.wikipediaMetadata?.wikipediaImages && Array.isArray(window.wikipediaMetadata.wikipediaImages) && window.wikipediaMetadata.wikipediaImages.length > 0) { 324 console.log(`[FancyUI] â Found ${window.wikipediaMetadata.wikipediaImages.length} Wikipedia images in window metadata (fallback)`); 325
326 window.wikipediaMetadata.wikipediaImages.forEach((imageData, index) => { 327 console.log(`[FancyUI] ð Processing window image[${index}]:`, { 328 imageData: imageData, 329 pageTitle: imageData.page_title || imageData.pageTitle || imageData.title, 330 imageUrl: imageData.imageUrl || imageData.image_url || imageData.url, 331 hasImageUrl: !!(imageData.imageUrl || imageData.image_url || imageData.url) 332 }); 333 334 const pageTitle = imageData.page_title || imageData.pageTitle || imageData.title; 335 const imageUrl = imageData.imageUrl || imageData.image_url || imageData.url; 336 337 if (pageTitle && imageUrl) { 338 // Find matching entity 339 const matchingEntity = unifiedData.find(entity => { 340 const matches = [ 341 entity.title === pageTitle, 342 entity.page_title === pageTitle, 343 entity.title === pageTitle.replace(/_/g, ' '), 344 entity.title.toLowerCase() === pageTitle.toLowerCase(), 345 entity.title.toLowerCase() === pageTitle.replace(/_/g, ' ').toLowerCase(), 346 // Additional fuzzy matching 347 entity.title.includes(pageTitle) || pageTitle.includes(entity.title), 348 entity.title.toLowerCase().includes(pageTitle.toLowerCase()) || pageTitle.toLowerCase().includes(entity.title.toLowerCase()) 349 ]; 350 return matches.some(match => match); 351 }); 352 353 if (matchingEntity && !matchingEntity.main_image_url) { 354 matchingEntity.main_image_url = imageUrl; 355 matchingEntity.imageUrl = imageUrl; 356 matchingEntity.image_url = imageUrl; 357 matchingEntity.thumbnail = imageUrl; 358 matchingEntity.wikipediaImage = imageUrl; 359 console.log(`[FancyUI] â WINDOW FALLBACK: Applied image to entity "${matchingEntity.title}": ${imageUrl}`); 360 } 361 } 362 }); 363 } else { 364 console.log('[FancyUI] â ï¸ No wikipediaImages found in window metadata'); 365 } 366 367 // ð¥ FINAL DEBUG: Log unified data structure after enrichment 368 console.log('[FancyUI] ð Unified data after enrichment:', { 369 count: unifiedData.length, 370 entitiesWithImages: unifiedData.filter(e => e.imageUrl || e.main_image_url || e.image_url || e.thumbnail || e.wikipediaImage || e.pageimage || e.image).length, 371 entities: unifiedData.map(e => ({ 372 title: e.title, 373 hasImage: !!(e.imageUrl || e.main_image_url || e.image_url || e.thumbnail || e.wikipediaImage || e.pageimage || e.image), 374 imageUrl: (e.imageUrl || e.main_image_url || e.image_url || e.thumbnail || e.wikipediaImage || e.pageimage || e.image) ? 375 (e.imageUrl || e.main_image_url || e.image_url || e.thumbnail || e.wikipediaImage || e.pageimage || e.image).substring(0, 50) + '...' : null, 376 allImageProps: { 377 imageUrl: !!e.imageUrl, 378 main_image_url: !!e.main_image_url, 379 image_url: !!e.image_url, 380 thumbnail: !!e.thumbnail, 381 wikipediaImage: !!e.wikipediaImage, 382 pageimage: !!e.pageimage, 383 image: !!e.image 384 } 385 })) 386 }); 387 388 // Create infoboxes grid 389 const infoboxesGrid = document.createElement('div'); 390 infoboxesGrid.style.cssText = ` 391 position: relative; 392 z-index: 1; 393 display: grid; 394 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 395 gap: 20px; 396 margin-top: 40px; 397 `; 398 399 unifiedData.forEach((infobox, index) => { 400 const infoboxCard = createEnhancedFancyInfoboxCard(infobox, index); 401 infoboxesGrid.appendChild(infoboxCard); 402 }); 403 404 previewContainer.appendChild(infoboxesGrid); 405 406 // Create image carousel section 407 const imageCarouselSection = createStandaloneImageCarousel(unifiedData); 408 previewContainer.appendChild(imageCarouselSection); 409 410 // Create Wikipedia Story section 411 const wikipediaStorySection = createWikipediaStorySection(data); 412 previewContainer.appendChild(wikipediaStorySection); 413 414 // ð³ Create biographical relationship viewer BEFORE adding the main preview 415 if (typeof createBiographicalRelationshipViewer === 'function') { 416 console.log('[FancyUI] ð³ Creating biographical relationship viewer...'); 417 createBiographicalRelationshipViewer(data, chatContainer); 418 } 419 420 // Add main preview to chat container (biographical viewer is already added above) 421 chatContainer.appendChild(previewContainer); 422 423 // Scroll to the biographical viewer if it exists, otherwise to the preview 424 const bioViewer = chatContainer.querySelector('.biographical-relationship-viewer'); 425 if (bioViewer) { 426 bioViewer.scrollIntoView({ behavior: 'smooth', block: 'start' }); 427 } else { 428 previewContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); 429 } 430
431 // Store data for relationship analysis 432 storeInfoboxData(data); 433 434 } catch (error) { 435 console.error("ð¯ [createFancyUIPreview] CRITICAL ERROR:", error); 436 } 437} 438 439// Helper function to create individual fancy infobox cards 440function createEnhancedFancyInfoboxCard(unifiedEntity, index) { 441 console.log(`[FancyUI] ð¨ Creating enhanced infobox card for: ${unifiedEntity.title}`); 442 443 const card = document.createElement('div'); 444 card.className = 'fancy-infobox-card'; 445 card.style.cssText = ` 446 background: rgba(255,255,255,0.1); 447 border: 1px solid rgba(255,255,255,0.2); 448 border-radius: 15px; 449 padding: 20px; 450 backdrop-filter: blur(10px); 451 transition: all 0.3s ease; 452 cursor: pointer; 453 position: relative; 454 overflow: hidden; 455 margin-bottom: 20px; 456 display: none; /* TEMPORARILY HIDDEN - WikiLive entity cards visually hidden */ 457 `; 458 459 // Create header with title 460 const header = document.createElement('div'); 461 header.style.cssText = ` 462 display: flex; 463 align-items: center; 464 margin-bottom: 15px; 465 border-bottom: 1px solid rgba(255,255,255,0.1); 466 padding-bottom: 10px; 467 `; 468 469 const title = document.createElement('h3'); 470 title.style.cssText = ` 471 margin: 0; 472 color: rgba(255,255,255,0.95); 473 font-size: 1.1em; 474 font-weight: 600; 475 flex: 1; 476 `; 477 title.textContent = unifiedEntity.title; 478 header.appendChild(title); 479 480 // Add Wikipedia link button 481 if (unifiedEntity.url) { 482 const linkButton = document.createElement('a'); 483 linkButton.href = unifiedEntity.url; 484 linkButton.target = '_blank'; 485 linkButton.rel = 'noopener noreferrer'; 486 linkButton.style.cssText = ` 487 background: rgba(255,255,255,0.2); 488 color: rgba(255,255,255,0.9); 489 padding: 6px 12px; 490 border-radius: 8px; 491 text-decoration: none; 492 font-size: 0.8em; 493 transition: all 0.2s ease; 494 margin-left: 10px; 495 `; 496 linkButton.textContent = 'ð Wikipedia'; 497 header.appendChild(linkButton); 498 } 499 500 // Add Relationship Tree button 501 const relationshipTreeButton = document.createElement('a'); 502 relationshipTreeButton.style.cssText = ` 503 background: rgba(255,255,255,0.2); 504 color: rgba(255,255,255,0.9); 505 padding: 6px 12px; 506 border-radius: 8px; 507 text-decoration: none; 508 font-size: 0.8em; 509 transition: all 0.2s ease; 510 margin-left: 10px; 511 cursor: pointer; 512 `; 513 relationshipTreeButton.textContent = 'ð³ Tree'; 514 515 relationshipTreeButton.addEventListener('click', (e) => { 516 e.preventDefault(); 517 const entityName = unifiedEntity.title || unifiedEntity.page_title || unifiedEntity.name; 518 console.log(`ð³ [Fancy UI] Opening relationship tree for: ${entityName}`); 519 if (window.openRelationshipTreeOverlay) { 520 window.openRelationshipTreeOverlay(entityName); 521 } else { 522 console.error('ð³ [Fancy UI] openRelationshipTreeOverlay function not available'); 523 } 524 }); 525 526 relationshipTreeButton.addEventListener('mouseenter', () => { 527 relationshipTreeButton.style.background = 'rgba(255,255,255,0.3)'; 528 }); 529 530 relationshipTreeButton.addEventListener('mouseleave', () => { 531 relationshipTreeButton.style.background = 'rgba(255,255,255,0.2)'; 532 }); 533 534 header.appendChild(relationshipTreeButton); 535 536 card.appendChild(header); 537 538 // Add image if available 539 if (unifiedEntity.imageUrl) { 540 const imageContainer = document.createElement('div'); 541 imageContainer.style.cssText = ` 542 width: 100%; 543 max-height: 200px; 544 overflow: hidden; 545 border-radius: 10px; 546 margin-bottom: 15px; 547 `; 548 549 const img = document.createElement('img'); 550 img.alt = unifiedEntity.title; 551 img.crossOrigin = 'anonymous'; // Enable CORS for Wikipedia images 552 img.style.cssText = `
553 width: 100%; 554 height: 150px; 555 object-fit: cover; 556 background: rgba(255,255,255,0.1); 557 border-radius: 5px; 558 `; 559 560 // Add loading and error handling 561 img.addEventListener('load', () => { 562 console.log(`[FancyUI] â Image loaded for: ${unifiedEntity.title}`); 563 img.style.opacity = '1'; 564 }); 565 566 img.addEventListener('error', (e) => { 567 console.log(`[FancyUI] â Image failed to load for: ${unifiedEntity.title}, trying fallback`); 568 569 // Try fallback image URL (remove thumbnail sizing) 570 const originalUrl = unifiedEntity.imageUrl.replace(/\/thumb\//, '/').replace(/\/\d+px-[^/]+$/, ''); 571 if (originalUrl !== unifiedEntity.imageUrl && !img.dataset.fallbackTried) { 572 img.dataset.fallbackTried = 'true'; 573 console.log(`[FancyUI] ð Trying fallback URL for ${unifiedEntity.title}: ${originalUrl}`); 574 img.src = originalUrl; 575 return; 576 } 577 578 // Final fallback: show placeholder with entity initial 579 img.style.display = 'none'; 580 const placeholder = document.createElement('div'); 581 placeholder.style.cssText = ` 582 width: 100%; 583 height: 150px; 584 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 585 display: flex; 586 align-items: center; 587 justify-content: center; 588 font-size: 48px; 589 font-weight: bold; 590 color: white; 591 border-radius: 5px; 592 `; 593 placeholder.textContent = unifiedEntity.title.charAt(0).toUpperCase(); 594 img.parentNode.insertBefore(placeholder, img); 595 console.log(`[FancyUI] ð· Using placeholder for: ${unifiedEntity.title}`); 596 }); 597 598 // Set image source after event listeners are attached 599 img.src = unifiedEntity.imageUrl; 600 img.style.opacity = '0.7'; // Start with slight transparency 601 img.style.transition = 'opacity 0.3s ease'; 602 603 imageContainer.appendChild(img); 604 card.appendChild(imageContainer); 605 } 606 607 // Add summary 608 const summaryText = unifiedEntity.summary || 609 unifiedEntity.wikipedia_narrative || 610 unifiedEntity.description || 611 unifiedEntity.extract || 612 null; 613 614 if (summaryText) { 615 const summaryDiv = document.createElement('div'); 616 summaryDiv.style.cssText = ` 617 color: rgba(255,255,255,0.8); 618 line-height: 1.6; 619 margin-bottom: 15px; 620 font-size: 0.9em; 621 `; 622 623 const truncatedSummary = summaryText.length > 200 ? 624 summaryText.substring(0, 200) + '...' : 625 summaryText; 626 627 summaryDiv.textContent = truncatedSummary; 628 card.appendChild(summaryDiv); 629 } 630 631 // Add hover effects 632 card.addEventListener('mouseenter', () => { 633 card.style.transform = 'translateY(-5px) scale(1.02)'; 634 card.style.boxShadow = '0 10px 30px rgba(0,0,0,0.3)'; 635 }); 636 637 card.addEventListener('mouseleave', () => { 638 card.style.transform = 'translateY(0) scale(1)'; 639 card.style.boxShadow = 'none'; 640 }); 641 642 return card; 643} 644 645// Unify data structure between main UI and fancy UI with optimization 646function unifyDataForFancyUI(wikipediaData, realtimeFacts, factsByEntity, fullData = null) { 647 console.log('[FancyUI] ð Starting optimized data unification process...'); 648 649 const unifiedData = []; 650 const processedEntities = new Set(); 651 const MAX_ENTITIES = 50; // ð¯ ENHANCED: Increased limit to show more cards as requested 652 653 // Process wikipediaData first (highest priority) 654 if (wikipediaData && Array.isArray(wikipediaData)) { 655 wikipediaData.forEach(entity => { 656 const unifiedEntity = createUnifiedEntityData(entity, 'wikipediaData'); 657 if (unifiedEntity && !processedEntities.has(unifiedEntity.id) && unifiedData.length < MAX_ENTITIES) { 658 unifiedData.push(unifiedEntity); 659 processedEntities.add(unifiedEntity.id); 660 } 661 }); 662 } 663 664 // Process factsByEntity (medium priority) 665 if (factsByEntity && typeof factsByEntity === 'object' && unifiedData.length < MAX_ENTITIES) {
666 Object.entries(factsByEntity).forEach(([entityKey, entityData]) => { 667 if (entityData && Array.isArray(entityData) && unifiedData.length < MAX_ENTITIES) { 668 entityData.forEach(entity => { 669 const unifiedEntity = createUnifiedEntityData(entity, 'factsByEntity'); 670 if (unifiedEntity && !processedEntities.has(unifiedEntity.id) && unifiedData.length < MAX_ENTITIES) { 671 unifiedData.push(unifiedEntity); 672 processedEntities.add(unifiedEntity.id); 673 } 674 }); 675 } 676 }); 677 } 678 679 // Process realtime_facts (lower priority) 680 if (realtimeFacts && Array.isArray(realtimeFacts) && unifiedData.length < MAX_ENTITIES) { 681 realtimeFacts.forEach(entity => { 682 const unifiedEntity = createUnifiedEntityData(entity, 'realtimeFacts'); 683 if (unifiedEntity && !processedEntities.has(unifiedEntity.id) && unifiedData.length < MAX_ENTITIES) { 684 unifiedData.push(unifiedEntity); 685 processedEntities.add(unifiedEntity.id); 686 } 687 }); 688 } 689 690 // Sort by relevance and filter high-quality entities 691 const filteredData = unifiedData 692 .filter(entity => entity.relevance >= 30) // Only keep entities with decent relevance 693 .sort((a, b) => (b.relevance || 0) - (a.relevance || 0)) 694 .slice(0, MAX_ENTITIES); 695 696 console.log(`[FancyUI] ð¯ Optimized data unification complete: ${filteredData.length}/${unifiedData.length} entities (filtered from ${unifiedData.length} total)`); 697 console.log(`[FancyUI] ð Entity types distribution:`, 698 filteredData.reduce((acc, entity) => { 699 acc[entity.type] = (acc[entity.type] || 0) + 1; 700 return acc; 701 }, {}) 702 ); 703 704 return filteredData; 705} 706 707// Create unified entity data structure with type classification and relevance scoring 708function createUnifiedEntityData(rawEntity, source) { 709 if (!rawEntity) return null; 710 711 const title = rawEntity.title || 712 rawEntity.page_title || 713 rawEntity.name || 714 'Unknown Entity'; 715 716 const id = `${title.toLowerCase().replace(/\s+/g, '_')}_${source}`; 717 718 const summary = rawEntity.summary || 719 rawEntity.wikipedia_narrative || 720 rawEntity.description || 721 rawEntity.extract || 722 null; 723 724 const url = rawEntity.url || 725 rawEntity.wikipedia_url || 726 (rawEntity.page_title ? `https://en.wikipedia.org/wiki/${encodeURIComponent(rawEntity.page_title.replace(/ /g, '_'))}` : null); 727 728 const imageUrl = rawEntity.main_image_url || 729 rawEntity.imageUrl || 730 rawEntity.image_url || 731 rawEntity.thumbnail || 732 null; 733 734 // ð¯ ENHANCED: Intelligent entity type classification 735 const entityType = classifyEntityType(rawEntity, title, summary); 736 737 // ð¯ ENHANCED: Calculate relevance score 738 const relevanceScore = calculateEntityRelevance(rawEntity, title, summary, imageUrl, source); 739 740 return { 741 id, 742 title, 743 page_title: rawEntity.page_title || title, 744 summary, 745 url, 746 imageUrl, 747 image_url: imageUrl, 748 thumbnail: imageUrl, 749 main_image_url: imageUrl, 750 source: source, 751 type: entityType, 752 relevance: relevanceScore 753 }; 754} 755 756// ð¯ ENHANCED: Intelligent entity type classification 757function classifyEntityType(rawEntity, title, summary) { 758 // Check explicit type fields first 759 if (rawEntity.type && rawEntity.type !== 'unknown') { 760 return rawEntity.type.toLowerCase(); 761 } 762 763 if (rawEntity.entity_type) { 764 return rawEntity.entity_type.toLowerCase(); 765 } 766 767 if (rawEntity.category) { 768 return rawEntity.category.toLowerCase(); 769 } 770 771 // Analyze title and summary for type indicators 772 const titleLower = title.toLowerCase(); 773 const summaryLower = (summary || '').toLowerCase(); 774 const combinedText = `${titleLower} ${summaryLower}`; 775 776 // Person indicators 777 const personIndicators = [ 778 'actor', 'actress', 'director', 'producer', 'writer', 'author', 'singer', 'musician', 779 'politician', 'president', 'minister', 'senator', 'governor', 'mayor', 780 'scientist', 'researcher', 'professor', 'doctor', 'engineer', 781 'athlete', 'player', 'coach', 'champion', 'winner', 782 'artist', 'painter', 'sculptor', 'photographer', 783 'born', 'died', 'age', 'years old', 'biography' 784 ]; 785 786 // Film/Movie indicators 787 const filmIndicators = [ 788 'film', 'movie', 'cinema', 'directed by', 'starring', 'cast', 789 'box office', 'premiere', 'released', 'screenplay', 'cinematography' 790 ]; 791 792 // Organization indicators 793 const organizationIndicators = [ 794 'company', 'corporation', 'organization', 'foundation', 'institute',
795 'university', 'college', 'school', 'hospital', 'museum', 796 'government', 'agency', 'department', 'ministry', 797 'founded', 'headquarters', 'ceo', 'president' 798 ]; 799 800 // Location indicators 801 const locationIndicators = [ 802 'city', 'town', 'village', 'country', 'state', 'province', 803 'region', 'district', 'county', 'municipality', 804 'capital', 'population', 'area', 'located', 'geography' 805 ]; 806 807 // Award indicators 808 const awardIndicators = [ 809 'award', 'prize', 'trophy', 'medal', 'honor', 'recognition', 810 'academy award', 'oscar', 'grammy', 'emmy', 'golden globe', 811 'nobel', 'pulitzer', 'winner', 'recipient' 812 ]; 813 814 // Event indicators 815 const eventIndicators = [ 816 'event', 'festival', 'conference', 'summit', 'meeting', 817 'war', 'battle', 'conflict', 'revolution', 'movement', 818 'occurred', 'happened', 'took place', 'date' 819 ]; 820 821 // Work/Creation indicators 822 const workIndicators = [ 823 'book', 'novel', 'album', 'song', 'painting', 'sculpture', 824 'play', 'opera', 'symphony', 'composition', 825 'published', 'written', 'created', 'composed' 826 ]; 827 828 // Count matches for each category 829 const scores = { 830 person: personIndicators.filter(indicator => combinedText.includes(indicator)).length, 831 film: filmIndicators.filter(indicator => combinedText.includes(indicator)).length, 832 organization: organizationIndicators.filter(indicator => combinedText.includes(indicator)).length, 833 location: locationIndicators.filter(indicator => combinedText.includes(indicator)).length, 834 award: awardIndicators.filter(indicator => combinedText.includes(indicator)).length, 835 event: eventIndicators.filter(indicator => combinedText.includes(indicator)).length, 836 work: workIndicators.filter(indicator => combinedText.includes(indicator)).length 837 }; 838 839 // Find the category with the highest score 840 const maxScore = Math.max(...Object.values(scores)); 841 if (maxScore > 0) { 842 const bestType = Object.keys(scores).find(key => scores[key] === maxScore); 843 return bestType; 844 } 845 846 // Default fallback 847 return 'unknown'; 848} 849 850// ð¯ ENHANCED: Calculate entity relevance score (0-100) 851function calculateEntityRelevance(rawEntity, title, summary, imageUrl, source) { 852 let score = 50; // Base score 853 854 // Source priority scoring 855 const sourcePriority = { 856 'wikipediaData': 20, 857 'factsByEntity': 15, 858 'realtimeFacts': 10 859 }; 860 score += sourcePriority[source] || 0; 861 862 // Content quality scoring 863 if (summary && summary.length > 100) score += 15; 864 if (summary && summary.length > 300) score += 10; 865 if (imageUrl) score += 10; 866 if (rawEntity.url || rawEntity.wikipedia_url) score += 5; 867 868 // Title quality scoring 869 if (title && title.length > 3 && title !== 'Unknown Entity') score += 10; 870 if (title && !title.includes('disambiguation')) score += 5; 871 872 // Penalize low-quality indicators 873 if (!summary || summary.length < 50) score -= 15; 874 if (title && title.toLowerCase().includes('list of')) score -= 20; 875 if (title && title.toLowerCase().includes('category:')) score -= 25; 876 if (!imageUrl) score -= 5; 877 878 // Additional metadata scoring 879 if (rawEntity.categories && Array.isArray(rawEntity.categories)) { 880 score += Math.min(rawEntity.categories.length * 2, 10); 881 } 882 883 if (rawEntity.infobox && Object.keys(rawEntity.infobox || {}).length > 5) { 884 score += 10; 885 } 886 887 // Ensure score is within bounds 888 return Math.max(0, Math.min(100, score)); 889} 890 891// Function to refresh the image gallery display with new images 892function refreshImageGallery(carouselSection, allImageSources) { 893 // Remove duplicates based on image URL 894 const uniqueImages = []; 895 const seenUrls = new Set(); 896 897 allImageSources.forEach(imageSource => { 898 if (imageSource.imageUrl && !seenUrls.has(imageSource.imageUrl)) { 899 seenUrls.add(imageSource.imageUrl); 900 uniqueImages.push(imageSource); 901 } 902 }); 903 904 console.log(`[Gallery Refresh] Displaying ${uniqueImages.length} unique images (${allImageSources.length - uniqueImages.length} duplicates removed)`); 905 906 // Find the existing carousel container 907 const existingCarousel = carouselSection.querySelector('.image-carousel-container'); 908 if (existingCarousel) { 909 // Update the existing carousel with new images 910 const carousel = existingCarousel.querySelector('.image-carousel'); 911 if (carousel) { 912 // Clear existing images 913 carousel.innerHTML = ''; 914 915 // Add all unique images
916 uniqueImages.forEach((imageSource, index) => { 917 const imageCard = document.createElement('div'); 918 imageCard.className = 'image-card'; 919 imageCard.style.cssText = ` 920 min-width: 200px; 921 margin-right: 15px; 922 border-radius: 10px; 923 overflow: hidden; 924 cursor: pointer; 925 transition: transform 0.3s ease; 926 background: rgba(255,255,255,0.1); 927 border: 1px solid rgba(255,255,255,0.2); 928 display: flex; 929 flex-direction: column; 930 `; 931 932 // Create image container 933 const imageContainer = document.createElement('div'); 934 imageContainer.style.cssText = ` 935 width: 200px; 936 height: 150px; 937 overflow: hidden; 938 border-radius: 10px 10px 0 0; 939 `; 940 941 const img = document.createElement('img'); 942 img.src = imageSource.imageUrl; 943 img.alt = imageSource.title || 'Image'; 944 img.style.cssText = ` 945 width: 100%; 946 height: 100%; 947 object-fit: cover; 948 `; 949 950 // Create text container below image 951 const textContainer = document.createElement('div'); 952 textContainer.className = 'image-caption'; 953 textContainer.style.cssText = ` 954 padding: 8px; 955 color: white; 956 font-size: 11px; 957 font-weight: 500; 958 background: rgba(255,255,255,0.1); 959 text-align: center; 960 line-height: 1.2; 961 width: 200px; 962 max-width: 200px; 963 min-height: 40px; 964 max-height: 40px; 965 overflow: hidden; 966 text-overflow: ellipsis; 967 display: -webkit-box; 968 -webkit-line-clamp: 2; 969 -webkit-box-orient: vertical; 970 word-wrap: break-word; 971 word-break: break-word; 972 box-sizing: border-box; 973 `; 974 975 // Clean up and truncate the title for better display 976 let displayTitle = imageSource.title || 'Image'; 977 978 // Handle duplicated titles (e.g., "File:xyz.jpg - File:xyz.jpg") 979 const parts = displayTitle.split(' - '); 980 if (parts.length === 2 && parts[0] === parts[1]) { 981 displayTitle = parts[0]; 982 } 983 984 // Remove "File:" prefix if present 985 if (displayTitle.startsWith('File:')) { 986 displayTitle = displayTitle.substring(5); 987 } 988 989 // Remove file extensions for cleaner display 990 displayTitle = displayTitle.replace(/\.(jpg|jpeg|png|gif|webp|svg)$/i, ''); 991 992 // Replace underscores with spaces for better readability 993 displayTitle = displayTitle.replace(/_/g, ' '); 994 995 // If still too long, truncate intelligently 996 if (displayTitle.length > 50) { 997 displayTitle = displayTitle.substring(0, 47) + '...'; 998 } 999 1000 textContainer.textContent = displayTitle; 1001 1002 imageContainer.appendChild(img); 1003 imageCard.appendChild(imageContainer); 1004 imageCard.appendChild(textContainer); 1005 1006 // Add click handler for Wikipedia link 1007 if (imageSource.wikipediaUrl) { 1008 imageCard.addEventListener('click', () => { 1009 window.open(imageSource.wikipediaUrl, '_blank'); 1010 }); 1011 imageCard.style.cursor = 'pointer'; 1012 } 1013 1014 // Add hover effect
1015 imageCard.addEventListener('mouseenter', () => { 1016 imageCard.style.transform = 'scale(1.05)'; 1017 }); 1018 imageCard.addEventListener('mouseleave', () => { 1019 imageCard.style.transform = 'scale(1)'; 1020 }); 1021 1022 carousel.appendChild(imageCard); 1023 }); 1024 1025 // Update the header to show total count 1026 const header = carouselSection.querySelector('h3'); 1027 if (header) { 1028 header.textContent = `ð¼ï¸ Image Gallery (${uniqueImages.length} images)`; 1029 } 1030 } 1031 } 1032} 1033 1034// Wikipedia API image fetching function 1035async function fetchWikipediaImages(entityTitle) { 1036 try { 1037 console.log(`[Gallery API] ð Fetching images for: ${entityTitle}`); 1038 1039 // Step 1: Get all image titles (filenames) from the Wikipedia page 1040 const pageTitle = entityTitle.replace(/ /g, '_'); 1041 const imagesApiUrl = `https://en.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(pageTitle)}&prop=images&format=json&imlimit=50&origin=*`; 1042 1043 console.log(`[Gallery API] ð¡ API Request: ${imagesApiUrl}`); 1044 1045 const imagesResponse = await fetch(imagesApiUrl, { 1046 method: 'GET', 1047 headers: { 1048 'Accept': 'application/json', 1049 }, 1050 timeout: 10000 // 10 second timeout 1051 }); 1052 1053 if (!imagesResponse.ok) { 1054 throw new Error(`HTTP ${imagesResponse.status}: ${imagesResponse.statusText}`); 1055 } 1056 1057 const imagesData = await imagesResponse.json(); 1058 1059 const pages = imagesData.query?.pages; 1060 if (!pages) { 1061 console.log(`[Gallery API] No pages found for: ${entityTitle}`); 1062 return []; 1063 } 1064 1065 const pageId = Object.keys(pages)[0]; 1066 const images = pages[pageId]?.images; 1067 1068 if (!images || images.length === 0) { 1069 console.log(`[Gallery API] No images found for: ${entityTitle}`); 1070 return []; 1071 } 1072 1073 console.log(`[Gallery API] ð¸ Found ${images.length} image files for: ${entityTitle}`); 1074 1075 // Step 2: Get image URLs for each file (limit to first 20 to avoid overwhelming) 1076 const imagesToProcess = images.slice(0, 20); 1077 console.log(`[Gallery API] ð Processing ${imagesToProcess.length} images for: ${entityTitle}`); 1078 1079 const imagePromises = imagesToProcess.map(async (image, index) => { 1080 try { 1081 const imageTitle = image.title; 1082 const imageInfoUrl = `https://en.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(imageTitle)}&prop=imageinfo&iiprop=url|size|mime&format=json&origin=*`; 1083 1084 const imageInfoResponse = await fetch(imageInfoUrl, { 1085 method: 'GET', 1086 headers: { 1087 'Accept': 'application/json', 1088 }, 1089 timeout: 8000 // 8 second timeout per image 1090 }); 1091 1092 if (!imageInfoResponse.ok) { 1093 console.warn(`[Gallery API] â ï¸ HTTP ${imageInfoResponse.status} for image: ${imageTitle}`); 1094 return null; 1095 } 1096 1097 const imageInfoData = await imageInfoResponse.json(); 1098 1099 const imagePages = imageInfoData.query?.pages; 1100 if (!imagePages) return null; 1101 1102 const imagePageId = Object.keys(imagePages)[0]; 1103 const imageInfo = imagePages[imagePageId]?.imageinfo?.[0]; 1104 1105 if (!imageInfo?.url) return null; 1106 1107 // Filter out non-image files and very small images 1108 const mime = imageInfo.mime || ''; 1109 const width = imageInfo.width || 0; 1110 const height = imageInfo.height || 0; 1111 1112 if (!mime.startsWith('image/') || width < 100 || height < 100) { 1113 return null; 1114 } 1115 1116 // Skip common Wikipedia interface images 1117 const filename = imageTitle.toLowerCase(); 1118 const skipPatterns = [ 1119 'commons-logo', 'wikimedia', 'edit-icon', 'folder', 'crystal', 1120 'nuvola', 'tango', 'oxygen', 'gnome', 'kde', 'ambox', 'info', 1121 'warning', 'question', 'merge', 'split', 'delete', 'redirect' 1122 ]; 1123 1124 if (skipPatterns.some(pattern => filename.includes(pattern))) { 1125 return null; 1126 } 1127 1128 return { 1129 title: `${entityTitle} - ${imageTitle.replace('File:', '').replace(/\.[^/.]+$/, '')}`, 1130 imageUrl: imageInfo.url, 1131 source: 'wikipedia_api', 1132 entity: { title: entityTitle }, 1133 infoboxField: 'wikipedia_api_images', 1134 wikipediaUrl: `https://en.wikipedia.org/wiki/${encodeURIComponent(pageTitle)}`, 1135 width: width, 1136 height: height, 1137 mime: mime 1138 }; 1139 } catch (error) { 1140 console.warn(`[Gallery API] Error fetching image info for ${image.title}:`, error); 1141 return null; 1142 } 1143 }); 1144 1145 // Wait for all image info requests to complete 1146 const imageResults = await Promise.all(imagePromises); 1147 const validImages = imageResults.filter(img => img !== null); 1148 1149 console.log(`[Gallery API] â Successfully fetched ${validImages.length} valid images for: ${entityTitle}`); 1150 return validImages; 1151 1152 } catch (error) { 1153 console.error(`[Gallery API] Error fetching images for ${entityTitle}:`, error); 1154 return []; 1155 } 1156} 1157 1158// Create standalone image carousel
1159function createStandaloneImageCarousel(unifiedData) { 1160 const carouselSection = document.createElement('div'); 1161 carouselSection.className = 'standalone-image-carousel'; 1162 carouselSection.style.cssText = ` 1163 margin-top: 40px; 1164 padding: 30px; 1165 background: rgba(255,255,255,0.1); 1166 border: 1px solid rgba(255,255,255,0.2); 1167 border-radius: 15px; 1168 backdrop-filter: blur(10px); 1169 `; 1170 1171 const header = document.createElement('h3'); 1172 header.textContent = 'ð¼ï¸ Image Gallery'; 1173 header.style.cssText = ` 1174 margin: 0 0 20px 0; 1175 color: white; 1176 font-size: 20px; 1177 text-align: center; 1178 `; 1179 carouselSection.appendChild(header); 1180 1181 const carousel = document.createElement('div'); 1182 carousel.style.cssText = ` 1183 display: grid; 1184 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 1185 gap: 15px; 1186 padding: 10px 0; 1187 max-height: 600px; 1188 overflow-y: auto; 1189 scroll-behavior: smooth; 1190 `; 1191 1192 // ð¥ SUPER ENHANCED: Aggregate ALL possible images from every source 1193 const allImageSources = []; 1194 1195 // Helper function to extract images from any object recursively 1196 function extractImagesFromObject(obj, entityTitle, source, entity, prefix = '') { 1197 if (!obj || typeof obj !== 'object') return; 1198 1199 Object.entries(obj).forEach(([key, value]) => { 1200 const fullKey = prefix ? `${prefix}.${key}` : key; 1201 1202 // Check if this key suggests an image 1203 const isImageKey = key.toLowerCase().includes('image') || 1204 key.toLowerCase().includes('photo') || 1205 key.toLowerCase().includes('picture') || 1206 key.toLowerCase().includes('thumbnail') || 1207 key.toLowerCase().includes('logo') || 1208 key.toLowerCase().includes('poster') || 1209 key.toLowerCase().includes('cover') || 1210 key.toLowerCase().includes('avatar') || 1211 key.toLowerCase().includes('banner'); 1212 1213 if (isImageKey) { 1214 let imageUrl = null; 1215 1216 // Handle string URLs 1217 if (typeof value === 'string') { 1218 if (value.startsWith('http') || 1219 value.includes('.jpg') || 1220 value.includes('.png') || 1221 value.includes('.jpeg') || 1222 value.includes('.gif') || 1223 value.includes('.webp') || 1224 value.includes('.svg')) { 1225 imageUrl = value; 1226 } 1227 } 1228 // Handle object with URL property 1229 else if (typeof value === 'object' && value !== null) { 1230 imageUrl = value.url || value.src || value.href || value.link; 1231 } 1232 1233 if (imageUrl) { 1234 // Better title and Wikipedia URL handling with field name cleanup 1235 let finalTitle = entityTitle; 1236 let finalWikipediaUrl = null; 1237 1238 // Clean up technical field names from titles 1239 const technicalFields = ['main_image_url', 'imageUrl', 'thumbnail', 'image', 'photo', 'picture', 'logo', 'poster', 'cover', 'avatar', 'banner']; 1240 const isTechnicalField = technicalFields.some(field => key.toLowerCase().includes(field.toLowerCase())); 1241 1242 // Only add field name to title if it's not a technical field and provides meaningful context 1243 if (!isTechnicalField && key !== 'url' && key !== 'src' && key !== 'href') { 1244 finalTitle = `${entityTitle} - ${key}`; 1245 } 1246 1247 // If entity has proper Wikipedia info, use it 1248 if (entity && (entity.wikipediaUrl || entity.url)) { 1249 finalWikipediaUrl = entity.wikipediaUrl || entity.url; 1250 } 1251 // If entity has a title, construct Wikipedia URL 1252 else if (entity && entity.title) { 1253 finalWikipediaUrl = `https://en.wikipedia.org/wiki/${encodeURIComponent(entity.title.replace(/ /g, '_'))}`; 1254 } 1255 // If entityTitle looks like a Wikipedia page title, construct URL 1256 else if (entityTitle && entityTitle !== 'Global Metadata' && !entityTitle.includes('Image')) { 1257 finalWikipediaUrl = `https://en.wikipedia.org/wiki/${encodeURIComponent(entityTitle.replace(/ /g, '_'))}`; 1258 } 1259 1260 allImageSources.push({ 1261 title: finalTitle, 1262 imageUrl: imageUrl, 1263 source: source, 1264 entity: entity, 1265 infoboxField: fullKey,
1266 wikipediaUrl: finalWikipediaUrl 1267 }); 1268 } 1269 } 1270 1271 // Recursively search nested objects and arrays 1272 if (typeof value === 'object' && value !== null) { 1273 if (Array.isArray(value)) { 1274 value.forEach((item, index) => { 1275 extractImagesFromObject(item, entityTitle, source, entity, `${fullKey}[${index}]`); 1276 }); 1277 } else { 1278 extractImagesFromObject(value, entityTitle, source, entity, fullKey); 1279 } 1280 } 1281 }); 1282 } 1283 1284 // 1. Collect images from main entities (expanded search) 1285 unifiedData.forEach(entity => { 1286 // Main image fields (expanded list) 1287 const mainImageFields = [ 1288 'imageUrl', 'main_image_url', 'image_url', 'thumbnail', 'wikipediaImage', 1289 'pageimage', 'image', 'photo', 'picture', 'logo', 'poster', 'cover', 1290 'avatar', 'banner', 'profileImage', 'mainImage', 'primaryImage' 1291 ]; 1292 1293 mainImageFields.forEach(field => { 1294 const imageValue = entity[field]; 1295 if (imageValue && typeof imageValue === 'string' && 1296 (imageValue.startsWith('http') || imageValue.includes('.jpg') || 1297 imageValue.includes('.png') || imageValue.includes('.jpeg'))) { 1298 allImageSources.push({ 1299 title: entity.title, 1300 imageUrl: imageValue, 1301 source: 'main_entity', 1302 entity: entity, 1303 wikipediaUrl: entity.wikipediaUrl || entity.url 1304 }); 1305 } 1306 }); 1307 1308 // 2. Deep extraction from infobox data 1309 if (entity.infobox) { 1310 extractImagesFromObject(entity.infobox, entity.title, 'infobox', entity); 1311 } 1312 1313 // 3. Deep extraction from entire entity (catch any missed images) 1314 extractImagesFromObject(entity, entity.title, 'entity_deep_scan', entity); 1315 1316 // 4. Check for related_links images (enhanced) 1317 if (entity.related_links && Array.isArray(entity.related_links)) { 1318 entity.related_links.forEach(link => { 1319 // Check multiple image fields in related links 1320 const imageFields = ['image', 'imageUrl', 'thumbnail', 'photo', 'picture', 'logo', 'poster']; 1321 imageFields.forEach(field => { 1322 const linkImage = link[field]; 1323 if (linkImage && typeof linkImage === 'string' && 1324 (linkImage.startsWith('http') || linkImage.includes('.jpg') || 1325 linkImage.includes('.png') || linkImage.includes('.jpeg'))) { 1326 allImageSources.push({ 1327 title: link.title || `${entity.title} - Related`, 1328 imageUrl: linkImage, 1329 source: 'related_links', 1330 entity: entity, 1331 relatedEntity: link, 1332 wikipediaUrl: link.url || link.wikipediaUrl 1333 }); 1334 } 1335 }); 1336 1337 // Deep scan related link objects for nested images 1338 extractImagesFromObject(link, link.title || `${entity.title} - Related`, 'related_links_deep', entity); 1339 }); 1340 } 1341 }); 1342 1343 // 5. Use images fetched by image-gallery.js for progressive loading 1344 console.log(`[Gallery API] Checking for globally fetched images from image-gallery.js`); 1345 console.log(`[Gallery API] Available global images:`, window.fetchedGalleryImages); 1346 1347 // ð ENHANCED DEBUG: Show entity titles vs stored image keys 1348 if (window.fetchedGalleryImages) { 1349 const storedKeys = Object.keys(window.fetchedGalleryImages); 1350 const entityTitles = unifiedData.map(e => e.title).filter(Boolean); 1351 1352 console.log(`[Gallery API] ð TITLE MATCHING DEBUG:`); 1353 console.log(`[Gallery API] ð Stored image keys (${storedKeys.length}):`, storedKeys); 1354 console.log(`[Gallery API] ð¯ Entity titles (${entityTitles.length}):`, entityTitles); 1355 1356 // Check for exact matches 1357 const exactMatches = entityTitles.filter(title => storedKeys.includes(title)); 1358 const missingMatches = entityTitles.filter(title => !storedKeys.includes(title)); 1359 1360 console.log(`[Gallery API] â Exact matches (${exactMatches.length}):`, exactMatches); 1361 console.log(`[Gallery API] â Missing matches (${missingMatches.length}):`, missingMatches); 1362 1363 // Check for partial matches (case insensitive)
1364 missingMatches.forEach(entityTitle => { 1365 const partialMatches = storedKeys.filter(key => 1366 key.toLowerCase().includes(entityTitle.toLowerCase()) || 1367 entityTitle.toLowerCase().includes(key.toLowerCase()) 1368 ); 1369 if (partialMatches.length > 0) { 1370 console.log(`[Gallery API] ð Potential partial match for "${entityTitle}":`, partialMatches); 1371 } 1372 }); 1373 } 1374 1375 // Process entities to get their fetched images 1376 const batchSize = 5; // Process 5 entities at a time 1377 const entityBatches = []; 1378 1379 for (let i = 0; i < unifiedData.length; i += batchSize) { 1380 entityBatches.push(unifiedData.slice(i, i + batchSize)); 1381 } 1382 1383 // Process each batch sequentially with delay 1384 let totalApiFetched = 0; 1385 1386 const processBatch = async (batchIndex) => { 1387 if (batchIndex >= entityBatches.length) { 1388 console.log(`[Gallery API] â Completed all batches. Total API images fetched: ${totalApiFetched}`); 1389 console.log(`[Gallery API] ð¯ Final gallery refresh with ALL ${allImageSources.length} images`); 1390 // NOW refresh gallery with ALL accumulated images 1391 refreshImageGallery(carouselSection, allImageSources); 1392 return; 1393 } 1394 1395 const batch = entityBatches[batchIndex]; 1396 console.log(`[Gallery API] Processing batch ${batchIndex + 1}/${entityBatches.length} (${batch.length} entities)`); 1397 1398 const batchPromises = batch.map(async (entity) => { 1399 if (!entity.title || !window.fetchedGalleryImages) { 1400 console.log(`[Gallery API] No title or global images available for entity:`, entity); 1401 return { entity: entity.title || 'unknown', images: [] }; 1402 } 1403 1404 // ð ENHANCED MATCHING: Try multiple matching strategies 1405 let globalImages = null; 1406 let matchedKey = null; 1407 1408 // Strategy 1: Exact match 1409 if (window.fetchedGalleryImages[entity.title]) { 1410 globalImages = window.fetchedGalleryImages[entity.title]; 1411 matchedKey = entity.title; 1412 console.log(`[Gallery API] â Exact match found for: ${entity.title}`); 1413 } 1414 // Strategy 2: Case-insensitive exact match 1415 else { 1416 const storedKeys = Object.keys(window.fetchedGalleryImages); 1417 const caseInsensitiveMatch = storedKeys.find(key => 1418 key.toLowerCase() === entity.title.toLowerCase() 1419 ); 1420 if (caseInsensitiveMatch) { 1421 globalImages = window.fetchedGalleryImages[caseInsensitiveMatch]; 1422 matchedKey = caseInsensitiveMatch; 1423 console.log(`[Gallery API] â Case-insensitive match: "${entity.title}" -> "${caseInsensitiveMatch}"`); 1424 } 1425 } 1426 // Strategy 3: Partial match (contains) 1427 if (!globalImages) { 1428 const storedKeys = Object.keys(window.fetchedGalleryImages); 1429 const partialMatch = storedKeys.find(key => 1430 key.toLowerCase().includes(entity.title.toLowerCase()) || 1431 entity.title.toLowerCase().includes(key.toLowerCase()) 1432 ); 1433 if (partialMatch) { 1434 globalImages = window.fetchedGalleryImages[partialMatch]; 1435 matchedKey = partialMatch; 1436 console.log(`[Gallery API] â Partial match: "${entity.title}" -> "${partialMatch}"`); 1437 } 1438 } 1439 1440 if (globalImages && globalImages.imageUrls && globalImages.imageUrls.length > 0) { 1441 console.log(`[Gallery API] ð¯ Found ${globalImages.totalImages || globalImages.imageUrls.length} globally stored images for: ${entity.title} (matched via: ${matchedKey})`); 1442 1443 // Convert global image URLs to the expected format 1444 const formattedImages = globalImages.imageUrls.map(url => ({ 1445 url: url, 1446 title: `Image from ${entity.title}`, 1447 source: 'global_fetch', 1448 entity: entity.title, 1449 matchedKey: matchedKey 1450 })); 1451 1452 return { entity: entity.title, images: formattedImages }; 1453 } else { 1454 console.log(`[Gallery API] â No globally stored images found for: ${entity.title}`); 1455 return { entity: entity.title, images: [] }; 1456 } 1457 }); 1458 1459 try { 1460 const batchResults = await Promise.all(batchPromises); 1461 1462 // Add batch results to gallery
1463 batchResults.forEach(result => { 1464 if (result.images.length > 0) { 1465 console.log(`[Gallery API] Adding ${result.images.length} images for ${result.entity}`); 1466 result.images.forEach(apiImage => { 1467 allImageSources.push(apiImage); 1468 }); 1469 totalApiFetched += result.images.length; 1470 } 1471 }); 1472 1473 // Progressive loading: refresh gallery after each batch with accumulated images 1474 console.log(`[Gallery API] Batch ${batchIndex + 1} completed. Current total images: ${allImageSources.length}`); 1475 console.log(`[Gallery API] ð Progressive refresh with ${allImageSources.length} images`); 1476 refreshImageGallery(carouselSection, allImageSources); 1477 1478 // Process next batch after a short delay 1479 setTimeout(() => { 1480 processBatch(batchIndex + 1); 1481 }, 1000); // 1 second delay between batches 1482 1483 } catch (error) { 1484 console.error(`[Gallery API] Error processing batch ${batchIndex + 1}:`, error); 1485 // Continue with next batch even if this one fails 1486 setTimeout(() => { 1487 processBatch(batchIndex + 1); 1488 }, 2000); 1489 } 1490 }; 1491 1492 // Start processing batches 1493 processBatch(0); 1494 1495 // 6. Add images from global Wikipedia metadata (WITH CORRECT PAGE ASSOCIATIONS) 1496 if (window.wikipediaMetadata) { 1497 console.log(`[Gallery] ð Scanning window.wikipediaMetadata:`, window.wikipediaMetadata); 1498 1499 // Create a mapping of available entities for proper page association 1500 const entityMapping = new Map(); 1501 unifiedData.forEach(entity => { 1502 if (entity.title) { 1503 const normalizedTitle = entity.title.toLowerCase(); 1504 entityMapping.set(normalizedTitle, entity); 1505 entityMapping.set(entity.title.replace(/ /g, '_').toLowerCase(), entity); 1506 entityMapping.set(entity.title.replace(/_/g, ' ').toLowerCase(), entity); 1507 } 1508 }); 1509 1510 console.log(`[Gallery] Available entities for page association:`, Array.from(entityMapping.keys())); 1511 1512 // Extract from wikipediaImages array with proper page association and filtering 1513 if (window.wikipediaMetadata.wikipediaImages && Array.isArray(window.wikipediaMetadata.wikipediaImages)) { 1514 console.log(`[Gallery] Found ${window.wikipediaMetadata.wikipediaImages.length} images in wikipediaImages array`); 1515 window.wikipediaMetadata.wikipediaImages.forEach((imageData, index) => { 1516 // Better page title extraction 1517 let pageTitle = imageData.page_title || imageData.pageTitle || imageData.title; 1518 let wikipediaUrl = imageData.wikipediaUrl || imageData.url; 1519 1520 // If no proper page title, try to extract from URL or use a more descriptive fallback 1521 if (!pageTitle || pageTitle.includes('main_image_url') || pageTitle.includes('image_url')) { 1522 if (wikipediaUrl && wikipediaUrl.includes('wikipedia.org/wiki/')) { 1523 // Extract page title from Wikipedia URL 1524 const urlParts = wikipediaUrl.split('/wiki/'); 1525 if (urlParts.length > 1) { 1526 pageTitle = decodeURIComponent(urlParts[1]).replace(/_/g, ' '); 1527 } 1528 } else { 1529 // Skip images without proper page association 1530 console.log(`[Gallery] Skipping image without proper page title: ${imageData}`); 1531 return; 1532 } 1533 } 1534 1535 // ð¯ CRITICAL FIX: Show ALL images regardless of entity matching 1536 // This ensures all fetched Wikipedia images appear in the gallery 1537 const pageTitleLower = pageTitle.toLowerCase(); 1538 let associatedEntity = null; 1539 1540 // Try to find the correct entity for this image's page 1541 associatedEntity = entityMapping.get(pageTitleLower) || 1542 entityMapping.get(pageTitleLower.replace(/ /g, '_')) || 1543 entityMapping.get(pageTitleLower.replace(/_/g, ' ')); 1544 1545 // ð¥ ALWAYS INCLUDE IMAGES: Create entity representation for ALL images 1546 if (!associatedEntity) { 1547 console.log(`[Gallery] â Including image from: ${pageTitle} (creating entity representation)`); 1548 associatedEntity = { 1549 title: pageTitle, 1550 wikipediaUrl: wikipediaUrl || `https://en.wikipedia.org/wiki/${encodeURIComponent(pageTitle.replace(/ /g, '_'))}` 1551 }; 1552 } else { 1553 console.log(`[Gallery] â Including image from matched entity: ${associatedEntity.title}`); 1554 } 1555 1556 console.log(`[Gallery] Associating image with correct entity: ${associatedEntity.title}`); 1557 1558 console.log(`[Gallery] Adding image from wikipediaImages: ${imageData.url || imageData.imageUrl}`); 1559 allImageSources.push({ 1560 title: `${associatedEntity.title} - ${imageData.title || 'Image'}`, 1561 imageUrl: imageData.url || imageData.imageUrl, 1562 source: 'global_metadata_wikipedia_images', 1563 entity: associatedEntity, 1564 infoboxField: 'wikipediaImages', 1565 wikipediaUrl: associatedEntity.wikipediaUrl || `https://en.wikipedia.org/wiki/${encodeURIComponent(associatedEntity.title.replace(/ /g, '_'))}` 1566 }); 1567 }); 1568 } 1569 1570 // Extract from any other image collections in metadata (WITH CORRECT PAGE ASSOCIATIONS)
1571 Object.keys(window.wikipediaMetadata).forEach(key => { 1572 if (key.toLowerCase().includes('image') && Array.isArray(window.wikipediaMetadata[key])) { 1573 console.log(`[Gallery] Found additional image array: ${key} with ${window.wikipediaMetadata[key].length} items`); 1574 window.wikipediaMetadata[key].forEach((item, index) => { 1575 if (typeof item === 'string' && (item.startsWith('http') || item.includes('.jpg') || item.includes('.png'))) { 1576 // Skip string URLs without entity association - they can't be properly associated 1577 console.log(`[Gallery] Skipping unassociated string image URL from ${key}`); 1578 return; 1579 } else if (typeof item === 'object' && item) { 1580 const imgUrl = item.url || item.imageUrl || item.src; 1581 if (imgUrl) { 1582 // Check if this image has page association 1583 const itemPageTitle = item.page_title || item.pageTitle || item.title; 1584 if (itemPageTitle) { 1585 const itemPageTitleLower = itemPageTitle.toLowerCase(); 1586 let itemAssociatedEntity = entityMapping.get(itemPageTitleLower) || 1587 entityMapping.get(itemPageTitleLower.replace(/ /g, '_')) || 1588 entityMapping.get(itemPageTitleLower.replace(/_/g, ' ')); 1589 1590 if (!itemAssociatedEntity) { 1591 console.log(`[Gallery] â Including image from extra metadata: ${itemPageTitle} (creating entity representation)`); 1592 itemAssociatedEntity = { 1593 title: itemPageTitle, 1594 wikipediaUrl: `https://en.wikipedia.org/wiki/${encodeURIComponent(itemPageTitle.replace(/ /g, '_'))}` 1595 }; 1596 } 1597 1598 console.log(`[Gallery] Associating extra metadata image with: ${itemAssociatedEntity.title}`); 1599 1600 allImageSources.push({ 1601 title: `${itemAssociatedEntity.title} - ${item.title || item.name || 'Image'}`, 1602 imageUrl: imgUrl, 1603 source: 'global_metadata_extra', 1604 entity: itemAssociatedEntity, 1605 wikipediaUrl: itemAssociatedEntity.wikipediaUrl || `https://en.wikipedia.org/wiki/${encodeURIComponent(itemAssociatedEntity.title.replace(/ /g, '_'))}` 1606 }); 1607 } else { 1608 console.log(`[Gallery] Skipping image without page association from ${key}`); 1609 return; 1610 } 1611 } 1612 } 1613 }); 1614 } 1615 }); 1616 1617 // Enhanced metadata scanning - check for page-specific image data (WITH CORRECT ASSOCIATIONS) 1618 if (window.wikipediaMetadata.factsByEntity && Array.isArray(window.wikipediaMetadata.factsByEntity)) { 1619 console.log(`[Gallery] Scanning factsByEntity for page-specific images`); 1620 window.wikipediaMetadata.factsByEntity.forEach(entityData => { 1621 if (entityData && entityData.pageTitle) { 1622 const pageTitle = entityData.pageTitle; 1623 const pageTitleLower = pageTitle.toLowerCase(); 1624 1625 // CRITICAL FIX: Ensure correct entity association for factsByEntity data 1626 let factsAssociatedEntity = entityMapping.get(pageTitleLower) || 1627 entityMapping.get(pageTitleLower.replace(/ /g, '_')) || 1628 entityMapping.get(pageTitleLower.replace(/_/g, ' ')); 1629 1630 if (!factsAssociatedEntity) { 1631 console.log(`[Gallery] No matching entity found for factsByEntity page: ${pageTitle} - skipping`); 1632 return; 1633 } 1634 1635 console.log(`[Gallery] Processing factsByEntity data for: ${factsAssociatedEntity.title}`); 1636 1637 // Extract images from this entity's data with correct association 1638 extractImagesFromObject(entityData, factsAssociatedEntity.title, 'global_metadata_facts', factsAssociatedEntity); 1639 } 1640 }); 1641 } 1642 1643 // Check for any other page-specific data structures (WITH CORRECT ASSOCIATIONS)
1644 Object.keys(window.wikipediaMetadata).forEach(key => { 1645 const data = window.wikipediaMetadata[key]; 1646 if (Array.isArray(data)) { 1647 data.forEach((item, index) => { 1648 if (item && typeof item === 'object' && (item.pageTitle || item.page_title)) { 1649 const pageTitle = item.pageTitle || item.page_title; 1650 const pageTitleLower = pageTitle.toLowerCase(); 1651 1652 // CRITICAL FIX: Ensure correct entity association for other metadata 1653 let otherAssociatedEntity = entityMapping.get(pageTitleLower) || 1654 entityMapping.get(pageTitleLower.replace(/ /g, '_')) || 1655 entityMapping.get(pageTitleLower.replace(/_/g, ' ')); 1656 1657 if (!otherAssociatedEntity) { 1658 console.log(`[Gallery] â Including image from ${key}: ${pageTitle} (creating entity representation)`); 1659 otherAssociatedEntity = { 1660 title: pageTitle, 1661 wikipediaUrl: `https://en.wikipedia.org/wiki/${encodeURIComponent(pageTitle.replace(/ /g, '_'))}` 1662 }; 1663 } 1664 1665 console.log(`[Gallery] Processing ${key} data for: ${otherAssociatedEntity.title}`); 1666 1667 // Extract images from this page-specific item with correct association 1668 extractImagesFromObject(item, otherAssociatedEntity.title, `global_metadata_${key}`, otherAssociatedEntity); 1669 } 1670 }); 1671 } 1672 }); 1673 1674 // Deep scan the entire metadata object for any missed images 1675 extractImagesFromObject(window.wikipediaMetadata, 'Global Metadata', 'metadata_deep_scan', null); 1676 } 1677 1678 // Remove duplicates based on image URL 1679 const uniqueImages = []; 1680 const seenUrls = new Set(); 1681 1682 allImageSources.forEach(imageSource => { 1683 if (!seenUrls.has(imageSource.imageUrl)) { 1684 seenUrls.add(imageSource.imageUrl); 1685 uniqueImages.push(imageSource); 1686 } 1687 }); 1688 1689 console.log(`[Gallery] ð¥ SUPER ENHANCED IMAGE AGGREGATION RESULTS:`); 1690 console.log(`[Gallery] Total image sources found: ${allImageSources.length}`); 1691 console.log(`[Gallery] Unique images after deduplication: ${uniqueImages.length}`); 1692 1693 const sourceBreakdown = { 1694 main_entity: allImageSources.filter(s => s.source === 'main_entity').length, 1695 infobox: allImageSources.filter(s => s.source === 'infobox').length, 1696 entity_deep_scan: allImageSources.filter(s => s.source === 'entity_deep_scan').length, 1697 related_links: allImageSources.filter(s => s.source === 'related_links').length, 1698 related_links_deep: allImageSources.filter(s => s.source === 'related_links_deep').length, 1699 global_metadata: allImageSources.filter(s => s.source === 'global_metadata').length, 1700 global_metadata_extra: allImageSources.filter(s => s.source === 'global_metadata_extra').length, 1701 entity_metadata: allImageSources.filter(s => s.source === 'entity_metadata').length, 1702 page_specific_metadata: allImageSources.filter(s => s.source === 'page_specific_metadata').length, 1703 metadata_deep_scan: allImageSources.filter(s => s.source === 'metadata_deep_scan').length 1704 }; 1705 1706 console.log(`[Gallery] ð Image sources breakdown:`, sourceBreakdown); 1707 1708 // Show sample images from each source 1709 Object.keys(sourceBreakdown).forEach(source => { 1710 const sourceImages = allImageSources.filter(s => s.source === source); 1711 if (sourceImages.length > 0) { 1712 console.log(`[Gallery] ð¼ï¸ Sample ${source} images:`, sourceImages.slice(0, 3).map(img => ({ 1713 title: img.title, 1714 url: img.imageUrl.substring(0, 80) + (img.imageUrl.length > 80 ? '...' : '') 1715 }))); 1716 } 1717 }); 1718 1719 console.log(`[Gallery] ð FINAL GALLERY: Will display ${uniqueImages.length} unique images`); 1720 1721 const entitiesWithImages = uniqueImages; 1722 1723 console.log(`[Gallery] Found ${entitiesWithImages.length} entities with images out of ${unifiedData.length} total entities`); 1724 1725 if (entitiesWithImages.length === 0) { 1726 console.log('[Gallery] No images available - showing fallback message'); 1727 const noImagesMsg = document.createElement('div'); 1728 noImagesMsg.textContent = 'No images available'; 1729 noImagesMsg.style.cssText = `
1730 color: rgba(255,255,255,0.6); 1731 text-align: center; 1732 padding: 20px; 1733 font-style: italic; 1734 `; 1735 carouselSection.appendChild(noImagesMsg); 1736 return carouselSection; 1737 } 1738 1739 console.log(`[Gallery] Creating carousel with ${entitiesWithImages.length} images`); 1740 1741 // ð DEBUG: Show Gallery JSON for carousel 1742 console.log('ð¯ [Gallery JSON for carousel] Complete data structure:'); 1743 console.log('ð Gallery JSON:', JSON.stringify({ 1744 totalEntities: entitiesWithImages.length, 1745 entities: entitiesWithImages.map(entity => ({ 1746 title: entity.title, 1747 imageUrl: entity.imageUrl, 1748 main_image_url: entity.main_image_url, 1749 image_url: entity.image_url, 1750 thumbnail: entity.thumbnail, 1751 wikipediaImage: entity.wikipediaImage, 1752 pageimage: entity.pageimage, 1753 image: entity.image, 1754 selectedImageUrl: entity.imageUrl || entity.main_image_url || entity.image_url || entity.thumbnail || entity.wikipediaImage || entity.pageimage || entity.image 1755 })) 1756 }, null, 2)); 1757 1758 console.log('ð¯ [Gallery JSON for carousel] Enhanced image sources being used:'); 1759 entitiesWithImages.forEach((imageSource, index) => { 1760 console.log(`${index + 1}. ${imageSource.title} (${imageSource.source}): ${imageSource.imageUrl}`); 1761 }); 1762 1763 entitiesWithImages.forEach((imageSource, index) => { 1764 // Use the pre-selected image URL from our enhanced aggregation 1765 const imageUrl = imageSource.imageUrl; 1766 1767 console.log(`[Gallery] Creating carousel item ${index + 1} for "${imageSource.title}" with image: ${imageUrl}`); 1768 1769 const item = document.createElement('div'); 1770 item.style.cssText = ` 1771 flex: 0 0 200px; 1772 background: rgba(255,255,255,0.1); 1773 border: 1px solid rgba(255,255,255,0.2); 1774 border-radius: 10px; 1775 overflow: hidden; 1776 cursor: pointer; 1777 transition: transform 0.3s ease; 1778 position: relative; 1779 overflow: hidden; 1780 margin-bottom: 20px; 1781 `; 1782 1783 const img = document.createElement('img'); 1784 img.src = imageUrl; 1785 img.alt = imageSource.title; 1786 img.crossOrigin = 'anonymous'; // Enable CORS for Wikipedia images 1787 img.style.cssText = ` 1788 width: 100%; 1789 height: 150px; 1790 object-fit: cover; 1791 background: rgba(255,255,255,0.1); 1792 border-radius: 5px; 1793 `; 1794 1795 // Add loading and error handling 1796 img.addEventListener('load', () => { 1797 console.log(`[FancyUI] â Image loaded for: ${imageSource.title}`); 1798 img.style.opacity = '1'; 1799 }); 1800 1801 img.addEventListener('error', (e) => { 1802 console.log(`[FancyUI] â Image failed to load for: ${imageSource.title}, trying fallback`); 1803 1804 // Try fallback image URL (remove thumbnail sizing) 1805 const originalUrl = imageUrl.replace(/\/thumb\//, '/').replace(/\/\d+px-[^/]+$/, ''); 1806 if (originalUrl !== imageUrl && !img.dataset.fallbackTried) { 1807 img.dataset.fallbackTried = 'true'; 1808 console.log(`[FancyUI] ð Trying fallback URL for ${imageSource.title}: ${originalUrl}`); 1809 img.src = originalUrl; 1810 } else { 1811 console.log(`[FancyUI] â ï¸ All image URLs failed for: ${imageSource.title}`); 1812 img.style.display = 'none'; 1813 } 1814 }); 1815 1816 const caption = document.createElement('div'); 1817 caption.textContent = imageSource.title; 1818 caption.style.cssText = ` 1819 padding: 10px; 1820 color: white; 1821 font-size: 14px; 1822 text-align: center; 1823 background: rgba(0,0,0,0.7); 1824 position: absolute; 1825 bottom: 0; 1826 left: 0; 1827 right: 0; 1828 backdrop-filter: blur(5px); 1829 `; 1830 1831 item.appendChild(img); 1832 item.appendChild(caption); 1833 1834 item.addEventListener('mouseenter', () => { 1835 item.style.transform = 'scale(1.05)'; 1836 }); 1837 1838 item.addEventListener('mouseleave', () => { 1839 item.style.transform = 'scale(1)'; 1840 }); 1841 1842 // ð Make image clickable - link to Wikipedia page 1843 item.addEventListener('click', () => { 1844 const wikipediaUrl = imageSource.wikipediaUrl || (imageSource.entity && (imageSource.entity.wikipediaUrl || imageSource.entity.url)); 1845 if (wikipediaUrl) { 1846 console.log(`[FancyUI] ð Opening Wikipedia page: ${wikipediaUrl}`); 1847 window.open(wikipediaUrl, '_blank'); 1848 } else { 1849 console.log(`[FancyUI] â ï¸ No Wikipedia URL found for: ${imageSource.title}`); 1850 } 1851 }); 1852 1853 // Add visual feedback for clickable items 1854 item.style.cursor = 'pointer'; 1855 item.title = `Click to view ${imageSource.title} on Wikipedia`; 1856 1857 carousel.appendChild(item); 1858 }); 1859 1860 carouselSection.appendChild(carousel); 1861 return carouselSection; 1862} 1863 1864// Create Wikipedia Story section 1865function createWikipediaStorySection(data) { 1866 const storySection = document.createElement('div'); 1867 storySection.className = 'wikipedia-story-section'; 1868 storySection.style.cssText = `
1869 margin-top: 40px; 1870 padding: 30px; 1871 background: rgba(255,255,255,0.1); 1872 border: 1px solid rgba(255,255,255,0.2); 1873 border-radius: 15px; 1874 backdrop-filter: blur(10px); 1875 position: relative; 1876 overflow: hidden; 1877 `; 1878 1879 // Create header 1880 const header = document.createElement('div'); 1881 header.style.cssText = ` 1882 display: flex; 1883 align-items: center; 1884 margin-bottom: 20px; 1885 padding: 0 10px; 1886 `; 1887 1888 const headerIcon = document.createElement('span'); 1889 headerIcon.textContent = 'ð'; 1890 headerIcon.style.cssText = ` 1891 font-size: 24px; 1892 margin-right: 10px; 1893 `; 1894 1895 const headerTitle = document.createElement('h3'); 1896 headerTitle.textContent = `Wikipedia Story: ${data.searchTerm}`; 1897 headerTitle.style.cssText = ` 1898 margin: 0; 1899 font-size: 20px; 1900 font-weight: 600; 1901 color: white; 1902 flex: 1; 1903 `; 1904 1905 const generateButton = document.createElement('button'); 1906 generateButton.textContent = 'ð Generate Story'; 1907 generateButton.className = 'generate-story-btn'; 1908 generateButton.style.cssText = ` 1909 padding: 12px 24px; 1910 background: linear-gradient(135deg, #4CAF50, #45a049); 1911 color: white; 1912 border: none; 1913 border-radius: 25px; 1914 font-size: 14px; 1915 font-weight: 600; 1916 cursor: pointer; 1917 transition: all 0.3s ease; 1918 margin-right: 10px; 1919 box-shadow: 0 2px 10px rgba(76,175,80,0.3); 1920 `; 1921 1922 let contentArea; 1923 generateButton.addEventListener('mouseover', () => { 1924 generateButton.style.transform = 'scale(1.05)'; 1925 generateButton.style.boxShadow = '0 5px 15px rgba(76,175,80,0.4)'; 1926 }); 1927 1928 generateButton.addEventListener('mouseleave', () => { 1929 generateButton.style.transform = 'scale(1)'; 1930 generateButton.style.boxShadow = '0 2px 10px rgba(76,175,80,0.3)'; 1931 }); 1932 1933 generateButton.addEventListener('click', () => { 1934 generateWikipediaStory(data, contentArea, generateButton); 1935 }); 1936 1937 // Create Wikipedia Map button 1938 const mapButton = document.createElement('button'); 1939 mapButton.textContent = 'ðºï¸ Wikipedia Map'; 1940 mapButton.className = 'wikipedia-map-btn'; 1941 mapButton.style.cssText = ` 1942 padding: 12px 24px; 1943 background: linear-gradient(135deg, #ff6b6b, #ee5a24); 1944 color: white; 1945 border: none; 1946 border-radius: 25px; 1947 font-size: 14px; 1948 font-weight: 600; 1949 cursor: pointer; 1950 transition: all 0.3s ease; 1951 margin-right: 10px; 1952 box-shadow: 0 2px 10px rgba(255,107,107,0.3); 1953 `; 1954 1955 mapButton.addEventListener('mouseover', () => { 1956 mapButton.style.transform = 'scale(1.05)'; 1957 mapButton.style.boxShadow = '0 5px 15px rgba(255,107,107,0.4)'; 1958 }); 1959 1960 mapButton.addEventListener('mouseleave', () => { 1961 mapButton.style.transform = 'scale(1)'; 1962 mapButton.style.boxShadow = '0 2px 10px rgba(255,107,107,0.3)'; 1963 }); 1964 1965 mapButton.addEventListener('click', () => { 1966 openRelationshipMap(); 1967 }); 1968 1969 const closeBtn = document.createElement('button'); 1970 closeBtn.innerHTML = 'â'; 1971 closeBtn.style.cssText = ` 1972 padding: 10px 15px; 1973 background: rgba(255,255,255,0.2); 1974 color: white; 1975 border: none; 1976 border-radius: 50%; 1977 cursor: pointer; 1978 font-size: 18px; 1979 font-weight: bold; 1980 transition: all 0.3s ease; 1981 `; 1982 1983 closeBtn.addEventListener('mouseover', () => { 1984 closeBtn.style.background = 'rgba(255,255,255,0.3)'; 1985 }); 1986 1987 closeBtn.addEventListener('mouseout', () => { 1988 closeBtn.style.background = 'rgba(255,255,255,0.2)'; 1989 }); 1990 1991 closeBtn.addEventListener('click', () => { 1992 previewContainer.remove(); 1993 }); 1994 1995 header.appendChild(headerIcon); 1996 header.appendChild(headerTitle); 1997 header.appendChild(generateButton); 1998 header.appendChild(mapButton); 1999 header.appendChild(closeBtn); 2000 storySection.appendChild(header); 2001 2002 // Create content area 2003 contentArea = document.createElement('div'); 2004 contentArea.className = 'story-content'; 2005 contentArea.style.cssText = `
2006 min-height: 200px; 2007 color: rgba(255,255,255,0.9); 2008 line-height: 1.6; 2009 margin-bottom: 15px; 2010 font-size: 16px; 2011 `; 2012 2013 // Initial content 2014 contentArea.innerHTML = ` 2015 <div style="text-align: center; padding: 40px 20px; color: rgba(255,255,255,0.6);"> 2016 <div style="font-size: 48px; margin-bottom: 20px;">ð</div> 2017 <p style="font-size: 18px; margin: 0;">Click "Generate Story" to create a Wikipedia-like narrative</p> 2018 <p style="font-size: 14px; margin: 10px 0 0 0;">Transform ${data.totalResults} entities into an engaging story</p> 2019 </div> 2020 `; 2021 2022 storySection.appendChild(contentArea); 2023 2024 return storySection; 2025} 2026 2027// Function to generate Wikipedia story using OpenAI with chunking 2028async function generateWikipediaStory(data, contentArea, button) { 2029 console.log('ð [WikipediaStory] Generating chunked story for:', data.searchTerm); 2030 2031 // Show loading state 2032 button.textContent = 'â³ Generating...'; 2033 button.disabled = true; 2034 button.style.opacity = '0.6'; 2035 2036 contentArea.innerHTML = ` 2037 <div style="text-align: center; padding: 40px 20px;"> 2038 <div class="loading-spinner" style=" 2039 width: 40px; 2040 height: 40px; 2041 border: 3px solid rgba(255,255,255,0.3); 2042 border-top: 3px solid white; 2043 border-radius: 50%; 2044 animation: spin 1s linear infinite; 2045 margin: 0 auto 20px; 2046 "></div> 2047 <p style="color: rgba(255,255,255,0.8); margin: 0;">Initializing story generation...</p> 2048 <p style="color: rgba(255,255,255,0.6); font-size: 14px; margin: 10px 0 0 0;" id="story-progress"> 2049 Analyzing ${data.totalResults} entities 2050 </p> 2051 </div> 2052 <style> 2053 @keyframes spin { 2054 0% { transform: rotate(0deg); } 2055 100% { transform: rotate(360deg); } 2056 } 2057 </style> 2058 `; 2059 2060 try { 2061 // Step 1: Initialize chunked story generation 2062 const initResponse = await fetch('/.netlify/functions/generate-wikipedia-story', { 2063 method: 'POST', 2064 headers: { 'Content-Type': 'application/json' }, 2065 body: JSON.stringify({ 2066 infoboxData: data.infoboxes, 2067 searchTerm: data.searchTerm, 2068 totalResults: data.totalResults 2069 }) 2070 }); 2071 2072 if (!initResponse.ok) { 2073 throw new Error(`Initialization failed: ${initResponse.status}`); 2074 } 2075 2076 const initResult = await initResponse.json(); 2077 console.log('ð [WikipediaStory] Initialization result:', initResult); 2078 2079 if (initResult.type !== 'chunked_story_init') { 2080 throw new Error('Invalid initialization response'); 2081 } 2082 2083 const totalChunks = initResult.totalChunks; 2084 console.log(`ð [WikipediaStory] Will generate ${totalChunks} sections`); 2085 2086 // Initialize story buffer 2087 const storyBuffer = { 2088 chunks: new Array(totalChunks).fill(null), 2089 totalChunks: totalChunks, 2090 receivedChunks: 0, 2091 searchTerm: data.searchTerm 2092 }; 2093 2094 // Update progress 2095 document.getElementById('story-progress').textContent = `Generating ${totalChunks} story sections...`; 2096 2097 // Step 2: Generate chunks with sequential batched processing (prevents 502s) 2098 console.log(`ð [WikipediaStory] Using sequential batched processing for ${totalChunks} sections`); 2099 await generateChunksSequentially(data, totalChunks, storyBuffer); 2100 2101 // Step 3: Assemble and display complete story 2102 console.log('ð [WikipediaStory] All sections received, assembling story'); 2103 const completeStory = assembleCompleteStory(storyBuffer); 2104 displayGeneratedStory(completeStory, contentArea, button); 2105 2106 } catch (error) { 2107 console.error('ð [WikipediaStory] Error generating story:', error); 2108 2109 // Show error state with fallback 2110 contentArea.innerHTML = ` 2111 <div style="text-align: center; padding: 40px 20px;"> 2112 <div style="font-size: 48px; margin-bottom: 20px;">â ï¸</div> 2113 <p style="color: rgba(255,255,255,0.8); margin: 0;">Story generation temporarily unavailable</p> 2114 <div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; text-align: left;"> 2115 <h4 style="color: white; margin: 0 0 15px 0;">ð Quick Summary</h4> 2116 <p style="color: rgba(255,255,255,0.8); margin: 0 0 15px 0;"> 2117 We found ${data.totalResults} related Wikipedia articles about "${data.searchTerm}". 2118 While the AI story generator is temporarily unavailable, you can explore the individual 2119 infoboxes above to learn more. 2120 </p> 2121 <button onclick="copyStoryToClipboard(event)" style=" 2122 background: rgba(255,255,255,0.2); 2123 border: 1px solid rgba(255,255,255,0.3); 2124 padding: 8px 16px; 2125 border-radius: 5px; 2126 color: white; 2127 font-size: 14px; 2128 cursor: pointer; 2129 transition: all 0.3s ease; 2130 ">ð Copy Story</button> 2131 </div> 2132 </div> 2133 `; 2134 2135 // Reset button 2136 button.textContent = 'ðºï¸ Wikipedia Map'; 2137 button.disabled = false;
2138 button.style.opacity = '1'; 2139 } 2140} 2141 2142// Function to generate chunks sequentially with retry logic 2143async function generateChunksSequentially(data, totalChunks, storyBuffer) { 2144 const chunkSize = 5; // Process 5 chunks at a time 2145 const maxRetries = 3; // Allow up to 3 retries per chunk 2146 2147 for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += chunkSize) { 2148 const chunkBatch = []; 2149 2150 // Create a batch of chunk requests 2151 for (let i = chunkIndex; i < Math.min(chunkIndex + chunkSize, totalChunks); i++) { 2152 chunkBatch.push(generateStoryChunk(data, i, totalChunks, storyBuffer, maxRetries)); 2153 } 2154 2155 // Wait for the batch to complete 2156 await Promise.all(chunkBatch); 2157 } 2158} 2159 2160// Generate individual story chunk with retry logic 2161async function generateStoryChunk(data, chunkIndex, totalChunks, storyBuffer, maxRetries) { 2162 let retries = 0; 2163 2164 while (retries <= maxRetries) { 2165 try { 2166 console.log(`ð [WikipediaStory] Generating section ${chunkIndex + 1}/${totalChunks}`); 2167 2168 const response = await fetch('/.netlify/functions/generate-wikipedia-story', { 2169 method: 'POST', 2170 headers: { 'Content-Type': 'application/json' }, 2171 body: JSON.stringify({ 2172 infoboxData: data.infoboxes, 2173 searchTerm: data.searchTerm, 2174 totalResults: data.totalResults, 2175 chunkIndex: chunkIndex, 2176 totalChunks: totalChunks 2177 }) 2178 }); 2179 2180 if (!response.ok) { 2181 throw new Error(`Section ${chunkIndex + 1} failed: ${response.status}`); 2182 } 2183 2184 const result = await response.json(); 2185 console.log(`ð [WikipediaStory] Section ${chunkIndex + 1} received:`, result.storyChunk?.length, 'characters'); 2186 2187 if (result.type === 'chunked_story_part') { 2188 storyBuffer.chunks[chunkIndex] = result.storyChunk; 2189 storyBuffer.receivedChunks++; 2190 2191 // Update progress 2192 const progressElement = document.getElementById('story-progress'); 2193 if (progressElement) { 2194 progressElement.textContent = `Generated ${storyBuffer.receivedChunks}/${totalChunks} sections...`; 2195 } 2196 2197 return; // Chunk generated successfully 2198 } 2199 2200 // If we reach this point, the chunk generation failed 2201 throw new Error(`Invalid section response for section ${chunkIndex + 1}`); 2202 2203 } catch (error) { 2204 console.error(`ð [WikipediaStory] Error generating section ${chunkIndex + 1}:`, error); 2205 2206 // If we've exceeded the max retries, give up on this chunk 2207 if (retries >= maxRetries) { 2208 console.log(`ð [WikipediaStory] Giving up on section ${chunkIndex + 1} after ${maxRetries} retries`); 2209 storyBuffer.chunks[chunkIndex] = ` 2210 <h3>ð Section ${chunkIndex + 1}</h3> 2211 <p style="color: rgba(255,255,255,0.7);"> 2212 <em>This section is temporarily unavailable. Please try regenerating the story.</em> 2213 </p> 2214 `; 2215 storyBuffer.receivedChunks++; 2216 return; 2217 } 2218 2219 // Otherwise, retry the chunk 2220 retries++; 2221 console.log(`ð [WikipediaStory] Retrying section ${chunkIndex + 1} (attempt ${retries + 1}/${maxRetries})`); 2222 await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retrying 2223 } 2224 } 2225} 2226 2227// Assemble complete story from chunks 2228function assembleCompleteStory(storyBuffer) { 2229 // Clean up chunks: remove AI-generated section headers like "### Section 2:" or "## Section 1:" 2230 const cleanedChunks = storyBuffer.chunks.map(chunk => { 2231 if (!chunk) return ''; 2232 // Remove markdown section headers (###, ##, #) followed by "Section X:" 2233 return chunk.replace(/^#{1,6}\s*Section\s+\d+:?\s*/gim, '').trim(); 2234 }); 2235 2236 const completeStoryContent = cleanedChunks.join('\n\n'); 2237 2238 return { 2239 story: ` 2240 <h2>ð ${storyBuffer.searchTerm} - Wikipedia Story</h2> 2241 ${completeStoryContent} 2242 `, 2243 searchTerm: storyBuffer.searchTerm, 2244 totalChunks: storyBuffer.totalChunks, 2245 storyLength: completeStoryContent.length, 2246 generatedAt: new Date().toISOString(), 2247 metadata: { 2248 chunksGenerated: storyBuffer.receivedChunks, 2249 totalChunks: storyBuffer.totalChunks, 2250 generationMethod: 'chunked' 2251 } 2252 }; 2253} 2254 2255// Function to display the generated story 2256function displayGeneratedStory(result, contentArea, button) { 2257 console.log('ð [WikipediaStory] Displaying generated story'); 2258 2259 // Create story content with metadata 2260 const storyHTML = ` 2261 <div class="wikipedia-story-content"> 2262 <div class="story-metadata" style=" 2263 background: rgba(255,255,255,0.1); 2264 padding: 15px; 2265 border-radius: 10px; 2266 margin-bottom: 20px; 2267 display: flex; 2268 justify-content: space-between; 2269 align-items: center; 2270 flex-wrap: wrap; 2271 gap: 10px; 2272 "> 2273 <div style="color: rgba(255,255,255,0.8); font-size: 14px;"> 2274 ð ${result.totalChunks} sections ⢠${result.storyLength} characters ⢠Generated ${new Date(result.generatedAt).toLocaleTimeString()} 2275 </div> 2276 ${result.metadata.generationMethod === 'chunked' ? '<span style="color: #4ecdc4; font-size: 12px;">⨠Chunked Generation</span>' : '<span style="color: #ff6b6b; font-size: 12px;">â ï¸ Fallback Story</span>'} 2277 </div> 2278 2279 <div class="story-text" style=" 2280 background: rgba(255,255,255,0.05); 2281 padding: 25px; 2282 border-radius: 12px; 2283 border-left: 4px solid #4ecdc4; 2284 line-height: 1.7; 2285 font-size: 16px; 2286 "> 2287 ${result.story} 2288 </div> 2289 2290 <div class="story-actions" style=" 2291 margin-top: 20px; 2292 display: flex; 2293 gap: 10px; 2294 flex-wrap: wrap; 2295 "> 2296 <button onclick="copyStoryToClipboard(event)" style=" 2297 background: rgba(255,255,255,0.2); 2298 border: 1px solid rgba(255,255,255,0.3); 2299 padding: 8px 16px; 2300 border-radius: 20px; 2301 color: white; 2302 font-size: 14px; 2303 cursor: pointer; 2304 transition: all 0.3s ease; 2305 ">ð Copy Story</button> 2306 2307 <button id="speakStoryButton" onclick="toggleWikipediaStoryTTS()" style=" 2308 background: rgba(255,255,255,0.2); 2309 border: 1px solid rgba(255,255,255,0.3); 2310 padding: 8px 16px; 2311 border-radius: 20px; 2312 color: white; 2313 font-size: 14px; 2314 cursor: pointer; 2315 transition: all 0.3s ease; 2316 ">ð Speak</button> 2317 2318 <button onclick="shareStory()" style=" 2319 background: rgba(255,255,255,0.2); 2320 border: 1px solid rgba(255,255,255,0.3); 2321 padding: 8px 16px; 2322 border-radius: 20px; 2323 color: white; 2324 font-size: 14px; 2325 cursor: pointer; 2326 transition: all 0.3s ease; 2327 ">ð Share</button> 2328 2329 <button onclick="exportStoryToPDF()" style=" 2330 background: rgba(255,255,255,0.2); 2331 border: 1px solid rgba(255,255,255,0.3); 2332 padding: 8px 16px; 2333 border-radius: 20px; 2334 color: white; 2335 font-size: 14px; 2336 cursor: pointer; 2337 transition: all 0.3s ease; 2338 ">ð Export PDF</button> 2339 </div> 2340 </div> 2341 2342 <style> 2343 .wikipedia-story-link { 2344 color: #4ecdc4 !important; 2345 text-decoration: none; 2346 border-bottom: 1px solid rgba(78, 205, 196, 0.3); 2347 transition: all 0.3s ease; 2348 } 2349 2350 .wikipedia-story-link:hover { 2351 color: #ff6b6b !important; 2352 border-bottom-color: rgba(255, 107, 107, 0.5); 2353 background: rgba(255, 107, 107, 0.1); 2354 padding: 2px 4px; 2355 border-radius: 3px; 2356 } 2357 2358 .story-entity-image { 2359 width: 100px; 2360 height: 100px; 2361 object-fit: cover; 2362 border-radius: 8px; 2363 margin-right: 12px; 2364 vertical-align: middle; 2365 border: 2px solid #4ecdc4; 2366 transition: all 0.3s ease; 2367 display: inline-block; 2368 } 2369 2370 .wikipedia-story-link:hover .story-entity-image { 2371 border-color: #ff6b6b; 2372 transform: scale(1.05); 2373 box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3); 2374 } 2375 2376 .story-actions button:hover { 2377 background: rgba(255,255,255,0.3) !important; 2378 transform: translateY(-1px); 2379 } 2380 </style> 2381 `; 2382 2383 contentArea.innerHTML = storyHTML; 2384 2385 // Reset button to allow regeneration 2386 button.textContent = 'ðºï¸ Wikipedia Map'; 2387 button.disabled = false;
2388 button.style.opacity = '1'; 2389 2390 // Scroll to story 2391 contentArea.scrollIntoView({ behavior: 'smooth', block: 'start' }); 2392 2393 // Store the current infobox data globally for relationship analysis 2394 storeInfoboxData(result); 2395} 2396 2397// Helper function to copy story to clipboard 2398window.copyStoryToClipboard = function(event) { 2399 const storyText = document.querySelector('.story-text'); 2400 if (storyText) { 2401 // Create a temporary element to extract plain text 2402 const tempDiv = document.createElement('div'); 2403 tempDiv.innerHTML = storyText.innerHTML; 2404 const plainText = tempDiv.textContent || tempDiv.innerText || ''; 2405 2406 navigator.clipboard.writeText(plainText).then(() => { 2407 // Show feedback - find button element properly 2408 const button = event ? event.target : document.querySelector('[onclick*="copyStoryToClipboard"]'); 2409 if (button) { 2410 const originalText = button.textContent; 2411 button.textContent = 'â Copied!'; 2412 setTimeout(() => { 2413 button.textContent = originalText; 2414 }, 2000); 2415 } 2416 }).catch(err => { 2417 console.error('Failed to copy story:', err); 2418 // Show error feedback 2419 const button = event ? event.target : document.querySelector('[onclick*="copyStoryToClipboard"]'); 2420 if (button) { 2421 const originalText = button.textContent; 2422 button.textContent = 'â Copy failed'; 2423 setTimeout(() => { 2424 button.textContent = originalText; 2425 }, 2000); 2426 } 2427 }); 2428 } 2429}; 2430 2431// Global variables for TTS state management 2432let wikipediaStoryAudio = null; 2433let wikipediaStoryController = null; 2434 2435// Global variables for chunked TTS playback 2436let ttsAudioQueue = []; 2437let ttsCurrentIndex = 0; 2438let ttsIsPlaying = false; 2439 2440// Function to split text into chunks (OpenAI TTS limit is 4096 characters) 2441function splitTextIntoChunks(text, maxLength = 3500) { 2442 const chunks = []; 2443 const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]; 2444 let currentChunk = ''; 2445 2446 for (const sentence of sentences) { 2447 if ((currentChunk + sentence).length > maxLength && currentChunk.length > 0) { 2448 chunks.push(currentChunk.trim()); 2449 currentChunk = sentence; 2450 } else { 2451 currentChunk += sentence; 2452 } 2453 } 2454 2455 if (currentChunk.trim().length > 0) { 2456 chunks.push(currentChunk.trim()); 2457 } 2458 2459 return chunks; 2460} 2461 2462// Function to play next audio chunk in queue 2463function playNextTTSChunk() { 2464 if (!ttsIsPlaying || ttsCurrentIndex >= ttsAudioQueue.length) { 2465 console.log('[TTS] Playback queue completed'); 2466 resetTTSButton(); 2467 ttsAudioQueue = []; 2468 ttsCurrentIndex = 0; 2469 ttsIsPlaying = false; 2470 return; 2471 } 2472 2473 const audioUrl = ttsAudioQueue[ttsCurrentIndex]; 2474 console.log(`[TTS] Playing chunk ${ttsCurrentIndex + 1}/${ttsAudioQueue.length}`); 2475 2476 wikipediaStoryAudio = new Audio(audioUrl); 2477 2478 wikipediaStoryAudio.onended = () => { 2479 URL.revokeObjectURL(audioUrl); 2480 ttsCurrentIndex++; 2481 playNextTTSChunk(); 2482 }; 2483 2484 wikipediaStoryAudio.onerror = (error) => { 2485 console.error('[TTS] Audio playback error:', error); 2486 URL.revokeObjectURL(audioUrl); 2487 resetTTSButton(); 2488 ttsIsPlaying = false; 2489 alert('Error playing audio'); 2490 }; 2491 2492 wikipediaStoryAudio.play(); 2493} 2494 2495// Function to speak the Wikipedia Story using OpenAI TTS 2496async function speakWikipediaStory() { 2497 const storyText = document.querySelector('.story-text'); 2498 const ttsButton = document.getElementById('speakStoryButton'); 2499 2500 if (!storyText || !ttsButton) { 2501 console.error('[TTS] Story text or button not found'); 2502 return; 2503 } 2504 2505 // Get the text content 2506 const text = storyText.innerText || storyText.textContent; 2507 2508 if (!text || text.trim().length === 0) { 2509 console.error('[TTS] No text to speak'); 2510 alert('No story text available to speak'); 2511 return; 2512 } 2513
2514 console.log(`[TTS] Starting OpenAI TTS for ${text.length} characters`); 2515 2516 // Split text into chunks (smaller chunks = faster processing, avoid timeouts) 2517 // Using 1000 chars to avoid Netlify 10-second timeout limit 2518 const chunks = splitTextIntoChunks(text, 1000); 2519 console.log(`[TTS] Split into ${chunks.length} chunks`); 2520 2521 // Update button to show loading state 2522 ttsButton.textContent = `â³ Generating ${chunks.length} parts...`; 2523 ttsButton.disabled = true; 2524 ttsButton.style.background = 'rgba(255,165,0,0.3)'; 2525 2526 try { 2527 // Create abort controller for cancellation 2528 wikipediaStoryController = new AbortController(); 2529 2530 // Initialize playback state 2531 ttsAudioQueue = []; 2532 ttsIsPlaying = true; 2533 ttsCurrentIndex = 0; 2534 let isFirstChunk = true; 2535 2536 // Update button to show playing state immediately 2537 ttsButton.textContent = 'â¸ï¸ Stop Speaking'; 2538 ttsButton.disabled = false; 2539 ttsButton.style.background = 'rgba(255,0,0,0.3)'; 2540 ttsButton.style.borderColor = 'rgba(255,0,0,0.5)'; 2541 2542 // Generate and play audio chunks progressively 2543 for (let i = 0; i < chunks.length; i++) { 2544 console.log(`[TTS] Generating chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`); 2545 2546 const response = await fetch('/.netlify/functions/text-to-speech', { 2547 method: 'POST', 2548 headers: { 2549 'Content-Type': 'application/json' 2550 }, 2551 body: JSON.stringify({ text: chunks[i] }), 2552 signal: wikipediaStoryController.signal 2553 }); 2554 2555 if (!response.ok) { 2556 const error = await response.json(); 2557 throw new Error(error.error || 'Failed to generate speech'); 2558 } 2559 2560 const audioBlob = await response.blob(); 2561 const audioUrl = URL.createObjectURL(audioBlob); 2562 ttsAudioQueue.push(audioUrl); 2563 2564 console.log(`[TTS] Chunk ${i + 1}/${chunks.length} ready`); 2565 2566 // Start playback immediately after first chunk is ready 2567 if (isFirstChunk) { 2568 isFirstChunk = false; 2569 console.log('[TTS] Starting progressive playback...'); 2570 playNextTTSChunk(); 2571 } 2572 } 2573 2574 console.log(`[TTS] All ${ttsAudioQueue.length} chunks generated, playback in progress`); 2575 2576 } catch (error) { 2577 if (error.name === 'AbortError') { 2578 console.log('[TTS] Request cancelled'); 2579 } else { 2580 console.error('[TTS] Error:', error); 2581 alert(`Failed to generate speech: ${error.message}`); 2582 } 2583 resetTTSButton(); 2584 ttsAudioQueue = []; 2585 ttsIsPlaying = false; 2586 } 2587} 2588 2589// Function to stop Wikipedia Story TTS 2590function stopWikipediaStoryTTS() { 2591 console.log('Stopping Wikipedia Story TTS'); 2592 2593 // Stop playback flag 2594 ttsIsPlaying = false; 2595 2596 // Stop audio playback 2597 if (wikipediaStoryAudio) { 2598 wikipediaStoryAudio.pause(); 2599 wikipediaStoryAudio.currentTime = 0; 2600 wikipediaStoryAudio = null; 2601 } 2602 2603 // Cancel any ongoing request 2604 if (wikipediaStoryController) { 2605 wikipediaStoryController.abort(); 2606 wikipediaStoryController = null; 2607 } 2608 2609 // Clean up audio queue 2610 ttsAudioQueue.forEach(url => URL.revokeObjectURL(url)); 2611 ttsAudioQueue = []; 2612 ttsCurrentIndex = 0; 2613 2614 // Reset button state 2615 resetTTSButton(); 2616} 2617 2618// Function to toggle Wikipedia Story TTS (start or stop) 2619function toggleWikipediaStoryTTS() { 2620 const ttsButton = document.getElementById('speakStoryButton'); 2621 2622 if (!ttsButton) { 2623 console.error('TTS button not found'); 2624 return; 2625 } 2626 2627 // Check if TTS is currently active (button shows "Stop Speaking") 2628 if (ttsButton.textContent.includes('Stop')) { 2629 // TTS is active, stop it 2630 stopWikipediaStoryTTS(); 2631 } else { 2632 // TTS is not active, start it 2633 speakWikipediaStory(); 2634 } 2635} 2636 2637// Function to reset TTS button to initial state 2638function resetTTSButton() { 2639 const ttsButton = document.getElementById('speakStoryButton'); 2640 if (ttsButton) { 2641 ttsButton.textContent = 'ð Speak'; 2642 ttsButton.style.background = 'rgba(255,255,255,0.2)'; 2643 ttsButton.style.borderColor = 'rgba(255,255,255,0.3)'; 2644 ttsButton.disabled = false;
2645 } 2646 2647 // Clean up global state 2648 wikipediaStoryAudio = null; 2649 wikipediaStoryController = null; 2650} 2651 2652// Export story to PDF using server-side Node.js generation 2653window.exportStoryToPDF = async function() { 2654 console.log('ð Starting Node.js PDF export...'); 2655 2656 try { 2657 // Show loading indicator 2658 const exportButton = document.querySelector('button[onclick="exportStoryToPDF()"]'); 2659 if (exportButton) { 2660 const originalText = exportButton.textContent; 2661 exportButton.textContent = 'ð Generating PDF...'; 2662 exportButton.disabled = true; 2663 2664 // Restore button after completion 2665 setTimeout(() => { 2666 exportButton.textContent = originalText; 2667 exportButton.disabled = false; 2668 }, 15000); // 15 second timeout for Node.js PDF generation 2669 } 2670 2671 // Extract story data 2672 const storyData = extractStoryDataForPDF(); 2673 if (!storyData) { 2674 throw new Error('Failed to extract story data'); 2675 } 2676 2677 console.log('ð Extracted story data:', storyData); 2678 2679 // Send to Node.js PDF generation endpoint 2680 const response = await fetch('/.netlify/functions/export-wikipedia-pdf', { 2681 method: 'POST', 2682 headers: { 2683 'Content-Type': 'application/json' 2684 }, 2685 body: JSON.stringify({ 2686 storyData: storyData 2687 }) 2688 }); 2689 2690 if (!response.ok) { 2691 let errorMessage = `HTTP ${response.status}: ${response.statusText}`; 2692 try { 2693 const errorData = await response.json(); 2694 errorMessage = errorData.error || errorMessage; 2695 } catch (e) { 2696 // If response is not JSON, use the status text 2697 console.warn('Non-JSON error response:', e); 2698 } 2699 throw new Error(errorMessage); 2700 } 2701 2702 const result = await response.json(); 2703 2704 if (!result.success) { 2705 throw new Error(result.error || 'PDF generation failed'); 2706 } 2707 2708 console.log('ð PDF generated successfully:', result.filename, `(${result.size} bytes)`); 2709 2710 // Convert base64 to blob and download 2711 const pdfBytes = atob(result.pdf); 2712 const pdfArray = new Uint8Array(pdfBytes.length); 2713 for (let i = 0; i < pdfBytes.length; i++) { 2714 pdfArray[i] = pdfBytes.charCodeAt(i); 2715 } 2716 2717 const blob = new Blob([pdfArray], { type: 'application/pdf' }); 2718 const url = URL.createObjectURL(blob); 2719 2720 // Create download link 2721 const downloadLink = document.createElement('a'); 2722 downloadLink.href = url; 2723 downloadLink.download = result.filename; 2724 downloadLink.style.display = 'none'; 2725 2726 // Trigger download 2727 document.body.appendChild(downloadLink); 2728 downloadLink.click(); 2729 document.body.removeChild(downloadLink); 2730 2731 // Clean up 2732 URL.revokeObjectURL(url); 2733 2734 // Show success message 2735 if (exportButton) { 2736 exportButton.textContent = 'â PDF Downloaded!'; 2737 setTimeout(() => { 2738 exportButton.textContent = 'ð Export PDF'; 2739 exportButton.disabled = false; 2740 }, 3000); 2741 } 2742 2743 console.log('ð Node.js PDF download completed successfully'); 2744 2745 } catch (error) { 2746 console.error('ð Node.js PDF export failed:', error); 2747 2748 // Show error message 2749 const exportButton = document.querySelector('button[onclick="exportStoryToPDF()"]'); 2750 if (exportButton) { 2751 exportButton.textContent = 'â Export Failed'; 2752 exportButton.disabled = false; 2753 setTimeout(() => { 2754 exportButton.textContent = 'ð Export PDF'; 2755 }, 3000); 2756 } 2757 2758 // Show user-friendly error with more details 2759 alert(`PDF export failed: ${error.message}\n\nThis may be due to:\n⢠Server processing timeout\n⢠Large content size\n⢠Network connectivity\n\nPlease try again or c
2759ontact support if the issue persists.`); 2760 } 2761}; 2762 2763// Enhanced text cleaning function for PDF export 2764function cleanTextForPDF(text) { 2765 if (!text || typeof text !== 'string') return ''; 2766 2767 // Remove encoding artifacts that cause PDF issues 2768 text = text.replace(/Ã=Ã[ÃÃ÷Ã]/g, ''); // Remove specific encoding artifacts 2769 text = text.replace(/Ã=Ã./g, ''); // Remove any Ã=à followed by any character 2770 text = text.replace(/[ÃÃÃÃÃ÷]/g, ''); // Remove individual problematic characters 2771 2772 // Remove other problematic characters 2773 text = text.replace(/[\u0080-\u00FF]/g, ''); // Remove Latin-1 supplement 2774 text = text.replace(/[\u0100-\u017F]/g, ''); // Remove Latin Extended-A 2775 text = text.replace(/[\u0180-\u024F]/g, ''); // Remove Latin Extended-B 2776 2777 // Fix spaced-out text ("G o o g l e" -> "Google") 2778 text = text.replace(/\b([a-zA-Z])\s+([a-zA-Z])\s+([a-zA-Z])/g, '$1$2$3'); 2779 text = text.replace(/\b([a-zA-Z])\s+([a-zA-Z])/g, '$1$2'); 2780 2781 // Normalize whitespace 2782 text = text.replace(/\s+/g, ' '); 2783 text = text.replace(/^\s+|\s+$/g, ''); 2784 2785 // Remove any remaining non-printable characters 2786 text = text.replace(/[^\x20-\x7E\n\r\t]/g, ''); 2787 2788 return text; 2789} 2790 2791// Function to extract story data for PDF 2792function extractStoryDataForPDF() { 2793 const storyContainer = document.querySelector('.wikipedia-story-content'); 2794 const storyText = document.querySelector('.story-text'); 2795 const storyMetadata = document.querySelector('.story-metadata'); 2796 2797 if (!storyText || !storyContainer) { 2798 console.error('Story content not found for PDF export'); 2799 return null; 2800 } 2801 2802 console.log('ð Extracting story data for PDF...'); 2803 2804 // Extract title from the story content 2805 const titleElement = storyText.querySelector('h2'); 2806 const title = titleElement ? cleanTextForPDF(titleElement.textContent.trim()) : 'Wikipedia Story'; 2807 2808 // Extract content by processing the HTML properly 2809 const content = []; 2810 2811 // Get the raw HTML content from the story text div 2812 let htmlContent = storyText.innerHTML; 2813 console.log('ð Raw HTML content length:', htmlContent.length); 2814 2815 // Split the content by section markers and process each section 2816 const sections = htmlContent.split(/###\s*Section\s*\d+:/i); 2817 console.log('ð Text sections found:', sections.length); 2818 2819 sections.forEach((section, index) => { 2820 if (!section.trim()) return; 2821 2822 // Create a temporary div to parse this section's HTML 2823 const tempDiv = document.createElement('div'); 2824 tempDiv.innerHTML = section.trim(); 2825 2826 // Extract text content from this section 2827 const sectionText = tempDiv.textContent || tempDiv.innerText || ''; 2828 const cleanText = cleanTextForPDF(sectionText.trim()); 2829 2830 if (cleanText.length > 50) { 2831 // Extract images from this section with full details 2832 const images = tempDiv.querySelectorAll('img.story-entity-image, img'); 2833 const imageData = Array.from(images).map(img => ({ 2834 src: img.src, 2835 alt: cleanTextForPDF(img.alt || 'Image'), 2836 width: parseInt(img.width) || 100, 2837 height: parseInt(img.height) || 100 2838 })); 2839 2840 console.log(`ð Section ${index} found ${imageData.length} images:`, imageData.map(img => img.src)); 2841 2842 // Extract hyperlinks from this section 2843 const links = tempDiv.querySelectorAll('a[href]'); 2844 const linkData = Array.from(links).map(link => ({ 2845 text: cleanTextForPDF(link.textContent.trim()), 2846 url: link.href, 2847 title: cleanTextForPDF(link.title || '') 2848 })).filter(link => link.text.length > 0 && link.url.length > 0); 2849 2850 console.log(`ð Section ${index} found ${linkData.length} links:`, linkData.map(link => `${link.text} -> ${link.url}`)); 2851 2852 if (index === 0) { 2853 // First section is usually the title 2854 content.push({ 2855 type: 'title', 2856 text: cleanText, 2857 images: imageData, 2858 links: linkData 2859 }); 2860 } else { 2861 // Other sections are content 2862 content.push({ 2863 type: 'section', 2864 text: `Section ${index}: ${cleanText}`, 2865 images: imageData, 2866 links: linkData 2867 }); 2868 } 2869 } 2870 }); 2871 2872 // If section splitting didn't work, try alternative approaches 2873 if (content.length <= 1) { 2874 console.log(`ð Section splitting failed, trying paragraph-based extraction...`); 2875 2876 // Clear previous content 2877 content.length = 0; 2878 2879 // Try to extract by paragraphs and headings 2880 const tempDiv = document.createElement('div'); 2881 tempDiv.innerHTML = htmlContent; 2882 2883 // Look for all text content and split by patterns 2884 const allText = tempDiv.textContent || tempDiv.innerText || ''; 2885 console.log('ð All text length:', allText.length); 2886 2887 // Split by common section patterns 2888 const textSections = allText.split(/(?=###\s*Section\s*\d+:|(?:\*\*[^*]+\*\*)|(?:\n\s*\n))/i);
2889 console.log('ð Text sections found:', textSections.length); 2890 2891 textSections.forEach((section, index) => { 2892 const cleanText = cleanTextForPDF(section.trim()); 2893 if (cleanText.length > 100) { 2894 // Extract images and links for this text section 2895 const sectionDiv = document.createElement('div'); 2896 sectionDiv.innerHTML = htmlContent; // Use full HTML for extraction 2897 2898 const images = Array.from(sectionDiv.querySelectorAll('img')).map(img => ({ 2899 src: img.src, 2900 alt: cleanTextForPDF(img.alt || 'Image'), 2901 width: parseInt(img.width) || 100, 2902 height: parseInt(img.height) || 100 2903 })); 2904 2905 const links = Array.from(sectionDiv.querySelectorAll('a[href]')).map(link => ({ 2906 text: cleanTextForPDF(link.textContent.trim()), 2907 url: link.href, 2908 title: cleanTextForPDF(link.title || '') 2909 })); 2910 2911 if (cleanText.includes('Wikipedia Story') && index === 0) { 2912 content.push({ type: 'title', text: cleanText, images: images, links: links }); 2913 } else if (cleanText.startsWith('###') || cleanText.includes('Section')) { 2914 content.push({ type: 'section', text: cleanText, images: images, links: links }); 2915 } else { 2916 content.push({ type: 'paragraph', text: cleanText, images: images, links: links }); 2917 } 2918 } 2919 }); 2920 } 2921 2922 // Final fallback: extract everything as one large text block and split manually 2923 if (content.length <= 1) { 2924 console.log('ð All extraction methods failed, using final fallback...'); 2925 2926 content.length = 0; 2927 2928 const tempDiv = document.createElement('div'); 2929 tempDiv.innerHTML = htmlContent; 2930 const fullText = cleanTextForPDF(tempDiv.textContent || tempDiv.innerText || ''); 2931 2932 // Extract all images and links from the full content 2933 const allImages = Array.from(tempDiv.querySelectorAll('img')).map(img => ({ 2934 src: img.src, 2935 alt: cleanTextForPDF(img.alt || 'Image'), 2936 width: parseInt(img.width) || 100, 2937 height: parseInt(img.height) || 100 2938 })); 2939 2940 const allLinks = Array.from(tempDiv.querySelectorAll('a[href]')).map(link => ({ 2941 text: cleanTextForPDF(link.textContent.trim()), 2942 url: link.href, 2943 title: cleanTextForPDF(link.title || '') 2944 })).filter(link => link.text.length > 0 && link.url.length > 0); 2945 2946 // If section splitting didn't work, try alternative approaches 2947 if (fullText.length > 100) { 2948 content.push({ type: 'paragraph', text: fullText, images: allImages, links: allLinks }); 2949 } 2950 } 2951 2952 console.log('ð Extracted content pieces:', content.length); 2953 content.forEach((item, index) => { 2954 console.log(`ð Content ${index + 1}: ${item.type} - ${item.text.substring(0, 100)}... (${item.images?.length || 0} images, ${item.links?.length || 0} links)`); 2955 }); 2956 2957 // Extract metadata 2958 let metadata = ''; 2959 if (storyMetadata) { 2960 metadata = cleanTextForPDF(storyMetadata.textContent.trim()); 2961 } 2962 2963 return { 2964 title, 2965 content, 2966 metadata, 2967 searchTerm: title.replace('ð ', '').replace(' - Wikipedia Story', ''), 2968 generatedDate: new Date().toLocaleDateString('en-US', { 2969 year: 'numeric', 2970 month: 'long', 2971 day: 'numeric' 2972 }) 2973 }; 2974} 2975 2976// Method to open the relationship map 2977function openRelationshipMap() { 2978 console.log('ðºï¸ [FancyUI] Opening AI-powered relationship map...'); 2979 2980 // FORCE CLEAR ALL CACHED DATA - Critical for preventing stale data 2981 window.lastSearchData = null; 2982 window.relationshipMapInstance = null; 2983 2984 // ALWAYS get the most current data - prioritize the latest stored data
2985 let currentData = window.currentFancyData; 2986 2987 // Log what data we're working with 2988 console.log('ð [FancyUI] Current data check:', { 2989 hasCurrentData: !!currentData, 2990 searchTerm: currentData?.searchTerm, 2991 timestamp: currentData?.timestamp, 2992 totalResults: currentData?.totalResults 2993 }); 2994 2995 // If no current data, try to extract fresh data from the DOM 2996 if (!currentData || !currentData.infoboxes || currentData.infoboxes.length === 0) { 2997 console.log('ð [FancyUI] No current data found, attempting to extract fresh data from DOM...'); 2998 2999 // Try to extract fresh data from existing infoboxes 3000 if (typeof extractInfoboxDataAsJSON === 'function') { 3001 try { 3002 // Get the chat container element 3003 const chatContainer = document.getElementById('chat-container') || document.querySelector('.chat-container'); 3004 if (chatContainer) { 3005 currentData = extractInfoboxDataAsJSON(chatContainer); 3006 console.log('â [FancyUI] Successfully extracted fresh data from DOM:', currentData); 3007 } else { 3008 console.error('â [FancyUI] Chat container not found'); 3009 } 3010 } catch (error) { 3011 console.error('â [FancyUI] Failed to extract fresh data from DOM:', error); 3012 } 3013 } 3014 3015 // Final fallback - but log warning 3016 if (!currentData || !currentData.infoboxes || currentData.infoboxes.length === 0) { 3017 console.warn('â ï¸ [FancyUI] Using fallback data - this may be stale!'); 3018 currentData = window.lastSearchData; 3019 } 3020 } 3021 3022 if (!currentData || !currentData.infoboxes || currentData.infoboxes.length === 0) { 3023 alert('â No infobox data available for relationship analysis. Please perform a search first.'); 3024 return; 3025 } 3026 3027 console.log('ð [FancyUI] Using relationship map data:', { 3028 searchTerm: currentData.searchTerm, 3029 timestamp: currentData.timestamp, 3030 totalResults: currentData.totalResults 3031 }); 3032 3033 // Create full-screen relationship map container 3034 const mapContainer = document.createElement('div'); 3035 mapContainer.id = 'relationship-map-container'; 3036 mapContainer.style.cssText = ` 3037 position: fixed; 3038 top: 0; 3039 left: 0; 3040 width: 100vw; 3041 height: 100vh; 3042 background: rgba(0,0,0,0.95); 3043 z-index: 10000; 3044 display: flex; 3045 flex-direction: column; 3046 `; 3047 3048 // Create header for the map 3049 const mapHeader = document.createElement('div'); 3050 mapHeader.style.cssText = ` 3051 padding: 20px; 3052 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 3053 color: white; 3054 display: flex; 3055 justify-content: space-between; 3056 align-items: center; 3057 `; 3058 3059 mapHeader.innerHTML = ` 3060 <div> 3061 <h1 style="margin: 0; font-size: 24px;">ðºï¸ WikiLive Search Map</h1> 3062 <p style="margin: 5px 0 0 0; opacity: 0.8;">Visualizing connections for "${currentData.searchTerm}"</p> 3063 </div> 3064 <button id="close-relationship-map" style=" 3065 background: rgba(255,255,255,0.2); 3066 border: none; 3067 border-radius: 50%; 3068 width: 40px; 3069 height: 40px; 3070 color: white; 3071 cursor: pointer; 3072 font-size: 20px; 3073 font-weight: bold; 3074 ">â</button> 3075 `; 3076 3077 // Create map content area 3078 const mapContent = document.createElement('div'); 3079 mapContent.id = 'relationship-map-content'; 3080 mapContent.style.cssText = ` 3081 flex: 1; 3082 position: relative; 3083 overflow: hidden; 3084 `; 3085 3086 mapContainer.appendChild(mapHeader); 3087 mapContainer.appendChild(mapContent); 3088 document.body.appendChild(mapContainer); 3089 3090 // Add close functionality 3091 document.getElementById('close-relationship-map').addEventListener('click', () => { 3092 mapContainer.remove(); 3093 }); 3094 3095 // Add escape key listener 3096 const escapeHandler = (e) => { 3097 if (e.key === 'Escape') { 3098 mapContainer.remove(); 3099 document.removeEventListener('keydown', escapeHandler); 3100 } 3101 }; 3102 document.addEventListener('keydown', escapeHandler); 3103 3104 // Load D3.js if not already loaded 3105 if (typeof d3 === 'undefined') {
3106 console.log('ð [FancyUI] Loading D3.js library...'); 3107 const d3Script = document.createElement('script'); 3108 d3Script.src = 'https://d3js.org/d3.v7.min.js'; 3109 d3Script.onload = () => { 3110 console.log('â [FancyUI] D3.js loaded successfully'); 3111 initializeRelationshipMap(); 3112 }; 3113 d3Script.onerror = () => { 3114 console.error('â [FancyUI] Failed to load D3.js'); 3115 mapContent.innerHTML = ` 3116 <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: white; text-align: center;"> 3117 <div> 3118 <h2>â Failed to load visualization library</h2> 3119 <p>Please check your internet connection and try again.</p> 3120 </div> 3121 </div> 3122 `; 3123 }; 3124 document.head.appendChild(d3Script); 3125 } else { 3126 initializeRelationshipMap(); 3127 } 3128 3129 function initializeRelationshipMap() { 3130 console.log('ð [FancyUI] Initializing relationship map with data:', currentData); 3131 3132 // VALIDATE DATA FRESHNESS - Critical check 3133 if (!currentData || !currentData.searchTerm || !currentData.timestamp) { 3134 console.error('â [FancyUI] Invalid or missing data for relationship map:', currentData); 3135 alert('Unable to open relationship map: No valid search data available. Please perform a search first.'); 3136 return; 3137 } 3138 3139 // Log data being used for relationship map 3140 console.log('ð [FancyUI] Using data for relationship map:', { 3141 searchTerm: currentData.searchTerm, 3142 timestamp: currentData.timestamp, 3143 totalResults: currentData.totalResults, 3144 firstFewTitles: currentData.infoboxes?.slice(0, 3).map(box => box.title) 3145 }); 3146 3147 // Clear any existing relationship map content first - CRITICAL for new searches 3148 const container = document.getElementById('relationship-map-content'); 3149 if (container) { 3150 container.innerHTML = ''; 3151 console.log('ð§¹ [FancyUI] Cleared existing relationship map container'); 3152 } 3153 3154 // Also clear any global relationship map instances 3155 if (window.relationshipMapInstance) { 3156 if (typeof window.relationshipMapInstance.clearVisualization === 'function') { 3157 window.relationshipMapInstance.clearVisualization(); 3158 } 3159 window.relationshipMapInstance = null; 3160 console.log('ðï¸ [FancyUI] Cleared previous relationship map instance'); 3161 } 3162 3163 // Create the relationship map instance 3164 const relationshipMap = new RelationshipMap('relationship-map-content'); 3165 3166 // Store the instance globally for cleanup 3167 window.relationshipMapInstance = relationshipMap; 3168 3169 // Load and analyze the data
3170 relationshipMap.loadRelationshipData(currentData); 3171 } 3172} 3173 3174// Store the current infobox data globally for relationship analysis 3175function storeInfoboxData(result) { 3176 try { 3177 // Clear any previous data to prevent stale data issues 3178 window.currentInfoboxData = null; 3179 window.lastSearchData = null; 3180 3181 // Store the fresh data 3182 window.currentInfoboxData = result; 3183 window.lastSearchData = result; 3184 3185 console.log('ð¾ [FancyUI] Stored infobox data for relationship analysis:', { 3186 searchTerm: result.searchTerm || 'Unknown', 3187 timestamp: result.timestamp || Date.now(), 3188 totalResults: result.totalResults || (result.infoboxes ? result.infoboxes.length : 0), 3189 hasInfboxes: !!result.infoboxes, 3190 hasRealtimeFacts: !!result.realtime_facts, 3191 hasWikipediaImages: !!result.wikipediaImages 3192 }); 3193 } catch (error) { 3194 console.error('ð¾ [FancyUI] Error in storeInfoboxData:', error); 3195 // Still store the data even if logging fails 3196 window.currentInfoboxData = result; 3197 window.lastSearchData = result; 3198 } 3199} 3200 3201// Category cache for fancy UI (5 minutes duration) 3202const fancyUICategoryCache = new Map(); 3203const FANCY_UI_CATEGORY_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes 3204 3205// Helper functions for Wikipedia category API calls in fancy UI 3206async function fetchWikipediaCategoryMembersFancyUI(pageTitle, limit = 20) { 3207 const cacheKey = `members_${pageTitle}_${limit}`; 3208 const cached = fancyUICategoryCache.get(cacheKey); 3209 3210 // Return cached result if still valid 3211 if (cached && Date.now() - cached.timestamp < FANCY_UI_CATEGORY_CACHE_DURATION) { 3212 console.log(`[FancyUI] Using cached category members for: ${pageTitle}`); 3213 return cached.data; 3214 } 3215 3216 try { 3217 console.log(`[FancyUI] Fetching fresh category members for: ${pageTitle}`); 3218 const response = await fetch('/.netlify/functions/api/wikipedia-category-members', { 3219 method: 'POST', 3220 headers: { 3221 'Content-Type': 'application/json' 3222 }, 3223 body: JSON.stringify({ pageTitle, limit }) 3224 }); 3225 3226 if (!response.ok) { 3227 throw new Error(`HTTP error! status: ${response.status}`); 3228 } 3229 3230 const data = await response.json(); 3231 3232 // Cache the result 3233 fancyUICategoryCache.set(cacheKey, { 3234 data: data, 3235 timestamp: Date.now() 3236 }); 3237 3238 return data; 3239 } catch (error) { 3240 console.error(`[FancyUI] Error fetching category members for ${pageTitle}:`, error); 3241 return { success: false, error: error.message }; 3242 } 3243} 3244 3245async function fetchWikipediaPageCategoriesFancyUI(pageTitle) { 3246 const cacheKey = `categories_${pageTitle}`; 3247 const cached = fancyUICategoryCache.get(cacheKey); 3248 3249 // Return cached result if still valid 3250 if (cached && Date.now() - cached.timestamp < FANCY_UI_CATEGORY_CACHE_DURATION) { 3251 console.log(`[FancyUI] Using cached page categories for: ${pageTitle}`); 3252 return cached.data; 3253 } 3254 3255 try { 3256 console.log(`[FancyUI] Fetching fresh page categories for: ${pageTitle}`); 3257 const response = await fetch('/.netlify/functions/api/wikipedia-page-categories', { 3258 method: 'POST', 3259 headers: { 3260 'Content-Type': 'application/json' 3261 }, 3262 body: JSON.stringify({ pageTitle }) 3263 }); 3264 3265 if (!response.ok) { 3266 throw new Error(`HTTP error! status: ${response.status}`); 3267 } 3268 3269 const data = await response.json(); 3270 3271 // Cache the result 3272 fancyUICategoryCache.set(cacheKey, { 3273 data: data, 3274 timestamp: Date.now() 3275 }); 3276 3277 return data; 3278 } catch (error) { 3279 console.error(`[FancyUI] Error fetching page categories for ${pageTitle}:`, error); 3280 return { success: false, error: error.message }; 3281 } 3282} 3283 3284// ð CRITICAL: Expose createFancyUIPreview to global scope for main.js access 3285if (typeof window !== 'undefined') { 3286 window.createFancyUIPreview = createFancyUIPreview; 3287 console.log('ð¯ [Global] createFancyUIPreview function exposed to window scope'); 3288} else if (typeof global !== 'undefined') { 3289 global.createFancyUIPreview = createFancyUIPreview; 3290 console.log('ð¯ [Global] createFancyUIPreview function exposed to global scope'); 3291}
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.