1/** 2 * AI Archive Search - Frontend JavaScript 3 * Version 1.1.0 - Added condition cards for 'conditions' custom post type 4 */ 5 6(function() { 7 'use strict'; 8 9 // State 10 const state = { 11 selectedCategories: [], 12 conversationHistory: [], 13 currentSearchId: null, 14 remainingFollowUps: 0, 15 isSearching: false, 16 categories: [], 17 filtersOpen: false, 18 isCompact: false 19 }; 20 21 // Storage key for persisting search state 22 const STORAGE_KEY = 'aias_search_state'; 23 24 // DOM Elements 25 let elements = {}; 26 27 // Initialize 28 document.addEventListener('DOMContentLoaded', init); 29 30 function init() { 31 const container = document.querySelector('.aias-search'); 32 if (!container) return; 33 34 // Detect compact mode and load settings 35 state.isCompact = container.getAttribute('data-mode') === 'compact'; 36 state.compactSettings = { showThumbnails: false, showExcerpts: false, showConditions: false }; 37 if (state.isCompact) { 38 var compactData = document.getElementById('aias-compact-settings'); 39 if (compactData) { 40 try { state.compactSettings = JSON.parse(compactData.textContent); } catch (e) {} 41 } 42 } 43 44 // Cache elements - all with null safety 45 elements.container = container; 46 elements.queryInput = document.getElementById('aias-query'); 47 elements.submitBtn = document.getElementById('aias-submit'); 48 elements.categorySearch = document.getElementById('aias-category-search'); 49 elements.categoryDropdown = document.getElementById('aias-category-dropdown'); 50 elements.selectedCategories = document.getElementById('aias-selected-categories'); 51 elements.dateFrom = document.getElementById('aias-date-from'); 52 elements.dateTo = document.getElementById('aias-date-to'); 53 elements.results = document.getElementById('aias-results'); 54 elements.conversation = document.getElementById('aias-conversation'); 55 elements.newSearchBtn = document.getElementById('aias-new-search'); 56 elements.loading = document.getElementById('aias-loading'); 57 elements.error = document.getElementById('aias-error'); 58 elements.popular = document.getElementById('aias-popular'); 59 elements.filtersToggle = document.getElementById('aias-filters-toggle'); 60 elements.filtersPanel = document.getElementById('aias-filters-panel'); 61 elements.filtersCount = document.getElementById('aias-filters-count'); 62 elements.followupInfo = document.getElementById('aias-followup-info'); 63 elements.followupRemaining = document.getElementById('aias-followup-remaining'); 64 65 // Load categories 66 const categoriesData = document.getElementById('aias-categories-data'); 67 if (categoriesData) { 68 try { 69 state.categories = JSON.parse(categoriesData.textContent); 70 } catch (e) { 71 state.categories = []; 72 } 73 } 74 75 // Bind events 76 bindEvents(); 77 78 // Check for query from URL param (e.g., from compact mode "Full AI Search" link) 79 var urlParams = new URLSearchParams(window.location.search); 80 var urlQuery = urlParams.get('aias_query'); 81 if (urlQuery && !state.isCompact) { 82 // Auto-trigger search with the passed query 83 if (elements.queryInput) { 84 elements.queryInput.value = urlQuery; 85 } 86 // Small delay to let everything initialize 87 setTimeout(function() { 88 performSearch(urlQuery); 89 }, 200); 90 return; // Skip restoring previous state 91 } 92 93 // Restore previous search state if available 94 restoreSearchState(); 95 } 96 97 function bindEvents() { 98 // Search submit 99 if (elements.submitBtn) { 100 elements.submitBtn.addEventListener('click', handleSearch); 101 } 102 103 // Enter key on search input 104 if (elements.queryInput) { 105 elements.queryInput.addEventListener('keypress', function(e) { 106 if (e.key === 'Enter') { 107 handleSearch(); 108 } 109 }); 110 } 111 112 // Filters toggle 113 if (elements.filtersToggle) { 114 elements.filtersToggle.addEventListener('click', function(e) { 115 e.preventDefault(); 116 e.stopPropagation(); 117 toggleFilters(); 118 }); 119 } 120 121 // Category search 122 if (elements.categorySearch) { 123 elements.categorySearch.addEventListener('input', handleCategorySearch); 124 elements.categorySearch.addEventListener('focus', handleCategorySearch); 125 } 126 127 // Close dropdown on outside click 128 document.addEventListener('click', function(e) { 129 if (elements.categoryDropdown && !e.target.closest('.aias-search__categories')) { 130 elements.categoryDropdown.style.display = 'none'; 131 } 132 }); 133 134 // Popular searches 135 document.querySelectorAll('.aias-search__popular-item').forEach(function(item) { 136 item.addEventListener('click', function() { 137 const prompt = this.getAttribute('data-prompt'); 138 if (elements.queryInput) { 139 elements.queryInput.value = prompt; 140 } 141 handleSearch(); 142 }); 143 }); 144 145 // New search 146 if (elements.newSearchBtn) { 147 elements.newSearchBtn.addEventListener('click', resetSearch); 148 } 149 150 // Date filter changes 151 if (elements.dateFrom) { 152 elements.dateFrom.addEventListener('change', updateFiltersCount); 153 } 154 if (elements.dateTo) { 155 elements.dateTo.addEventListener('change', updateFiltersCount); 156 } 157 } 158 159 function toggleFilters() { 160 state.filtersOpen = !state.filtersOpen; 161 162 if (elements.filtersPanel) { 163 elements.filtersPanel.style.display = state.filtersOpen ? 'flex' : 'none'; 164 } 165 166 if (elements.filtersToggle) { 167 if (state.filtersOpen) { 168 elements.filtersToggle.classList.add('aias-search__filters-toggle--active'); 169 } else { 170 elements.filtersToggle.classList.remove('aias-search__filters-toggle--active'); 171 } 172 } 173 } 174 175 function updateFiltersCount() { 176 let count = state.selectedCategories.length; 177 178 if (elements.dateFrom && elements.dateFrom.value) count++; 179 if (elements.dateTo && elements.dateTo.value) count++; 180 181 if (elements.filtersCount) { 182 if (count > 0) { 183 elements.filtersCount.textContent = count; 184 elements.filtersCount.style.display = 'inline'; 185 } else { 186 elements.filtersCount.style.display = 'none'; 187 } 188 } 189 } 190 191 /**
192 * Save search state to localStorage (shared across tabs) 193 * Expires after 30 minutes 194 */ 195 function saveSearchState() { 196 try { 197 const conversationHtml = elements.conversation ? elements.conversation.innerHTML : ''; 198 localStorage.setItem(STORAGE_KEY, JSON.stringify({ 199 conversationHistory: state.conversationHistory, 200 currentSearchId: state.currentSearchId, 201 remainingFollowUps: state.remainingFollowUps, 202 selectedCategories: state.selectedCategories, 203 dateFrom: elements.dateFrom ? elements.dateFrom.value : '', 204 dateTo: elements.dateTo ? elements.dateTo.value : '', 205 conversationHtml: conversationHtml, 206 timestamp: Date.now() 207 })); 208 console.log('[AIAS Search] Search state saved'); 209 } catch (e) { 210 console.error('[AIAS Search] Failed to save search state:', e); 211 } 212 } 213 214 /** 215 * Restore search state from localStorage 216 * Only restores if less than 30 minutes old 217 */ 218 function restoreSearchState() { 219 try { 220 const saved = localStorage.getItem(STORAGE_KEY); 221 console.log('[AIAS Search] Checking for saved state:', saved ? 'Found' : 'None'); 222 if (saved) { 223 const data = JSON.parse(saved); 224 225 // Check if expired (30 minutes = 1800000 ms) 226 const maxAge = 30 * 60 * 1000; 227 if (Date.now() - data.timestamp > maxAge) { 228 console.log('[AIAS Search] Saved state expired, clearing'); 229 localStorage.removeItem(STORAGE_KEY); 230 return false; 231 } 232 233 // Restore state 234 state.conversationHistory = data.conversationHistory || []; 235 state.currentSearchId = data.currentSearchId; 236 state.remainingFollowUps = data.remainingFollowUps || 0; 237 state.selectedCategories = data.selectedCategories || []; 238 239 // Restore date filters 240 if (elements.dateFrom && data.dateFrom) { 241 elements.dateFrom.value = data.dateFrom; 242 } 243 if (elements.dateTo && data.dateTo) { 244 elements.dateTo.value = data.dateTo; 245 } 246 247 // Restore selected categories UI 248 renderSelectedCategories(); 249 updateFiltersCount(); 250 251 // Restore conversation HTML 252 if (elements.conversation && data.conversationHtml) { 253 elements.conversation.innerHTML = data.conversationHtml; 254 bindPostClicks(); 255 } 256 257 // Show results panel if there was a conversation 258 if (state.conversationHistory.length > 0) { 259 if (elements.results) { 260 elements.results.style.display = 'block'; 261 } 262 if (elements.popular) { 263 elements.popular.style.display = 'none'; 264 } 265 if (elements.queryInput) { 266 elements.queryInput.placeholder = 'Ask a follow-up question...'; 267 } 268 updateFollowUpInfo(); 269 } 270 271 console.log('[AIAS Search] Restored search state'); 272 return true; 273 } 274 } catch (e) { 275 console.error('[AIAS Search] Failed to restore search state:', e); 276 } 277 return false; 278 } 279 280 /** 281 * Clear saved search state 282 */ 283 function clearSearchState() { 284 try { 285 localStorage.removeItem(STORAGE_KEY); 286 console.log('[AIAS Search] Search state cleared'); 287 } catch (e) { 288 // Ignore errors 289 } 290 } 291 292 function handleCategorySearch() { 293 if (!elements.categoryDropdown) return; 294 295 const query = elements.categorySearch ? elements.categorySearch.value.toLowerCase() : ''; 296 297 const filtered = state.categories.filter(function(cat) { 298 const matchesQuery = !query || cat.name.toLowerCase().includes(query); 299 const notSelected = !state.selectedCategories.find(function(s) { 300 return s.id === cat.id; 301 }); 302 return matchesQuery && notSelected; 303 }).slice(0, 10); 304 305 if (filtered.length === 0) { 306 elements.categoryDropdown.innerHTML = '<div class="aias-search__category-empty">No categories found</div>'; 307 } else { 308 elements.categoryDropdown.innerHTML = filtered.map(function(cat) { 309 return '<div class="aias-search__category-option" data-id="' + cat.id + '" data-name="' + escapeHtml(cat.name) + '">' + 310 escapeHtml(cat.name) + 311 '</div>'; 312 }).join(''); 313 314 elements.categoryDropdown.querySelectorAll('.aias-search__category-option').forEach(function(option) { 315 option.addEventListener('click', function() { 316 addCategory(parseInt(this.getAttribute('data-id')), this.getAttribute('data-name')); 317 }); 318 }); 319 } 320 321 elements.categoryDropdown.style.display = 'block'; 322 } 323 324 function addCategory(id, name) { 325 if (state.selectedCategories.find(function(c) { return c.id === id; })) { 326 return; 327 } 328 329 state.selectedCategories.push({ id: id, name: name }); 330 renderSelectedCategories(); 331 332 if (elements.categorySearch) { 333 elements.categorySearch.value = ''; 334 } 335 if (elements.categoryDropdown) { 336 elements.categoryDropdown.style.display = 'none'; 337 } 338 updateFiltersCount(); 339 } 340 341 function removeCategory(id) { 342 state.selectedCategories = state.selectedCategories.filter(function(c) { 343 return c.id !== id; 344 }); 345 renderSelectedCategories(); 346 updateFiltersCount(); 347 } 348
349 function renderSelectedCategories() { 350 if (!elements.selectedCategories) return; 351 352 elements.selectedCategories.innerHTML = state.selectedCategories.map(function(cat) { 353 return '<span class="aias-search__category-tag">' + 354 escapeHtml(cat.name) + 355 '<button type="button" class="aias-search__category-remove" data-id="' + cat.id + '">×</button>' + 356 '</span>'; 357 }).join(''); 358 359 elements.selectedCategories.querySelectorAll('.aias-search__category-remove').forEach(function(btn) { 360 btn.addEventListener('click', function() { 361 removeCategory(parseInt(this.getAttribute('data-id'))); 362 }); 363 }); 364 365 if (elements.categorySearch) { 366 elements.categorySearch.placeholder = state.selectedCategories.length > 0 367 ? 'Add more...' 368 : 'All categories (type to filter)'; 369 } 370 } 371 372 function handleSearch() { 373 const query = elements.queryInput ? elements.queryInput.value.trim() : ''; 374 375 if (!query) { 376 showError('Please enter a search query'); 377 return; 378 } 379 380 const isFollowUp = state.conversationHistory.length > 0 && state.remainingFollowUps > 0; 381 382 if (!isFollowUp) { 383 state.conversationHistory = []; 384 state.currentSearchId = null; 385 } 386 387 performSearch(query); 388 } 389 390 function performSearch(query) { 391 if (state.isSearching) return; 392 393 state.isSearching = true; 394 showLoading(); 395 hideError(); 396 397 if (elements.popular) { 398 elements.popular.style.display = 'none'; 399 } 400 401 const maxResults = elements.container ? parseInt(elements.container.getAttribute('data-max-results')) || 0 : 0; 402 403 const data = { 404 query: query, 405 categories: state.selectedCategories.map(function(c) { return c.id; }), 406 date_from: elements.dateFrom ? elements.dateFrom.value : '', 407 date_to: elements.dateTo ? elements.dateTo.value : '', 408 conversation_history: state.conversationHistory, 409 compact: state.isCompact 410 }; 411 412 if (maxResults > 0) { 413 data.max_results = maxResults; 414 } 415 416 // In compact mode, hide conditions unless admin enabled them 417 if (state.isCompact && !state.compactSettings.showConditions) { 418 data.max_condition_results = 0; 419 } 420 421 fetch(aiasConfig.restUrl + 'search', { 422 method: 'POST', 423 headers: { 424 'Content-Type': 'application/json', 425 'X-WP-Nonce': aiasConfig.nonce 426 }, 427 body: JSON.stringify(data) 428 }) 429 .then(function(response) { 430 if (response.status === 429) { 431 throw new Error(aiasConfig.strings.rateLimited); 432 } 433 return response.json(); 434 }) 435 .then(function(result) { 436 state.isSearching = false; 437 hideLoading(); 438 439 if (!result.success && result.message) { 440 showError(result.message); 441 return; 442 } 443 444 state.currentSearchId = result.search_id; 445 state.remainingFollowUps = result.remaining_follow_ups; 446 447 state.conversationHistory.push({ role: 'user', content: query }); 448 state.conversationHistory.push({ role: 'assistant', content: result.ai_response }); 449 450 renderConversation(query, result, result.no_results || false); 451 452 // Save search state for persistence across page navigations 453 saveSearchState(); 454 455 if (elements.queryInput) { 456 elements.queryInput.value = ''; 457 elements.queryInput.placeholder = 'Ask a follow-up question...'; 458 } 459 }) 460 .catch(function(error) { 461 state.isSearching = false; 462 hideLoading(); 463 showError(error.message || aiasConfig.strings.error); 464 }); 465 } 466 467 function renderConversation(query, result, noResults) { 468 if (!elements.conversation) return; 469 470 // Build compact "Full Search" card if needed 471 var fullSearchCard = ''; 472 if (state.isCompact) { 473 var lastQuery = state.conversationHistory.length >= 2 ? state.conversationHistory[state.conversationHistory.length - 2].content : query; 474 var fullSearchUrl = (aiasConfig.fullSearchUrl || '/ai-search/') + '?aias_query=' + encodeURIComponent(lastQuery); 475 var linkTitle = aiasConfig.fullSearchTitle || 'Full AI Search Experience'; 476 var linkSub = aiasConfig.fullSearchSubtitle || 'More searches, more results'; 477 fullSearchCard = '<a href="' + fullSearchUrl + '" class="aias-search__full-link" target="_blank" rel="noopener">' + 478 '<span class="aias-search__full-link-title">' + escapeHtml(linkTitle) + '</span>' + 479 '<span class="aias-search__full-link-sub">' + escapeHtml(linkSub) + ' ›</span>' + 480 '</a>'; 481 } 482 483 let messageHtml = 484 '<div class="aias-search__message aias-search__message--user">' + 485 '<div class="aias-search__message-content">' + escapeHtml(query) + '</div>' + 486 '</div>' + 487 '<div class="aias-search__message aias-search__message--assistant">' + 488 '<div class="aias-search__message-content">' + formatResponse(result.ai_response) + '</div>' + 489 (result.posts && result.posts.length > 0 ? renderPosts(result.posts, noResults) : '') + 490 ((!state.isCompact || state.compactSettings.showConditions) && result.conditions && result.conditions.length > 0 ? renderConditions(result.conditions) : '') + 491 fullSearchCard + 492 '</div>'; 493 494 elements.conversation.innerHTML += messageHtml; 495 496 if (elements.results) { 497 elements.results.style.display = 'block'; 498 } 499 500 updateFollowUpInfo(); 501 502 setTimeout(function() { 503 if (elements.results) { 504 elements.results.scrollTop = elements.results.scrollHeight; 505 } 506 }, 100); 507 508 bindPostClicks(); 509 } 510 511 function renderPosts(posts, noResults) { 512 if (!posts || posts.length === 0) return ''; 513 514 var label = noResults ? 'Unrelated Posts' : 'Recommended Articles'; 515 let html = '<div class="aias-search__posts' + (noResults ? ' aias-search__posts--unrelated' : '') + '">' + 516 '<div class="aias-search__posts-label">' + label + '</div>'; 517
518 posts.forEach(function(post) { 519 // Decode HTML entities in categories before displaying 520 var categoryText = ''; 521 if (post.categories && post.categories.length > 0) { 522 categoryText = post.categories.slice(0, 2).map(function(cat) { 523 return decodeHtml(cat); 524 }).join(', '); 525 } 526 527 if (state.isCompact) { 528 // Compact: configurable thumbnails/excerpts via admin settings 529 var showThumb = state.compactSettings.showThumbnails && post.thumbnail; 530 var showExc = state.compactSettings.showExcerpts; 531 532 html += '<a href="' + escapeHtml(post.url) + '" target="_blank" rel="noopener" class="aias-search__post aias-search__post--compact" data-post-id="' + post.id + '">' + 533 (showThumb ? '<img src="' + escapeHtml(post.thumbnail) + '" alt="" class="aias-search__post-thumbnail">' : '') + 534 '<div class="aias-search__post-content">' + 535 '<div class="aias-search__post-title">' + escapeHtml(post.title) + '</div>' + 536 '<div class="aias-search__post-meta">' + 537 '<span class="aias-search__post-date">' + escapeHtml(post.date) + '</span>' + 538 (categoryText ? 539 '<span class="aias-search__post-categories">' + escapeHtml(categoryText) + '</span>' 540 : '') + 541 '</div>' + 542 (showExc ? '<div class="aias-search__post-excerpt">' + escapeHtml(post.excerpt) + '</div>' : '') + 543 '</div>' + 544 '</a>'; 545 } else { 546 html += '<a href="' + escapeHtml(post.url) + '" target="_blank" rel="noopener" class="aias-search__post" data-post-id="' + post.id + '">' + 547 (post.thumbnail ? '<img src="' + escapeHtml(post.thumbnail) + '" alt="" class="aias-search__post-thumbnail">' : '') + 548 '<div class="aias-search__post-content">' + 549 '<div class="aias-search__post-title">' + escapeHtml(post.title) + '</div>' + 550 '<div class="aias-search__post-meta">' + 551 '<span class="aias-search__post-date">' + escapeHtml(post.date) + '</span>' + 552 (categoryText ? 553 '<span class="aias-search__post-categories">' + escapeHtml(categoryText) + '</span>' 554 : '') + 555 '</div>' + 556 '<div class="aias-search__post-excerpt">' + escapeHtml(post.excerpt) + '</div>' + 557 '</div>' + 558 '</a>'; 559 } 560 }); 561 562 html += '</div>'; 563 return html; 564 } 565 566 function renderConditions(conditions) { 567 if (!conditions || conditions.length === 0) return ''; 568 569 let html = '<div class="aias-search__conditions">' + 570 '<div class="aias-search__conditions-divider">' + 571 '<span>Related Conditions</span>' + 572 '</div>' + 573 '<div class="aias-search__conditions-list">'; 574 575 conditions.forEach(function(condition) { 576 html += '<a href="' + escapeHtml(condition.url) + '" target="_blank" rel="noopener" class="aias-search__condition-card">' + 577 (condition.thumbnail ? '<img src="' + escapeHtml(condition.thumbnail) + '" alt="" class="aias-search__condition-thumbnail">' : '') + 578 '<div class="aias-search__condition-content">' + 579 '<div class="aias-search__condition-title">' + escapeHtml(condition.title) + '</div>' + 580 '<div class="aias-search__condition-summary">' + escapeHtml(condition.summary) + '</div>' + 581 '</div>' + 582 '</a>'; 583 }); 584 585 html += '</div></div>'; 586 return html; 587 } 588 589 function bindPostClicks() { 590 document.querySelectorAll('.aias-search__post').forEach(function(postEl) { 591 if (!postEl.dataset.clickBound) { 592 postEl.dataset.clickBound = 'true'; 593 postEl.addEventListener('click', function() { 594 trackClick(this.getAttribute('data-post-id')); 595 }); 596 } 597 }); 598 } 599 600 function trackClick(postId) { 601 if (!state.currentSearchId) return; 602
603 fetch(aiasConfig.restUrl + 'track-click', { 604 method: 'POST', 605 headers: { 606 'Content-Type': 'application/json', 607 'X-WP-Nonce': aiasConfig.nonce 608 }, 609 body: JSON.stringify({ 610 search_id: state.currentSearchId, 611 post_id: parseInt(postId) 612 }) 613 }).catch(function() {}); 614 } 615 616 function updateFollowUpInfo() { 617 if (!elements.followupInfo || !elements.followupRemaining) return; 618 619 if (state.remainingFollowUps > 0) { 620 elements.followupInfo.style.display = 'block'; 621 elements.followupRemaining.textContent = state.remainingFollowUps + ' follow-up' + (state.remainingFollowUps !== 1 ? 's' : '') + ' remaining'; 622 } else { 623 elements.followupInfo.style.display = 'block'; 624 elements.followupRemaining.textContent = 'No follow-ups remaining. Start a new search.'; 625 } 626 } 627 628 function resetSearch() { 629 state.conversationHistory = []; 630 state.currentSearchId = null; 631 state.remainingFollowUps = 0; 632 633 // Clear saved search state 634 clearSearchState(); 635 636 if (elements.conversation) { 637 elements.conversation.innerHTML = ''; 638 } 639 640 if (elements.results) { 641 elements.results.style.display = 'none'; 642 } 643 644 if (elements.popular) { 645 elements.popular.style.display = 'flex'; 646 } 647 648 if (elements.followupInfo) { 649 elements.followupInfo.style.display = 'none'; 650 } 651 652 if (elements.queryInput) { 653 elements.queryInput.value = ''; 654 elements.queryInput.placeholder = 'Search our archive...'; 655 elements.queryInput.focus(); 656 } 657 658 hideError(); 659 } 660 661 function formatResponse(text) { 662 if (!text) return ''; 663 const paragraphs = text.split(/\n\n+/); 664 return paragraphs.map(function(p) { 665 return '<p>' + escapeHtml(p).replace(/\n/g, '<br>') + '</p>'; 666 }).join(''); 667 } 668 669 function showLoading() { 670 if (elements.loading) { 671 elements.loading.style.display = 'flex'; 672 } 673 if (elements.popular) { 674 elements.popular.style.display = 'none'; 675 } 676 } 677 678 function hideLoading() { 679 if (elements.loading) { 680 elements.loading.style.display = 'none'; 681 } 682 } 683 684 function showError(message) { 685 if (elements.error) { 686 elements.error.textContent = message; 687 elements.error.style.display = 'block'; 688 } 689 if (elements.popular && state.conversationHistory.length === 0) { 690 elements.popular.style.display = 'flex'; 691 } 692 } 693 694 function hideError() { 695 if (elements.error) { 696 elements.error.style.display = 'none'; 697 } 698 } 699 700 function escapeHtml(text) { 701 if (!text) return ''; 702 const div = document.createElement('div'); 703 div.textContent = text; 704 return div.innerHTML; 705 } 706 707 function decodeHtml(text) { 708 if (!text) return ''; 709 const div = document.createElement('div'); 710 div.innerHTML = text; 711 return div.textContent || div.innerText || ''; 712 } 713 714})();
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.