overview / js /utils /search.js
yjernite's picture
yjernite HF Staff
Upload 4 files
98f8ae8 verified
raw
history blame
13.5 kB
// search.js - Complete MiniSearch implementation with resources and scores
import { sampleResources } from '../data/resources.js';
import { areasData } from '../data/areas.js';
let miniSearch, miniSearchResources;
let allArtifacts; // no longer needs to be explicitly defined
// Helper to create and populate a MiniSearch instance
function createMiniSearchIndex(data, storeFields) {
const search = new MiniSearch({
fields: ['title', 'description', 'areas', 'topics'],
storeFields: storeFields
});
search.addAll(data);
return search;
}
// Initialize search
export async function initializeSearch(artifactsData) {
// Assign the passed artifactsData to allArtifacts
allArtifacts = artifactsData;
// The fetch for artifacts.json is removed from here since it's now loaded in main.js
// Prepare data for MiniSearch
const searchData = allArtifacts.map((artifact, index) => ({
id: index,
title: artifact.title,
description: artifact.description,
type: artifact.type,
areas: (artifact.areas || []).join(' '),
topics: (artifact.topics || []).join(' '),
url: artifact.url,
date: artifact.date
}));
// Prepare resources data
const resourcesData = sampleResources.map((resource, index) => ({
id: index,
title: resource.title,
description: resource.description,
type: resource.type,
areas: (resource.areaTags || []).join(' '),
topics: (resource.subAreaTags || []).join(' '),
url: resource.url,
date: resource.date
}));
// Initialize MiniSearch for artifacts
miniSearch = createMiniSearchIndex(searchData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']);
// Initialize MiniSearch for resources
miniSearchResources = createMiniSearchIndex(resourcesData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']);
}
export function searchContent(query) {
if (!query || query.trim().length < 2) {
return { artifacts: [], resources: [] };
}
const artifactResults = miniSearch.search(query, {
prefix: true,
fuzzy: 0.2,
boost: { title: 2, description: 1 }
});
const resourceResults = miniSearchResources.search(query, {
prefix: true,
fuzzy: 0.2,
boost: { title: 2, description: 1 }
});
return {
artifacts: artifactResults, // Show all results, not limited to 5
resources: resourceResults
};
}
// Helper functions
function getAreaDisplayName(area) {
return areasData[area]?.title || area;
}
function getSubAreaDisplayName(areaId, subArea) {
const area = areasData[areaId];
if (!area || !area.subAreas) return subArea;
const subAreaData = area.subAreas[subArea];
return typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || subArea;
}
// Search UI
export function initializeSearchUI(artifactsData) {
initializeSearch(artifactsData).then(() => {
console.log('Search initialized');
});
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
if (!searchInput || !searchResults) return;
let searchTimeout;
// Function to perform search
function performSearch() {
const query = searchInput.value.trim();
if (query.length < 2) {
searchResults.innerHTML = `<div class="text-gray-500 text-center py-8"><p>Enter a search term...</p></div>`;
return;
}
const results = searchContent(query);
displaySearchResults(results, query);
}
// Handle input events (typing)
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
searchResults.innerHTML = `<div class="text-gray-500 text-center py-8"><p>Enter a search term...</p></div>`;
return;
}
// Debounce search
searchTimeout = setTimeout(() => {
performSearch();
}, 300);
});
// Handle Enter key (immediate search)
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
clearTimeout(searchTimeout);
performSearch();
}
});
}
// Update the displaySearchResults function in search.js
export function displaySearchResults(results, query) {
const { artifacts, resources } = results;
const totalResults = artifacts.length + resources.length;
if (totalResults === 0) {
document.getElementById('search-results').innerHTML = `
<div class="text-gray-500 text-center py-8"><p>No results found for "${query}"</p></div>
`;
return;
}
let html = `<div class="text-sm text-gray-600 mb-4">
Found ${totalResults} results (${artifacts.length} writings, ${resources.length} resources)
</div>`;
// Display artifacts with collapsible header
if (artifacts.length > 0) {
html += `
<div class="mb-4">
<button onclick="toggleCategory('artifacts')" class="flex items-center justify-between w-full p-2 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors">
<h4 class="font-semibold text-gray-900">Writings (${artifacts.length})</h4>
<svg id="artifacts-arrow" class="w-4 h-4 text-gray-600 transform transition-transform" style="transform: rotate(0deg);">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="artifacts-content" class="space-y-2 mt-2">
`;
artifacts.forEach(result => {
const score = Math.round(result.score * 100);
// Create area links
const areaLinks = result.areas ? result.areas.split(' ').map(area => {
const areaData = areasData[area];
if (!areaData) return '';
const colorClass = areaData?.colors?.bg || 'bg-blue-100';
const textColorClass = areaData?.colors?.text || 'text-blue-800';
return `<a href="/${area}#overview" class="text-xs px-2 py-1 ${colorClass} ${textColorClass} rounded hover:opacity-80 transition-opacity">${areaData?.title || area}</a>`;
}).filter(link => link).join('') : '';
// Create sub-area links
const subAreaLinks = result.topics ? result.topics.split(' ').map(topic => {
const primaryArea = result.areas?.split(' ')[0] || 'efficiency';
const areaData = areasData[primaryArea];
if (!areaData || !areaData.subAreas) return '';
const subAreaData = areaData.subAreas[topic];
if (!subAreaData) return '';
const subAreaName = typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || topic;
const colorClass = subAreaData?.color || 'bg-gray-100 text-gray-800';
return `<a href="/${primaryArea}#${topic}" class="text-xs px-2 py-1 ${colorClass} rounded hover:opacity-80 transition-opacity">${subAreaName}</a>`;
}).filter(link => link).join('') : '';
html += `
<div class="p-3 bg-gray-50 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<h5 class="font-medium text-sm text-gray-900">${result.title}</h5>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">${score}%</span>
<a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
</a>
</div>
</div>
<div class="flex items-center gap-2 mb-2 text-xs text-gray-600">
<span class="px-2 py-1 bg-gray-200 text-gray-700 rounded">${result.type}</span>
<span>${result.date}</span>
</div>
<div class="flex flex-wrap gap-1">
${areaLinks}
${subAreaLinks}
</div>
</div>
`;
});
html += `</div></div>`;
}
// Display resources with collapsible header
if (resources.length > 0) {
html += `
<div class="mb-4">
<button onclick="toggleCategory('resources')" class="flex items-center justify-between w-full p-2 bg-green-50 hover:bg-green-100 rounded-lg transition-colors">
<h4 class="font-semibold text-gray-900">Resources (${resources.length})</h4>
<svg id="resources-arrow" class="w-4 h-4 text-gray-600 transform transition-transform" style="transform: rotate(0deg);">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="resources-content" class="space-y-2 mt-2">
`;
resources.forEach(result => {
const score = Math.round(result.score * 100);
// Create area links for resources
const areaLinks = result.areas ? result.areas.split(' ').map(area => {
const areaData = areasData[area];
if (!areaData) return '';
const colorClass = areaData?.colors?.bg || 'bg-green-100';
const textColorClass = areaData?.colors?.text || 'text-green-800';
return `<a href="/${area}#overview" class="text-xs px-2 py-1 ${colorClass} ${textColorClass} rounded hover:opacity-80 transition-opacity">${areaData?.title || area}</a>`;
}).filter(link => link).join('') : '';
// Create sub-area links for resources
const subAreaLinks = result.topics ? result.topics.split(' ').map(topic => {
const primaryArea = result.areas?.split(' ')[0] || 'efficiency';
const areaData = areasData[primaryArea];
if (!areaData || !areaData.subAreas) return '';
const subAreaData = areaData.subAreas[topic];
if (!subAreaData) return '';
const subAreaName = typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || topic;
const colorClass = subAreaData?.color || 'bg-gray-100 text-gray-800';
return `<a href="/${primaryArea}#${topic}" class="text-xs px-2 py-1 ${colorClass} rounded hover:opacity-80 transition-opacity">${subAreaName}</a>`;
}).filter(link => link).join('') : '';
html += `
<div class="p-3 bg-gray-50 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<h5 class="font-medium text-sm text-gray-900">${result.title}</h5>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">${score}%</span>
<a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
</a>
</div>
</div>
<div class="flex items-center gap-2 mb-2 text-xs text-gray-600">
<span class="px-2 py-1 bg-gray-200 text-gray-700 rounded">${result.type}</span>
<span>${result.date}</span>
</div>
<div class="flex flex-wrap gap-1">
${areaLinks}
${subAreaLinks}
</div>
</div>
`;
});
html += `</div></div>`;
}
document.getElementById('search-results').innerHTML = html;
}
// Add this function to handle category toggling
window.toggleCategory = function(category) {
const content = document.getElementById(`${category}-content`);
const arrow = document.getElementById(`${category}-arrow`);
if (content.style.display === 'none') {
content.style.display = 'block';
arrow.style.transform = 'rotate(0deg)';
} else {
content.style.display = 'none';
arrow.style.transform = 'rotate(-90deg)';
}
};