search working but too small
This commit is contained in:
479
lib/search.js
Normal file
479
lib/search.js
Normal file
@@ -0,0 +1,479 @@
|
||||
var Search = (function() {
|
||||
var dropdownVisible = false;
|
||||
var selectedIndex = -1;
|
||||
var results = [];
|
||||
var debounceTimer = null;
|
||||
var currentInput = null;
|
||||
|
||||
// Page title mapping
|
||||
var PAGE_TITLES = {
|
||||
'index.html': 'Home',
|
||||
'terminology.html': 'Terminology',
|
||||
'techniques.html': 'Techniques',
|
||||
'use-cases.html': 'Use Cases',
|
||||
'model-types.html': 'Model Types',
|
||||
'prompts.html': 'Prompt Guide',
|
||||
'math.html': 'Math & Concepts',
|
||||
'chat.html': 'Chat',
|
||||
'image-gen.html': 'Image Gen'
|
||||
};
|
||||
|
||||
// Content to skip during crawling
|
||||
var SKIP_CONTENT = [
|
||||
'AI Cheat Sheet',
|
||||
'Toggle dark mode',
|
||||
'Toggle menu',
|
||||
'Send',
|
||||
'Ask me anything about AI',
|
||||
'Ask a Question',
|
||||
'Ask anything about AI',
|
||||
'Powered by your configured LLM',
|
||||
'Try AI right now',
|
||||
'Full Chat',
|
||||
'Generate',
|
||||
'Clear',
|
||||
'Image Generation',
|
||||
'Enter image prompt'
|
||||
];
|
||||
|
||||
function init() {
|
||||
var navInner = document.querySelector('.nav-inner');
|
||||
if (!navInner) return;
|
||||
|
||||
var brand = navInner.querySelector('.nav-brand');
|
||||
if (!brand) return;
|
||||
|
||||
// Create search wrapper
|
||||
var searchWrapper = document.createElement('div');
|
||||
searchWrapper.className = 'sidebar-search';
|
||||
searchWrapper.style.cssText = 'position:relative; margin-top:0.5rem; margin-bottom:0.25rem;';
|
||||
|
||||
var searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.className = 'sidebar-search-input';
|
||||
searchInput.placeholder = 'Search...';
|
||||
searchInput.setAttribute('aria-label', 'Search all pages');
|
||||
searchInput.autocomplete = 'off';
|
||||
|
||||
searchWrapper.appendChild(searchInput);
|
||||
|
||||
// Insert after brand
|
||||
brand.parentNode.insertBefore(searchWrapper, brand.nextSibling);
|
||||
|
||||
// Move results container inside search wrapper for correct absolute positioning
|
||||
var existingContainer = document.getElementById('searchResultsContainer');
|
||||
if (existingContainer) {
|
||||
searchWrapper.appendChild(existingContainer);
|
||||
}
|
||||
|
||||
currentInput = searchInput;
|
||||
|
||||
// Event listeners
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
clearTimeout(debounceTimer);
|
||||
var query = e.target.value.trim();
|
||||
if (query.length === 0) {
|
||||
hideDropdown();
|
||||
return;
|
||||
}
|
||||
debounceTimer = setTimeout(function() {
|
||||
performSearch(query);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
if (!dropdownVisible || results.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
|
||||
updateSelection();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
updateSelection();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex >= 0 && selectedIndex < results.length) {
|
||||
navigateToResult(results[selectedIndex]);
|
||||
} else if (results.length > 0) {
|
||||
navigateToResult(results[0]);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
hideDropdown();
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('focus', function() {
|
||||
if (results.length > 0 && currentInput.value.trim().length > 0) {
|
||||
showDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchWrapper.contains(e.target)) {
|
||||
hideDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function crawlPage(url, callback) {
|
||||
fetch(url)
|
||||
.then(function(response) { return response.text(); })
|
||||
.then(function(html) {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(html, 'text/html');
|
||||
var results = [];
|
||||
|
||||
// Get page title
|
||||
var pageTitle = PAGE_TITLES[doc.title.replace(' - Cheat Sheet', '').replace('AI Cheat Sheet', '').trim()] || doc.title;
|
||||
|
||||
// Extract def-cards
|
||||
var defCards = doc.querySelectorAll('.def-card');
|
||||
defCards.forEach(function(card) {
|
||||
var heading = card.querySelector('h3');
|
||||
var p = card.querySelector('p');
|
||||
var example = card.querySelector('.example');
|
||||
var category = card.querySelector('.category');
|
||||
|
||||
if (!heading || !p) return;
|
||||
|
||||
var headingText = heading.textContent.trim();
|
||||
var defText = p.textContent.trim();
|
||||
var catText = category ? category.textContent.trim() : '';
|
||||
var exText = example ? example.textContent.trim() : '';
|
||||
|
||||
var fullText = (catText + ' ' + headingText + ' ' + defText + ' ' + exText).trim();
|
||||
if (fullText.length < 10) return;
|
||||
|
||||
// Skip boilerplate content
|
||||
var skip = false;
|
||||
for (var i = 0; i < SKIP_CONTENT.length; i++) {
|
||||
if (fullText.indexOf(SKIP_CONTENT[i]) === 0) { skip = true; break; }
|
||||
}
|
||||
if (skip) return;
|
||||
|
||||
results.push({
|
||||
page: pageTitle,
|
||||
url: url,
|
||||
heading: headingText,
|
||||
category: catText,
|
||||
snippet: defText,
|
||||
fullText: fullText.toLowerCase()
|
||||
});
|
||||
});
|
||||
|
||||
// Extract table rows
|
||||
var tables = doc.querySelectorAll('.glossary-table tbody tr');
|
||||
tables.forEach(function(row) {
|
||||
var tds = row.querySelectorAll('td');
|
||||
if (tds.length < 2) return;
|
||||
|
||||
var acronym = tds[0].textContent.trim();
|
||||
var meaning = tds[1].textContent.trim();
|
||||
|
||||
results.push({
|
||||
page: pageTitle,
|
||||
url: url,
|
||||
heading: acronym,
|
||||
category: 'Acronym',
|
||||
snippet: meaning,
|
||||
fullText: (acronym + ' ' + meaning).toLowerCase()
|
||||
});
|
||||
});
|
||||
|
||||
// Extract section headings for pages without def-cards (like use-cases, model-types)
|
||||
var sections = doc.querySelectorAll('h2.section-title');
|
||||
sections.forEach(function(h2) {
|
||||
var sectionName = h2.textContent.trim();
|
||||
// Check if there are cards under this section
|
||||
var next = h2.nextElementSibling;
|
||||
var cards = [];
|
||||
while (next && !next.classList.contains('section-title')) {
|
||||
if (next.classList && (next.classList.contains('card') || next.classList.contains('use-card') || next.classList.contains('prompt-block') || next.classList.contains('def-card'))) {
|
||||
cards.push(next);
|
||||
}
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
|
||||
cards.forEach(function(card) {
|
||||
var h3 = card.querySelector('h3');
|
||||
var p = card.querySelector('p');
|
||||
if (!h3 || !p) return;
|
||||
|
||||
var headingText = h3.textContent.trim();
|
||||
var cardText = p.textContent.trim();
|
||||
|
||||
// Skip if already captured as def-card
|
||||
if (card.classList.contains('def-card')) return;
|
||||
|
||||
results.push({
|
||||
page: pageTitle,
|
||||
url: url,
|
||||
heading: headingText,
|
||||
category: sectionName,
|
||||
snippet: cardText,
|
||||
fullText: (sectionName + ' ' + headingText + ' ' + cardText).toLowerCase()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// For pages with .use-grid, extract use-cards
|
||||
var useGrid = doc.querySelector('.use-grid');
|
||||
if (useGrid) {
|
||||
var useCards = useGrid.querySelectorAll('.use-card');
|
||||
useCards.forEach(function(card) {
|
||||
var h3 = card.querySelector('h3');
|
||||
var p = card.querySelector('p');
|
||||
if (!h3 || !p) return;
|
||||
var headingText = h3.textContent.trim();
|
||||
var cardText = p.textContent.trim();
|
||||
|
||||
results.push({
|
||||
page: pageTitle,
|
||||
url: url,
|
||||
heading: headingText,
|
||||
category: 'Use Case',
|
||||
snippet: cardText,
|
||||
fullText: (headingText + ' ' + cardText).toLowerCase()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// For prompt guide, extract prompt-blocks
|
||||
var promptBlocks = doc.querySelectorAll('.prompt-block');
|
||||
if (promptBlocks.length > 0) {
|
||||
promptBlocks.forEach(function(block) {
|
||||
var h3 = block.querySelector('h3');
|
||||
var label = block.querySelector('.label');
|
||||
var p = block.querySelector('p');
|
||||
|
||||
if (!h3) return;
|
||||
var headingText = h3.textContent.trim();
|
||||
var labelText = label ? label.textContent.trim() : '';
|
||||
var cardText = p ? p.textContent.trim() : '';
|
||||
|
||||
results.push({
|
||||
page: pageTitle,
|
||||
url: url,
|
||||
heading: headingText,
|
||||
category: labelText || 'Prompt',
|
||||
snippet: cardText,
|
||||
fullText: (headingText + ' ' + labelText + ' ' + cardText).toLowerCase()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// For math page, extract def-cards (already handled above)
|
||||
// For model-types, also grab comparison table
|
||||
var modelTables = doc.querySelectorAll('.glossary-table');
|
||||
if (modelTables.length > 0) {
|
||||
modelTables.forEach(function(table) {
|
||||
var rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(function(row) {
|
||||
var tds = row.querySelectorAll('td');
|
||||
if (tds.length === 0) return;
|
||||
var rowText = '';
|
||||
for (var i = 0; i < tds.length; i++) {
|
||||
rowText += ' ' + tds[i].textContent.trim();
|
||||
}
|
||||
if (rowText.length < 5) return;
|
||||
|
||||
results.push({
|
||||
page: pageTitle,
|
||||
url: url,
|
||||
heading: tds[0] ? tds[0].textContent.trim() : 'Table Entry',
|
||||
category: 'Comparison',
|
||||
snippet: rowText.trim(),
|
||||
fullText: rowText.toLowerCase()
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
callback(results);
|
||||
})
|
||||
.catch(function() {
|
||||
callback([]);
|
||||
});
|
||||
}
|
||||
|
||||
function crawlAllPages(callback) {
|
||||
var pages = [
|
||||
{ url: '/index.html', title: 'Home' },
|
||||
{ url: '/pages/terminology.html', title: 'Terminology' },
|
||||
{ url: '/pages/techniques.html', title: 'Techniques' },
|
||||
{ url: '/pages/use-cases.html', title: 'Use Cases' },
|
||||
{ url: '/pages/model-types.html', title: 'Model Types' },
|
||||
{ url: '/pages/prompts.html', title: 'Prompt Guide' },
|
||||
{ url: '/pages/math.html', title: 'Math & Concepts' }
|
||||
];
|
||||
|
||||
var allResults = [];
|
||||
var completed = 0;
|
||||
|
||||
if (pages.length === 0) {
|
||||
callback(allResults);
|
||||
return;
|
||||
}
|
||||
|
||||
pages.forEach(function(page) {
|
||||
crawlPage(page.url, function(results) {
|
||||
allResults = allResults.concat(results);
|
||||
completed++;
|
||||
if (completed === pages.length) {
|
||||
callback(allResults);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function performSearch(query) {
|
||||
var q = query.toLowerCase().trim();
|
||||
if (q.length === 0) { hideDropdown(); return; }
|
||||
|
||||
crawlAllPages(function(allResults) {
|
||||
searchResults(allResults, q);
|
||||
});
|
||||
}
|
||||
|
||||
function searchResults(allResults, q) {
|
||||
var qWords = q.split(/\s+/).filter(function(w) { return w.length > 0; });
|
||||
var scored = [];
|
||||
|
||||
for (var i = 0; i < allResults.length; i++) {
|
||||
var item = allResults[i];
|
||||
var text = item.fullText || '';
|
||||
var score = 0;
|
||||
|
||||
if (text.indexOf(q) !== -1) score += 100;
|
||||
|
||||
for (var j = 0; j < qWords.length; j++) {
|
||||
if (text.indexOf(qWords[j]) !== -1) score += 10;
|
||||
if (item.heading && item.heading.toLowerCase().indexOf(qWords[j]) === 0) score += 20;
|
||||
if (item.category && item.category.toLowerCase().indexOf(qWords[j]) !== -1) score += 5;
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
scored.push({ item: item, score: score });
|
||||
}
|
||||
}
|
||||
|
||||
scored.sort(function(a, b) { return b.score - a.score; });
|
||||
var topResults = scored.slice(0, 10).map(function(s) { return s.item; });
|
||||
displayResults(topResults, q);
|
||||
}
|
||||
|
||||
function highlightText(text, query) {
|
||||
if (!text || !query) return escapeHTML(text);
|
||||
var escaped = escapeHTML(text);
|
||||
var qWords = query.toLowerCase().split(/\s+/).filter(function(w) { return w.length > 0; });
|
||||
|
||||
for (var i = 0; i < qWords.length; i++) {
|
||||
var word = qWords[i];
|
||||
var regex = new RegExp('(' + escapeRegex(word) + ')', 'gi');
|
||||
escaped = escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
|
||||
}
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function displayResults(foundResults, query) {
|
||||
results = foundResults;
|
||||
selectedIndex = -1;
|
||||
|
||||
if (results.length === 0) {
|
||||
showDropdown();
|
||||
var container = document.getElementById('searchResultsContainer');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="search-no-results">No results found for "' + escapeHTML(query) + '"</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var container = document.getElementById('searchResultsContainer');
|
||||
var html = '';
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var r = results[i];
|
||||
var categoryLabel = r.category ? '<span class="search-result-category">' + escapeHTML(r.category) + '</span>' : '';
|
||||
var highlightedSnippet = highlightText(r.snippet, query);
|
||||
|
||||
html += '<div class="search-result-item" data-index="' + i + '" data-url="' + escapeHTML(r.url) + '">';
|
||||
html += '<div class="search-result-title">';
|
||||
html += '<span class="search-result-page">' + escapeHTML(r.page) + '</span>';
|
||||
html += categoryLabel;
|
||||
html += '</div>';
|
||||
html += '<div class="search-result-heading">' + highlightText(r.heading, query) + '</div>';
|
||||
html += '<div class="search-result-snippet">' + highlightedSnippet + '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
showDropdown();
|
||||
|
||||
// Attach click handlers
|
||||
var items = container.querySelectorAll('.search-result-item');
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
items[j].addEventListener('click', function() {
|
||||
var url = this.getAttribute('data-url');
|
||||
navigateToResult({ url: url });
|
||||
});
|
||||
items[j].addEventListener('mouseenter', function() {
|
||||
var idx = parseInt(this.getAttribute('data-index'), 10);
|
||||
selectedIndex = idx;
|
||||
updateSelection();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelection() {
|
||||
var items = document.querySelectorAll('.search-result-item');
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (i === selectedIndex) {
|
||||
items[i].classList.add('active');
|
||||
items[i].scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
items[i].classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showDropdown() {
|
||||
var container = document.getElementById('searchResultsContainer');
|
||||
if (container) {
|
||||
container.style.display = 'block';
|
||||
dropdownVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
function hideDropdown() {
|
||||
var container = document.getElementById('searchResultsContainer');
|
||||
if (container) {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
dropdownVisible = false;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
|
||||
function navigateToResult(result) {
|
||||
hideDropdown();
|
||||
if (currentInput) currentInput.blur();
|
||||
window.location.href = result.url;
|
||||
}
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user