search working but too small

This commit is contained in:
2026-05-05 09:41:32 -04:00
parent 6359aefd18
commit 9042468fe8
12 changed files with 696 additions and 3 deletions

View File

@@ -1,5 +1,4 @@
// Shared LLM module - provides streaming chat to any page
var LLM = (function() {
var Search = (function() {
var defaultApiUrl = 'https://llama-instruct.reeselink.com/v1';
var defaultModel = 'instruct';

479
lib/search.js Normal file
View 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
};
})();