PageSourceSearch

https://blackdoctor.com/wp-content/plugins/ai-archive-search/public/js/search.js?ver=1.4.0

js blackdoctor.com collected 2026-10-02 18:19:01 UTC 27,218 bytes, 714 lines download raw bytes

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 + '">&times;</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) + ' &rsaquo;</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.