1/** 2 * Enhanced Global Search - v1.27.0 3 * Comprehensive search with external APIs and semantic understanding 4 * Features: 5 * - Enhanced metadata extraction (dates, sources, authors, keywords) 6 * - Fuzzy matching for typo tolerance 7 * - Advanced filtering (type, date, source credibility) 8 * - Better relevance scoring with TF-IDF 9 * - Search suggestions and auto-complete 10 * - Export results (JSON/CSV) 11 * - Improved UI with snippets and highlights 12 * - External API integration (arXiv, CrossRef) 13 * - Semantic search with natural language understanding 14 * - Question answering system 15 */ 16 17class EnhancedGlobalSearch { 18 constructor() { 19 this.searchIndex = []; 20 this.searchHistory = []; // Search history disabled 21 this.currentResultIndex = -1; 22 this.results = []; 23 this.externalResults = []; 24 this.directAnswer = null; 25 26 // Get translation function 27 this.t = window.t || ((key) => key); 28 29 // Initialize external API integration 30 this.apiIntegration = window.SearchAPIIntegration ? new window.SearchAPIIntegration() : null; 31 32 // Initialize semantic search 33 this.semanticSearch = null; // Will be initialized after index is built 34 this.filters = { 35 type: 'all', // all, article, gallery, publication, faq 36 dateRange: 'all', // all, recent (last 6 months), older 37 source: 'all' // all, official (NASA/ESA), scientific, news 38 }; 39 this.sortBy = 'relevance'; // relevance, date, popularity 40 41 // Keywords and synonyms for better matching 42 this.synonyms = { 43 'interstellar': ['extrasolar', 'extragalactic', 'alien'], 44 'comet': ['cometary', 'coma', 'nucleus'], 45 'perihelion': ['closest approach', 'closest to sun'], 46 'discovery': ['detected', 'found', 'identified'], 47 'orbit': ['trajectory', 'path'], 48 'observation': ['imaging', 'photograph', 'picture'], 49 'mars': ['martian'], 50 'earth': ['terrestrial'], 51 'jupiter': ['jovian'] 52 }; 53 54 // Source credibility scores 55 this.sourceCredibility = { 56 'nasa': 100, 57 'esa': 100, 58 'jpl': 100, 59 'nature': 90, 60 'science': 90, 61 'arxiv': 80, 62 'phys.org': 70, 63 'sciencedaily': 70, 64 'space.com': 60, 65 'iflscience': 60 66 }; 67 68 this.buildSearchIndex(); 69 this.initUI(); 70 } 71 72 /** 73 * Build enhanced search index with metadata 74 */ 75 buildSearchIndex() { 76 // console.log('[Enhanced Search] Building search index with metadata...'); 77 78 // Index articles with enhanced metadata 79 document.querySelectorAll('.article-card').forEach((article, index) => { 80 const titleElem = article.querySelector('h3, [data-i18n*="article"]'); 81 const sourceElem = article.querySelector('.source'); 82 const linkElem = article.querySelector('a'); 83 84 if (titleElem && linkElem) { 85 const title = titleElem.textContent.replace('NEW', '').trim(); 86 const sourceText = sourceElem ? sourceElem.textContent : ''; 87 88 // Extract date and source from "Source | Date" format 89 const parts = sourceText.split('|').map(p => p.trim()); 90 const source = parts[0] || ''; 91 const dateStr = parts[1] || ''; 92 93 // Parse date 94 let date = null; 95 if (dateStr) { 96 date = this.parseDate(dateStr); 97 } 98 99 // Determine if it's a research paper (arXiv) or news article 100 const isArxiv = linkElem.href.includes('arxiv.org') || source.toLowerCase().includes('arxiv'); 101 const isResearchPaper = isArxiv || linkElem.href.includes('doi.org'); 102 const itemType = isResearchPaper ? 'publication' : 'article'; 103 104 // Calculate source credibility 105 const credibility = this.getSourceCredibility(source); 106 107 // Extract keywords from title
108 const keywords = this.extractKeywords(title); 109 110 // Check if article is marked as NEW 111 const isNew = article.querySelector('.new-badge, [data-i18n="articles.new_badge"]') !== null; 112 113 this.searchIndex.push({ 114 type: itemType, 115 title: title, 116 content: sourceText, 117 url: linkElem.href, 118 source: source, 119 date: date, 120 dateStr: dateStr, 121 credibility: credibility, 122 keywords: keywords, 123 isNew: isNew, 124 isArxiv: isArxiv, 125 isPeerReviewed: !isArxiv && isResearchPaper, 126 element: article, 127 index: index 128 }); 129 } 130 }); 131 132 // Index gallery items with enhanced metadata 133 document.querySelectorAll('.gallery-item').forEach((item, index) => { 134 const titleElem = item.querySelector('h4'); 135 const descElem = item.querySelector('p'); 136 const metaElem = item.querySelector('.gallery-meta'); 137 const linkElem = item.querySelector('a'); 138 const imgElem = item.querySelector('img'); 139 140 if (titleElem && linkElem) { 141 const title = titleElem.textContent.replace('NEW', '').trim(); 142 const description = descElem ? descElem.textContent.trim() : ''; 143 const metaText = metaElem ? metaElem.textContent : ''; 144 145 // Extract photographer/source and date from metadata 146 const parts = metaText.split('â¢').map(p => p.trim()); 147 const photographer = parts[0] || ''; 148 const dateStr = parts[1] || ''; 149 150 let date = null; 151 if (dateStr) { 152 date = this.parseDate(dateStr); 153 } 154 155 // Extract alt text and keywords 156 const altText = imgElem ? imgElem.alt : ''; 157 const keywords = this.extractKeywords(title + ' ' + description + ' ' + altText); 158 159 const isNew = item.querySelector('.new-badge, [data-i18n="articles.new_badge"]') !== null; 160 161 this.searchIndex.push({ 162 type: 'gallery', 163 title: title, 164 content: description, 165 url: linkElem.href, 166 source: photographer, 167 date: date, 168 dateStr: dateStr, 169 credibility: 70, // Default for gallery 170 keywords: keywords, 171 altText: altText, 172 isNew: isNew, 173 element: item, 174 index: index 175 }); 176 } 177 }); 178 179 // Index publications with enhanced metadata 180 document.querySelectorAll('.publication-item, .article-link').forEach((pub, index) => { 181 const titleElem = pub.querySelector('strong, b') || pub; 182 const linkElem = pub.querySelector('a') || pub; 183 184 if (titleElem && linkElem.href) { 185 const title = titleElem.textContent.trim(); 186 187 // Detect if it's arXiv or peer-reviewed 188 const isArxiv = linkElem.href.includes('arxiv.org'); 189 const isPeerReviewed = !isArxiv && ( 190 linkElem.href.includes('nature.com') || 191 linkElem.href.includes('science.org') || 192 linkElem.href.includes('doi.org') 193 ); 194 195 const credibility = isPeerReviewed ? 90 : (isArxiv ? 80 : 70); 196 const keywords = this.extractKeywords(title); 197 198 this.searchIndex.push({ 199 type: 'publication', 200 title: title, 201 content: '', 202 url: linkElem.href, 203 source: isArxiv ? 'arXiv' : (isPeerReviewed ? 'Peer-Reviewed' : 'Publication'), 204 date: null, 205 dateStr: '', 206 credibility: credibility, 207 keywords: keywords, 208 isPeerReviewed: isPeerReviewed, 209 isArxiv: isArxiv, 210 element: pub, 211 index: index 212 }); 213 } 214 }); 215 216 // Index FAQ items 217 document.querySelectorAll('.faq-item').forEach((faq, index) => { 218 const questionElem = faq.querySelector('.faq-question'); 219 const answerElem = faq.querySelector('.faq-answer'); 220 221 if (questionElem) { 222 const question = questionElem.textContent.trim(); 223 const answer = answerElem ? answerElem.textContent.trim() : '';
224 const keywords = this.extractKeywords(question + ' ' + answer); 225 226 this.searchIndex.push({ 227 type: 'faq', 228 title: question, 229 content: answer, 230 url: '#' + (faq.id || 'faq'), 231 source: 'FAQ', 232 date: null, 233 dateStr: '', 234 credibility: 100, // Official FAQ 235 keywords: keywords, 236 element: faq, 237 index: index 238 }); 239 } 240 }); 241 242 // Build term frequency map for TF-IDF 243 this.buildTermFrequencyMap(); 244 245 // Initialize semantic search now that index is built 246 if (window.SemanticSearch) { 247 this.semanticSearch = new window.SemanticSearch(this.searchIndex); 248 // console.log('[Enhanced Search] Semantic search initialized'); 249 } 250 251 // console.log(`[Enhanced Search] Index built: ${this.searchIndex.length} items`); 252 // console.log(`[Enhanced Search] Types: Articles=${this.searchIndex.filter(i => i.type === 'article').length}, Gallery=${this.searchIndex.filter(i => i.type === 'gallery').length}, Publications=${this.searchIndex.filter(i => i.type === 'publication').length}, FAQs=${this.searchIndex.filter(i => i.type === 'faq').length}`); 253 254 // Build autocomplete suggestions 255 this.buildSuggestions(); 256 257 // Optionally pre-load external API results for 3I/ATLAS 258 if (this.apiIntegration) { 259 // console.log('[Enhanced Search] External APIs available'); 260 } 261 } 262 263 /** 264 * Build autocomplete suggestions from search index 265 */ 266 buildSuggestions() { 267 this.suggestions = new Set(); 268 269 // Common search queries - comprehensive list 270 const commonQueries = [ 271 // Mission milestones 272 'perihelion', 'discovery', 'closest approach', 'when discovered', 'when perihelion', 273 274 // Physical properties 275 'composition', 'water', 'size', 'diameter', 'mass', 'coma', 'tail', 276 'carbon dioxide', 'co2', 'cyanide', 'nickel', 277 278 // Orbital mechanics 279 'orbit', 'trajectory', 'speed', 'velocity', 'distance', 'eccentricity', 280 'hyperbolic', 'orbital period', 281 282 // Celestial bodies 283 'mars', 'earth', 'jupiter', 'sun', 'solar system', 284 285 // Observations 286 'telescope', 'images', 'pictures', 'photos', 'observations', 287 'hubble', 'webb', 'swift', 'atlas', 288 289 // Scientific content 290 'papers', 'research', 'articles', 'publications', 'arxiv', 291 292 // Organizations 293 'nasa', 'esa', 'jpl', 'cnsa', 294 295 // General 296 'comet', 'interstellar', 'alien', 'extrasolar', 297 298 // Questions 299 'when', 'what', 'how', 'where', 'why', 'who' 300 ]; 301 302 commonQueries.forEach(q => this.suggestions.add(q.toLowerCase())); 303 304 // Extract popular keywords from titles 305 this.searchIndex.forEach(item => { 306 const titleWords = item.title.toLowerCase() 307 .replace(/[^\w\s]/g, ' ') 308 .split(/\s+/) 309 .filter(word => word.length > 3); 310 311 titleWords.slice(0, 3).forEach(word => this.suggestions.add(word)); // Top 3 words from each title 312 }); 313 314 this.suggestionsArray = Array.from(this.suggestions).sort(); 315 // console.log(`[Enhanced Search] Built ${this.suggestionsArray.length} autocomplete suggestions`); 316 } 317 318 /** 319 * Get autocomplete suggestions for a query 320 */ 321 getAutocomplete(query) { 322 if (!query || query.length < 2) return []; 323 324 const lowerQuery = query.toLowerCase(); 325 326 return this.suggestionsArray 327 .filter(suggestion => suggestion.includes(lowerQuery)) 328 .sort((a, b) => { 329 // Prioritize suggestions that start with the query 330 const aStarts = a.startsWith(lowerQuery); 331 const bStarts = b.startsWith(lowerQuery); 332 if (aStarts && !bStarts) return -1; 333 if (!aStarts && bStarts) return 1; 334 return a.length - b.length; // Shorter suggestions first 335 }) 336 .slice(0, 5); // Top 5 suggestions 337 } 338 339 /** 340 * Extract keywords from text 341 */ 342 extractKeywords(text) {
343 // Remove common stop words 344 const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might', 'must', 'can', 'this', 'that', 'these', 'those', 'it', 'its', 'they', 'their', 'them']); 345 346 const words = text.toLowerCase() 347 .replace(/[^\w\s]/g, ' ') 348 .split(/\s+/) 349 .filter(word => word.length > 2 && !stopWords.has(word)); 350 351 return [...new Set(words)]; // Unique keywords 352 } 353 354 /** 355 * Parse date from various formats 356 */ 357 parseDate(dateStr) { 358 // Try to parse "Month DD, YYYY" format 359 const monthNames = { 360 'january': 0, 'jan': 0, 361 'february': 1, 'feb': 1, 362 'march': 2, 'mar': 2, 363 'april': 3, 'apr': 3, 364 'may': 4, 365 'june': 5, 'jun': 5, 366 'july': 6, 'jul': 6, 367 'august': 7, 'aug': 7, 368 'september': 8, 'sep': 8, 'sept': 8, 369 'october': 9, 'oct': 9, 370 'november': 10, 'nov': 10, 371 'december': 11, 'dec': 11 372 }; 373 374 // Prefer the release date in parentheses, e.g. "(Released July 6, 2026)" 375 const releasedMatch = dateStr.match(/\((?:Released\s+)?([a-z]+\s+\d{1,2},?\s+\d{4})\)/i); 376 const candidateStr = releasedMatch ? releasedMatch[1] : dateStr; 377 378 // Match "Month DD, YYYY" 379 const match = candidateStr.match(/([a-z]+)\s+(\d{1,2}),?\s+(\d{4})/i); 380 if (match) {
381 const month = monthNames[match[1].toLowerCase()]; 382 const day = parseInt(match[2]); 383 const year = parseInt(match[3]); 384 385 if (month !== undefined && day && year) { 386 return new Date(year, month, day); 387 } 388 } 389 390 // Fallback: try Date.parse 391 const parsed = Date.parse(dateStr); 392 return isNaN(parsed) ? null : new Date(parsed); 393 } 394 395 /** 396 * Get source credibility score 397 */ 398 getSourceCredibility(source) { 399 const sourceLower = source.toLowerCase(); 400 401 for (const [key, score] of Object.entries(this.sourceCredibility)) { 402 if (sourceLower.includes(key)) { 403 return score; 404 } 405 } 406 407 return 50; // Default credibility 408 } 409 410 /** 411 * Build term frequency map for TF-IDF 412 */ 413 buildTermFrequencyMap() { 414 this.termDocumentFrequency = {}; 415 416 this.searchIndex.forEach(item => { 417 const terms = new Set([...item.keywords, ...item.title.toLowerCase().split(/\s+/)]); 418 419 terms.forEach(term => { 420 if (!this.termDocumentFrequency[term]) { 421 this.termDocumentFrequency[term] = 0; 422 } 423 this.termDocumentFrequency[term]++; 424 }); 425 }); 426 } 427 428 /** 429 * Calculate TF-IDF score 430 */ 431 calculateTFIDF(item, term) { 432 const termFreq = item.keywords.filter(k => k === term).length; 433 const docFreq = this.termDocumentFrequency[term] || 1; 434 const idf = Math.log(this.searchIndex.length / docFreq); 435 436 return termFreq * idf; 437 } 438 439 /** 440 * Levenshtein distance for fuzzy matching 441 */ 442 levenshteinDistance(str1, str2) { 443 const len1 = str1.length; 444 const len2 = str2.length; 445 const matrix = []; 446 447 for (let i = 0; i <= len1; i++) { 448 matrix[i] = [i]; 449 } 450 451 for (let j = 0; j <= len2; j++) { 452 matrix[0][j] = j; 453 } 454 455 for (let i = 1; i <= len1; i++) { 456 for (let j = 1; j <= len2; j++) { 457 if (str1[i - 1] === str2[j - 1]) { 458 matrix[i][j] = matrix[i - 1][j - 1]; 459 } else { 460 matrix[i][j] = Math.min( 461 matrix[i - 1][j - 1] + 1, // substitution 462 matrix[i][j - 1] + 1, // insertion 463 matrix[i - 1][j] + 1 // deletion 464 ); 465 } 466 } 467 } 468 469 return matrix[len1][len2]; 470 } 471 472 /** 473 * Fuzzy match with typo tolerance 474 */ 475 fuzzyMatch(query, target, threshold = 2) { 476 const distance = this.levenshteinDistance(query.toLowerCase(), target.toLowerCase()); 477 const maxLen = Math.max(query.length, target.length); 478 479 // Allow up to threshold edits OR 20% difference for longer words 480 return distance <= threshold || distance <= maxLen * 0.2; 481 } 482 483 /** 484 * Enhanced search with fuzzy matching, filters, and TF-IDF scoring 485 */ 486 search(query) { 487 if (!query || query.length < 2) return []; 488 489 query = query.toLowerCase().trim(); 490 let words = query.split(/\s+/); 491 492 // Semantic search processing 493 let semanticResults = null; 494 if (this.semanticSearch) { 495 semanticResults = this.semanticSearch.search(query, []); 496 497 // Store direct answer if found 498 this.directAnswer = semanticResults.answer; 499 500 // Expand query with semantic terms 501 if (semanticResults.expandedTerms && semanticResults.expandedTerms.length > 0) { 502 words = [...words, ...semanticResults.expandedTerms]; 503 } 504 } 505 506 // Apply filters 507 let filteredIndex = [...this.searchIndex]; 508 509 // Type filter 510 if (this.filters.type !== 'all') { 511 filteredIndex = filteredIndex.filter(item => item.type === this.filters.type); 512 } 513 514 // Date range filter 515 if (this.filters.dateRange !== 'all') { 516 const now = new Date(); 517 const sixMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate()); 518 519 if (this.filters.dateRange === 'recent') { 520 filteredIndex = filteredIndex.filter(item => item.date && item.date >= sixMonthsAgo); 521 } else if (this.filters.dateRange === 'older') { 522 filteredIndex = filteredIndex.filter(item => item.date && item.date < sixMonthsAgo); 523 } 524 } 525 526 // Source credibility filter 527 if (this.filters.source === 'official') { 528 filteredIndex = filteredIndex.filter(item => item.credibility >= 90); 529 } else if (this.filters.source === 'scientific') { 530 filteredIndex = filteredIndex.filter(item => item.credibility >= 70); 531 } 532
533 // Search with enhanced scoring (includes semantic boost) 534 const localResults = filteredIndex 535 .map(item => { 536 const baseScore = this.calculateRelevance(item, query, words); 537 const semanticBoost = semanticResults && this.semanticSearch ? 538 this.semanticSearch.scoreSemanticRelevance(item, query, semanticResults.intent) : 0; 539 540 return { 541 ...item, 542 score: baseScore + semanticBoost 543 }; 544 }) 545 .filter(item => item.score > 0) 546 .sort((a, b) => { 547 // Apply sorting based on user preference 548 if (this.sortBy === 'date') { 549 // Sort by date (newest first) 550 if (!a.date && !b.date) return b.score - a.score; // Fallback to relevance 551 if (!a.date) return 1; // Items without date go to end 552 if (!b.date) return -1; 553 return b.date - a.date; 554 } else if (this.sortBy === 'popularity') { 555 // Sort by credibility score (proxy for popularity) 556 const credDiff = b.credibility - a.credibility; 557 if (credDiff !== 0) return credDiff; 558 return b.score - a.score; // Fallback to relevance 559 } else { 560 // Default: Sort by relevance score 561 return b.score - a.score; 562 } 563 }) 564 .slice(0, 20); // Top 20 local results 565 566 return localResults; 567 } 568 569 /** 570 * Calculate relevance score with TF-IDF and fuzzy matching 571 */ 572 calculateRelevance(item, query, words) { 573 let score = 0; 574 const title = item.title.toLowerCase(); 575 const content = item.content.toLowerCase(); 576 const keywords = item.keywords.join(' '); 577 578 // Exact title match = highest score 579 if (title === query) { 580 score += 200; 581 } 582 // Title starts with query 583 else if (title.startsWith(query)) { 584 score += 150; 585 } 586 // Title contains query 587 else if (title.includes(query)) { 588 score += 100; 589 } 590 591 // Check each word with fuzzy matching 592 words.forEach(word => { 593 // Exact keyword match 594 if (item.keywords.includes(word)) { 595 score += 30 + this.calculateTFIDF(item, word) * 10; 596 } 597 598 // Fuzzy keyword match 599 else { 600 const fuzzyMatches = item.keywords.filter(k => this.fuzzyMatch(word, k)); 601 if (fuzzyMatches.length > 0) { 602 score += 20; 603 } 604 } 605 606 // Title word match 607 if (title.includes(word)) { 608 score += 25; 609 } 610 611 // Content word match 612 if (content.includes(word)) { 613 score += 10; 614 } 615 616 // Synonym matching 617 for (const [key, synonyms] of Object.entries(this.synonyms)) { 618 if (word === key || synonyms.includes(word)) { 619 const allTerms = [key, ...synonyms]; 620 const matchesKeywords = allTerms.some(term => item.keywords.includes(term)); 621 const matchesTitle = allTerms.some(term => title.includes(term)); 622 623 if (matchesKeywords) score += 15; 624 if (matchesTitle) score += 10; 625 } 626 } 627 }); 628 629 // Boost score based on metadata 630 631 // Credibility boost 632 score += item.credibility * 0.3; 633 634 // Recency boost (newer content gets slight boost) 635 if (item.date) { 636 const daysSincePublish = (new Date() - item.date) / (1000 * 60 * 60 * 24); 637 if (daysSincePublish < 180) { // Last 6 months 638 score += 20; 639 } 640 } 641 642 // NEW badge boost 643 if (item.isNew) { 644 score += 15; 645 } 646 647 // Peer-reviewed boost 648 if (item.isPeerReviewed) { 649 score += 25; 650 } 651 652 return score; 653 } 654 655 /** 656 * Get search suggestions based on query 657 */ 658 getSuggestions(query) { 659 if (!query || query.length < 2) return []; 660 661 const suggestions = new Set(); 662 const queryLower = query.toLowerCase(); 663 664 // Search in titles 665 this.searchIndex.forEach(item => { 666 const titleWords = item.title.toLowerCase().split(/\s+/); 667 titleWords.forEach(word => { 668 if (word.startsWith(queryLower) && word.length > 2) { 669 suggestions.add(word); 670 } 671 }); 672 }); 673 674 // Add synonyms
675 Object.keys(this.synonyms).forEach(key => { 676 if (key.startsWith(queryLower)) { 677 suggestions.add(key); 678 } 679 }); 680 681 return Array.from(suggestions).slice(0, 5); 682 } 683 684 /** 685 * Export search results 686 */ 687 exportResults(format = 'json') { 688 if (this.results.length === 0) { 689 alert('No results to export'); 690 return; 691 } 692 693 const exportData = this.results.map(item => ({ 694 title: item.title, 695 type: item.type, 696 url: item.url, 697 source: item.source, 698 date: item.dateStr, 699 credibility: item.credibility, 700 relevance_score: item.score.toFixed(2) 701 })); 702 703 let content, filename, mimeType; 704 705 if (format === 'json') { 706 content = JSON.stringify(exportData, null, 2); 707 filename = `3iatlas-search-results-${Date.now()}.json`; 708 mimeType = 'application/json'; 709 } else if (format === 'csv') { 710 const headers = ['Title', 'Type', 'URL', 'Source', 'Date', 'Credibility', 'Relevance Score']; 711 const rows = exportData.map(item => [ 712 `"${item.title.replace(/"/g, '""')}"`, 713 item.type, 714 item.url, 715 `"${item.source.replace(/"/g, '""')}"`, 716 item.date, 717 item.credibility, 718 item.relevance_score 719 ]); 720 721 content = headers.join(',') + '\n' + rows.map(r => r.join(',')).join('\n'); 722 filename = `3iatlas-search-results-${Date.now()}.csv`; 723 mimeType = 'text/csv'; 724 } 725 726 // Download file 727 const blob = new Blob([content], { type: mimeType }); 728 const link = document.createElement('a'); 729 link.href = URL.createObjectURL(blob); 730 link.download = filename; 731 link.click(); 732 URL.revokeObjectURL(link.href); 733 734 if (window.Analytics) { 735 Analytics.trackEvent('search-export', { format, count: this.results.length }); 736 } 737 } 738 739 /** 740 * Initialize enhanced search UI 741 */ 742 initUI() { 743 // Check if inline container exists (for inline search below Mission Control) 744 const inlineContainer = document.getElementById('inline-search-container'); 745 746 if (inlineContainer) { 747 // Create inline search UI 748 inlineContainer.innerHTML = ` 749 <div class="search-container inline" role="search" aria-label="Search"> 750 <div class="search-input-wrapper"> 751 <i class="fas fa-search" aria-hidden="true"></i> 752 <input 753 type="text" 754 id="global-search-input" 755 placeholder="${this.t('search.placeholder')}" 756 autocomplete="off" 757 aria-label="Search input" 758 list="search-autocomplete" 759 /> 760 <datalist id="search-autocomplete"></datalist> 761 <button class="search-clear" aria-label="Clear search" style="display: none;"> 762 <i class="fas fa-times-circle"></i> 763 </button> 764 </div> 765 766 <!-- Search Filters --> 767 <div class="search-filters"> 768 <div class="filter-group"> 769 <label class="filter-label">${this.t('search.filter.type')}</label> 770 <button class="filter-btn active" data-filter-type="all">${this.t('search.filter.type.all')}</button> 771 <button class="filter-btn" data-filter-type="article">${this.t('search.filter.type.articles')}</button> 772 <button class="filter-btn" data-filter-type="gallery">${this.t('search.filter.type.gallery')}</button> 773 <button class="filter-btn" data-filter-type="publication">${this.t('search.filter.type.papers')}</button> 774 <button class="filter-btn" data-filter-type="faq">${this.t('search.filter.type.faqs')}</button> 775 </div> 776 <div class="filter-group"> 777 <label class="filter-label">${this.t('search.filter.date')}</label> 778 <button class="filter-btn active" data-filter-date="all">${this.t('search.filter.date.all')}</button> 779 <button class="filter-btn" data-filter-date="recent">${this.t('search.filter.date.recent')}</button> 780 <button class="filter-btn" data-filter-date="older">${this.t('search.filter.date.older')}</button> 781 </div> 782 <div class="filter-group"> 783 <label class="filter-label">${this.t('search.filter.source')}</label> 784 <button class="filter-btn active" data-filter-source="all">${this.t('search.filter.source.all')}</button> 785 <button class="filter-btn" data-filter-source="official">${this.t('search.filter.source.official')}</button> 786 <button class="filter-btn" data-filter-source="scientific">${this.t('search.filter.source.scientific')}</button> 787 </div> 788 <div class="filter-group"> 789 <label class="filter-label">${this.t('search.sort')}</label> 790 <button class="sort-btn active" data-sort="relevance">${this.t('search.sort.relevance')}</button> 791 <button class="sort-btn" data-sort="date">${this.t('search.sort.date')}</button> 792 <button class="sort-btn" data-sort="popularity">${this.t('search.sort.popularity')}</button> 793 </div> 794 </div> 795 796 <!-- Result count --> 797 <div id="search-result-count" class="search-result-count" style="display: none;"></div> 798 799 <!-- Results container --> 800 <div id="search-results" class="search-results" role="listbox" aria-label="Search results"></div> 801 </div> 802 `; 803 } else { 804 // Create search overlay with enhanced UI (modal version) 805 const searchOverlay = document.createElement('div'); 806 searchOverlay.id = 'search-overlay'; 807 searchOverlay.className = 'search-overlay'; 808 searchOverlay.innerHTML = ` 809 <div class="search-container" role="dialog" aria-label="Search" aria-modal="true"> 810 <div class="search-input-wrapper"> 811 <i class="fas fa-search" aria-hidden="true"></i> 812 <input 813 type="text" 814 id="global-search-input" 815 placeholder="${this.t('search.placeholder')}" 816 autocomplete="off" 817 aria-label="Search input" 818 list="search-autocomplete" 819 /> 820 <datalist id="search-autocomplete"></datalist> 821 <button class="search-clear" aria-label="Clear search" style="display: none;"> 822 <i class="fas fa-times-circle"></i> 823 </button> 824 <button class="search-close" aria-label="Close search"> 825 <i class="fas fa-times"></i> 826 </button> 827 </div> 828 829 <!-- Search Filters --> 830 <div class="search-filters"> 831 <div class="filter-group"> 832 <label class="filter-label">${this.t('search.filter.type')}</label> 833 <button class="filter-btn active" data-filter-type="all">${this.t('search.filter.type.all')}</button> 834 <button class="filter-btn" data-filter-type="article">${this.t('search.filter.type.articles')}</button> 835 <button class="filter-btn" data-filter-type="gallery">${this.t('search.filter.type.gallery')}</button> 836 <button class="filter-btn" data-filter-type="publication">${this.t('search.filter.type.papers')}</button> 837 <button class="filter-btn" data-filter-type="faq">${this.t('search.filter.type.faqs')}</button> 838 </div> 839 <div class="filter-group"> 840 <label class="filter-label">${this.t('search.filter.date')}</label> 841 <button class="filter-btn active" data-filter-date="all">${this.t('search.filter.date.all')}</button> 842 <button class="filter-btn" data-filter-date="recent">${this.t('search.filter.date.recent')}</button> 843 <button class="filter-btn" data-filter-date="older">${this.t('search.filter.date.older')}</button> 844 </div> 845 <div class="filter-group"> 846 <label class="filter-label">${this.t('search.filter.source')}</label> 847 <button class="filter-btn active" data-filter-source="all">${this.t('search.filter.source.all')}</button> 848 <button class="filter-btn" data-filter-source="official">${this.t('search.filter.source.official')}</button> 849 <button class="filter-btn" data-filter-source="scientific">${this.t('search.filter.source.scientific')}</button> 850 </div> 851 <div class="filter-group"> 852 <label class="filter-label">${this.t('search.sort')}</label> 853 <button class="sort-btn active" data-sort="relevance">${this.t('search.sort.relevance')}</button> 854 <button class="sort-btn" data-sort="date">${this.t('search.sort.date')}</button> 855 <button class="sort-btn" data-sort="popularity">${this.t('search.sort.popularity')}</button> 856 </div> 857 </div> 858 859 <!-- Result count --> 860 <div id="search-result-count" class="search-result-count" style="display: none;"></div> 861 862 <!-- Results container --> 863 <div id="search-results" class="search-results" role="listbox" aria-label="Search results"></div> 864 865 <!-- Footer --> 866 <div class="search-footer">
867 <span class="search-hint"> 868 <kbd>â</kbd> <kbd>â</kbd> navigate · <kbd>Enter</kbd> select · <kbd>Esc</kbd> close 869 </span> 870 </div> 871 </div> 872 `; 873 document.body.appendChild(searchOverlay); 874 } 875 876 // Get elements 877 const input = document.getElementById('global-search-input'); 878 const container = inlineContainer || document.getElementById('search-overlay'); 879 const closeBtn = container.querySelector('.search-close'); 880 const clearBtn = container.querySelector('.search-clear'); 881 const resultsContainer = document.getElementById('search-results'); 882 const resultCount = document.getElementById('search-result-count'); 883 884 // Input event with debouncing 885 let searchTimeout; 886 input.addEventListener('input', (e) => { 887 const query = e.target.value; 888 clearBtn.style.display = query ? 'block' : 'none'; 889 890 // Update autocomplete suggestions 891 const datalist = document.getElementById('search-autocomplete'); 892 if (datalist && query.length >= 2) { 893 const suggestions = this.getAutocomplete(query); 894 datalist.innerHTML = suggestions.map(s => `<option value="${s}">`).join(''); 895 } else if (datalist) { 896 datalist.innerHTML = ''; 897 } 898 899 clearTimeout(searchTimeout); 900 searchTimeout = setTimeout(() => { 901 this.handleSearch(query); 902 }, 200); // Debounce 200ms 903 }); 904 905 // Clear button 906 clearBtn.addEventListener('click', () => { 907 input.value = ''; 908 clearBtn.style.display = 'none'; 909 resultsContainer.innerHTML = ''; 910 resultCount.style.display = 'none'; 911 input.focus(); 912 }); 913 914 // Close button (only for modal) 915 if (closeBtn) { 916 closeBtn.addEventListener('click', () => { 917 this.closeSearch(); 918 }); 919 } 920 921 // Type filter buttons 922 const filterBtns = container.querySelectorAll('[data-filter-type]'); 923 filterBtns.forEach(btn => { 924 btn.addEventListener('click', () => { 925 const filterType = btn.getAttribute('data-filter-type'); 926 this.filters.type = filterType; 927 928 // Update active state within same group 929 const parent = btn.parentElement; 930 parent.querySelectorAll('[data-filter-type]').forEach(b => b.classList.remove('active')); 931 btn.classList.add('active'); 932 933 // Re-run search 934 this.handleSearch(input.value); 935 }); 936 }); 937 938 // Date filter buttons 939 const dateBtns = container.querySelectorAll('[data-filter-date]'); 940 dateBtns.forEach(btn => { 941 btn.addEventListener('click', () => { 942 const filterDate = btn.getAttribute('data-filter-date'); 943 this.filters.dateRange = filterDate; 944 945 // Update active state within same group 946 const parent = btn.parentElement; 947 parent.querySelectorAll('[data-filter-date]').forEach(b => b.classList.remove('active')); 948 btn.classList.add('active'); 949 950 // Re-run search 951 this.handleSearch(input.value); 952 }); 953 }); 954 955 // Source filter buttons 956 const sourceBtns = container.querySelectorAll('[data-filter-source]'); 957 sourceBtns.forEach(btn => { 958 btn.addEventListener('click', () => { 959 const filterSource = btn.getAttribute('data-filter-source'); 960 this.filters.source = filterSource; 961 962 // Update active state within same group 963 const parent = btn.parentElement; 964 parent.querySelectorAll('[data-filter-source]').forEach(b => b.classList.remove('active')); 965 btn.classList.add('active'); 966 967 // Re-run search 968 this.handleSearch(input.value); 969 }); 970 }); 971 972 // Sort buttons 973 const sortBtns = container.querySelectorAll('.sort-btn'); 974 sortBtns.forEach(btn => { 975 btn.addEventListener('click', () => { 976 const sortType = btn.getAttribute('data-sort'); 977 this.sortBy = sortType; 978 979 // Update active state within same group 980 const parent = btn.parentElement; 981 parent.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); 982 btn.classList.add('active'); 983 984 // Re-run search 985 this.handleSearch(input.value); 986 }); 987 }); 988 989 // Keyboard navigation 990 input.addEventListener('keydown', (e) => { 991 if (e.key === 'ArrowDown') { 992 e.preventDefault(); 993 this.navigateResults(1); 994 } else if (e.key === 'ArrowUp') { 995 e.preventDefault(); 996 this.navigateResults(-1); 997 } else if (e.key === 'Enter') { 998 e.preventDefault(); 999 this.selectResult(); 1000 } 1001 }); 1002 1003 // Click overlay to close (only for modal) 1004 if (!inlineContainer) { 1005 const searchOverlay = document.getElementById('search-overlay'); 1006 searchOverlay.addEventListener('click', (e) => { 1007 if (e.target === searchOverlay) { 1008 this.closeSearch(); 1009 } 1010 }); 1011 } 1012 1013 // Search toggle button in header (only for modal, hide if inline) 1014 const searchToggle = document.getElementById('searchToggle'); 1015 if (inlineContainer && searchToggle) { 1016 // Hide search toggle button if search is inline 1017 searchToggle.style.display = 'none'; 1018 // Show initial state for inline search 1019 this.handleSearch(''); 1020 } else if (searchToggle) { 1021 searchToggle.addEventListener('click', () => { 1022 this.openSearch(); 1023 }); 1024 } 1025 } 1026 1027 /** 1028 * Handle search query 1029 */ 1030 handleSearch(query) { 1031 this.results = this.search(query); 1032 this.currentResultIndex = -1; 1033 const resultsContainer = document.getElementById('search-results'); 1034 const resultCount = document.getElementById('search-result-count'); 1035 1036 if (!query || query.length < 2) { 1037 resultsContainer.innerHTML = this.renderSearchHistory(); 1038 resultCount.style.display = 'none'; 1039 this.directAnswer = null; 1040 this.externalResults = []; 1041 return; 1042 } 1043 1044 // Track search query in analytics 1045 if (window.Analytics) { 1046 Analytics.trackEvent('search-query', { 1047 query: query.substring(0, 50), // Limit query length for privacy 1048 resultCount: this.results.length 1049 }); 1050 } 1051 1052 // Build results HTML 1053 let resultsHTML = ''; 1054 1055 // Show direct answer if available 1056 if (this.directAnswer) { 1057 resultsHTML += ` 1058 <div class="search-direct-answer"> 1059 <div class="direct-answer-header"> 1060 <i class="fas fa-lightbulb"></i>
1061 <span>Direct Answer</span> 1062 <span class="answer-confidence">${Math.round(this.directAnswer.confidence * 100)}% confident</span> 1063 </div> 1064 <div class="direct-answer-content"> 1065 ${this.semanticSearch ? this.semanticSearch.highlightAnswer(this.directAnswer.answer, query) : this.escapeHtml(this.directAnswer.answer)} 1066 </div> 1067 <div class="direct-answer-source"> 1068 <i class="fas fa-info-circle"></i> Source: ${this.escapeHtml(this.directAnswer.source)} 1069 </div> 1070 </div> 1071 `; 1072 } 1073 1074 // Start external API search asynchronously - DISABLED for v1.25 1075 // if (this.apiIntegration && query.length >= 3) { 1076 // this.searchExternalAPIs(query); 1077 // } 1078 1079 // Show result count 1080 const totalCount = this.results.length + this.externalResults.length; 1081 resultCount.textContent = `${totalCount} result${totalCount !== 1 ? 's' : ''} found${this.externalResults.length > 0 ? ` (${this.externalResults.length} from external sources)` : ''}`; 1082 resultCount.style.display = 'block'; 1083 1084 if (this.results.length === 0 && this.externalResults.length === 0 && !this.directAnswer) { 1085 // Track zero results for analytics 1086 if (window.Analytics) { 1087 Analytics.trackEvent('search-no-results', { 1088 query: query.substring(0, 50) 1089 }); 1090 } 1091 1092 resultsContainer.innerHTML = ` 1093 <div class="search-no-results"> 1094 <i class="fas fa-search" aria-hidden="true"></i> 1095 <p>No results found for "${this.escapeHtml(query)}"</p> 1096 <p class="search-tip">Try different keywords or check spelling</p> 1097 ${this.apiIntegration ? '<p class="search-tip"><i class="fas fa-spinner fa-spin"></i> Searching external sources...</p>' : ''} 1098 </div> 1099 `; 1100 return; 1101 } 1102 1103 // Render local results 1104 resultsHTML += this.results.map((item, index) => ` 1105 <div class="search-result-item" data-index="${index}" data-url="${this.escapeHtml(item.url)}"> 1106 <div class="search-result-icon"> 1107 <i class="fas fa-${this.getIcon(item.type)}" aria-hidden="true"></i> 1108 </div> 1109 <div class="search-result-content"> 1110 <div class="search-result-title">${this.highlightMatch(item.title, query)}</div> 1111 <div class="search-result-meta"> 1112 <span class="search-result-type">${this.getTypeLabel(item.type)}</span> 1113 ${item.source ? `<span>·</span><span>${this.escapeHtml(item.source)}</span>` : ''} 1114 ${item.dateStr ? `<span>·</span><span>${this.escapeHtml(item.dateStr)}</span>` : ''} 1115 ${item.credibility >= 90 ? '<span class="badge-verified"><i class="fas fa-check-circle"></i> Verified</span>' : ''} 1116 ${item.isPeerReviewed ? '<span class="badge-peer-reviewed">Peer-Reviewed</span>' : ''} 1117 </div> 1118 ${item.content ? `<div class="search-result-preview">${this.createSnippet(item.content, query)}</div>` : ''} 1119 </div> 1120 <div class="search-result-score" title="Relevance score"> 1121 ${Math.round(item.score)} 1122 </div> 1123 </div> 1124 `).join(''); 1125 1126 // Set results HTML 1127 resultsContainer.innerHTML = resultsHTML; 1128 1129 // Add click handlers 1130 resultsContainer.querySelectorAll('.search-result-item').forEach((item, index) => { 1131 item.addEventListener('click', () => { 1132 const url = item.getAttribute('data-url'); 1133 const result = this.results[index]; 1134 1135 // Track result click in analytics 1136 if (window.Analytics && result) { 1137 Analytics.trackEvent('search-result-click', { 1138 query: query.substring(0, 50), 1139 type: result.type, 1140 position: index + 1, 1141 title: result.title.substring(0, 50) 1142 }); 1143 } 1144 1145 this.addToHistory(query); 1146 window.open(url, '_blank'); 1147 }); 1148 }); 1149 } 1150 1151 /** 1152 * Search external APIs asynchronously 1153 */ 1154 async searchExternalAPIs(query) { 1155 if (!this.apiIntegration) return; 1156 1157 const resultsContainer = document.getElementById('search-results'); 1158 const resultCount = document.getElementById('search-result-count'); 1159 1160 // Show loading indicator 1161 const loadingDiv = document.createElement('div'); 1162 loadingDiv.className = 'search-loading'; 1163 loadingDiv.innerHTML = ` 1164 <div class="loading-spinner"> 1165 <i class="fas fa-spinner fa-spin"></i>
1166 <span>Searching arXiv and CrossRef...</span> 1167 </div> 1168 `; 1169 resultsContainer.appendChild(loadingDiv); 1170 1171 try { 1172 // Search all external APIs 1173 const externalResults = await this.apiIntegration.searchAll(query); 1174 1175 this.externalResults = externalResults; 1176 1177 // Remove loading indicator 1178 loadingDiv.remove(); 1179 1180 // External results section disabled 1181 // if (externalResults.length > 0) { 1182 // // Add external results to the display 1183 // const externalHTML = ` 1184 // <div class="search-section-header"> 1185 // <i class="fas fa-cloud"></i> 1186 // <span>External Sources (${externalResults.length})</span> 1187 // </div> 1188 // ` + externalResults.map((item, index) => ` 1189 // <div class="search-result-item external-result" data-index="${index}" data-url="${this.escapeHtml(item.url)}"> 1190 // <div class="search-result-icon"> 1191 // <i class="fas fa-${this.getIcon(item.type)}" aria-hidden="true"></i> 1192 // </div> 1193 // <div class="search-result-content"> 1194 // <div class="search-result-title">${this.escapeHtml(item.title)}</div> 1195 // <div class="search-result-meta"> 1196 // <span class="search-result-type">${item.type}</span> 1197 // ${item.source ? `<span>·</span><span>${this.escapeHtml(item.source)}</span>` : ''} 1198 // ${item.dateStr ? `<span>·</span><span>${this.escapeHtml(item.dateStr)}</span>` : ''} 1199 // ${item.authors ? `<span>·</span><span>${this.escapeHtml(item.authors.substring(0, 50))}${item.authors.length > 50 ? '...' : ''}</span>` : ''} 1200 // ${item.credibility >= 90 ? '<span class="badge-verified"><i class="fas fa-check-circle"></i> Verified</span>' : ''} 1201 // ${item.isPeerReviewed ? '<span class="badge-peer-reviewed">Peer-Reviewed</span>' : ''} 1202 // ${item.isArxiv ? '<span class="badge-arxiv">arXiv</span>' : ''} 1203 // <span class="badge-external">External</span> 1204 // </div> 1205 // ${item.content ? `<div class="search-result-preview">${this.escapeHtml(item.content)}</div>` : ''} 1206 // </div> 1207 // <div class="search-result-score" title="Credibility score"> 1208 // ${item.credibility} 1209 // </div> 1210 // </div> 1211 // `).join(''); 1212 // 1213 // resultsContainer.innerHTML += externalHTML; 1214 // 1215 // // Update result count 1216 // const totalCount = this.results.length + this.externalResults.length; 1217 // resultCount.textContent = `${totalCount} result${totalCount !== 1 ? 's' : ''} found (${this.externalResults.length} from external sources)`; 1218 // 1219 // // Add click handlers for external results 1220 // resultsContainer.querySelectorAll('.external-result').forEach(item => { 1221 // item.addEventListener('click', () => { 1222 // const url = item.getAttribute('data-url'); 1223 // window.open(url, '_blank'); 1224 // 1225 // if (window.Analytics) { 1226 // Analytics.trackEvent('search-external-click', { source: 'external_api' }); 1227 // } 1228 // }); 1229 // }); 1230 // 1231 // console.log(`[Enhanced Search] Added ${externalResults.length} external results`); 1232 // } else { 1233 // console.log('[Enhanced Search] No external results found'); 1234 // } 1235 } catch (error) { 1236 console.error('[Enhanced Search] Error fetching external results:', error); 1237 loadingDiv.innerHTML = ` 1238 <div class="search-error"> 1239 <i class="fas fa-exclamation-triangle"></i>
1240 <span>Could not load external results</span> 1241 </div> 1242 `; 1243 setTimeout(() => loadingDiv.remove(), 3000); 1244 } 1245 } 1246 1247 /** 1248 * Create snippet with context around matched terms 1249 */ 1250 createSnippet(text, query, maxLength = 150) { 1251 const queryLower = query.toLowerCase(); 1252 const textLower = text.toLowerCase(); 1253 1254 // Find first occurrence of any query word 1255 const words = queryLower.split(/\s+/); 1256 let matchPos = -1; 1257 1258 for (const word of words) { 1259 const pos = textLower.indexOf(word); 1260 if (pos !== -1) { 1261 matchPos = pos; 1262 break; 1263 } 1264 } 1265 1266 if (matchPos === -1) { 1267 // No match found, return start of text 1268 return this.escapeHtml(text.substring(0, maxLength)) + (text.length > maxLength ? '...' : ''); 1269 } 1270 1271 // Extract snippet around match 1272 const start = Math.max(0, matchPos - 50); 1273 const end = Math.min(text.length, matchPos + maxLength - 50); 1274 let snippet = text.substring(start, end); 1275 1276 if (start > 0) snippet = '...' + snippet; 1277 if (end < text.length) snippet = snippet + '...'; 1278 1279 return this.highlightMatch(snippet, query); 1280 } 1281 1282 /** 1283 * Navigate through results with arrow keys 1284 */ 1285 navigateResults(direction) { 1286 if (this.results.length === 0) return; 1287 1288 this.currentResultIndex += direction; 1289 1290 if (this.currentResultIndex < 0) { 1291 this.currentResultIndex = this.results.length - 1; 1292 } else if (this.currentResultIndex >= this.results.length) { 1293 this.currentResultIndex = 0; 1294 } 1295 1296 this.setActiveResult(this.currentResultIndex); 1297 } 1298 1299 /** 1300 * Set active result 1301 */ 1302 setActiveResult(index) { 1303 const items = document.querySelectorAll('.search-result-item'); 1304 items.forEach((item, i) => { 1305 if (i === index) { 1306 item.classList.add('active'); 1307 item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); 1308 } else { 1309 item.classList.remove('active'); 1310 } 1311 }); 1312 this.currentResultIndex = index; 1313 } 1314 1315 /** 1316 * Select current result 1317 */ 1318 selectResult() { 1319 const activeItem = document.querySelector('.search-result-item.active'); 1320 if (activeItem) { 1321 const query = document.getElementById('global-search-input').value; 1322 this.addToHistory(query); 1323 activeItem.click(); 1324 } 1325 } 1326 1327 /** 1328 * Open search 1329 */ 1330 openSearch() { 1331 const overlay = document.getElementById('search-overlay'); 1332 const input = document.getElementById('global-search-input'); 1333 1334 overlay.classList.add('active'); 1335 document.body.style.overflow = 'hidden'; 1336 1337 // Focus input after animation 1338 setTimeout(() => { 1339 input.focus(); 1340 // Show search history 1341 this.handleSearch(''); 1342 }, 100); 1343 1344 // Announce to screen readers 1345 if (window.ARIAHelpers) { 1346 ARIAHelpers.announce('Search opened'); 1347 } 1348 } 1349 1350 /** 1351 * Close search 1352 */ 1353 closeSearch() { 1354 const overlay = document.getElementById('search-overlay'); 1355 const input = document.getElementById('global-search-input'); 1356 const clearBtn = document.querySelector('.search-clear'); 1357 const resultCount = document.getElementById('search-result-count'); 1358 1359 overlay.classList.remove('active'); 1360 document.body.style.overflow = ''; 1361 input.value = ''; 1362 clearBtn.style.display = 'none'; 1363 document.getElementById('search-results').innerHTML = ''; 1364 resultCount.style.display = 'none'; 1365 1366 // Reset filters 1367 this.filters.type = 'all'; 1368 document.querySelectorAll('.filter-btn').forEach(btn => { 1369 btn.classList.toggle('active', btn.getAttribute('data-filter-type') === 'all'); 1370 }); 1371 1372 // Announce to screen readers 1373 if (window.ARIAHelpers) { 1374 ARIAHelpers.announce('Search closed'); 1375 } 1376 } 1377 1378 /**
1379 * Render empty search state 1380 */ 1381 renderSearchHistory() { 1382 return ` 1383 <div class="search-empty"> 1384 <i class="fas fa-search" aria-hidden="true"></i> 1385 <p>Search results will appear here</p> 1386 </div> 1387 `; 1388 } 1389 1390 /** 1391 * Add query to search history (disabled) 1392 */ 1393 addToHistory(query) { 1394 // Search history functionality disabled 1395 return; 1396 } 1397 1398 /** 1399 * Highlight matching text 1400 */ 1401 highlightMatch(text, query) { 1402 if (!query) return this.escapeHtml(text); 1403 1404 const words = query.toLowerCase().split(/\s+/); 1405 let highlighted = this.escapeHtml(text); 1406 1407 words.forEach(word => { 1408 const regex = new RegExp(`(${this.escapeRegex(word)})`, 'gi'); 1409 highlighted = highlighted.replace(regex, '<mark>$1</mark>'); 1410 }); 1411 1412 return highlighted; 1413 } 1414 1415 /** 1416 * Escape HTML 1417 */ 1418 escapeHtml(text) { 1419 const div = document.createElement('div'); 1420 div.textContent = text; 1421 return div.innerHTML; 1422 } 1423 1424 /** 1425 * Escape regex special characters 1426 */ 1427 escapeRegex(str) { 1428 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 1429 } 1430 1431 /** 1432 * Get icon for content type 1433 */ 1434 getIcon(type) { 1435 const icons = { 1436 'article': 'newspaper', 1437 'gallery': 'images', 1438 'publication': 'atom', 1439 'faq': 'question-circle' 1440 }; 1441 return icons[type] || 'file'; 1442 } 1443 1444 /** 1445 * Get display label for content type 1446 */ 1447 getTypeLabel(type) { 1448 const labels = { 1449 'article': 'Article', 1450 'gallery': 'Gallery', 1451 'publication': 'Research Paper', 1452 'faq': 'FAQ' 1453 }; 1454 return labels[type] || type; 1455 } 1456} 1457 1458// Initialize on page load 1459if (document.readyState === 'loading') { 1460 document.addEventListener('DOMContentLoaded', () => { 1461 window.GlobalSearch = new EnhancedGlobalSearch(); 1462 }); 1463} else { 1464 window.GlobalSearch = new EnhancedGlobalSearch(); 1465}
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.