fix: make FuzzySearch generic to support typed items

- Add SearchableItem base interface for minimum required fields
- Make FuzzySearch class generic with type parameter
- Update all page scripts to use typed FuzzySearch instances
- Fix type casting in calculateScore method
This commit is contained in:
Aaron Powell
2026-01-29 09:56:20 +11:00
parent 63fb276a6c
commit d46210b2de
8 changed files with 27 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
{ {
"generated": "2026-01-28T22:44:20.356Z", "generated": "2026-01-28T22:56:09.687Z",
"counts": { "counts": {
"agents": 140, "agents": 140,
"prompts": 134, "prompts": 134,

View File

@@ -25,7 +25,7 @@ interface AgentsData {
const resourceType = 'agent'; const resourceType = 'agent';
let allItems: Agent[] = []; let allItems: Agent[] = [];
let search = new FuzzySearch(); let search = new FuzzySearch<Agent>();
let modelSelect: Choices; let modelSelect: Choices;
let toolSelect: Choices; let toolSelect: Choices;

View File

@@ -2,13 +2,14 @@
* Collections page functionality * Collections page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from '../choices'; import { createChoices, getChoicesValues, type Choices } from '../choices';
import { FuzzySearch, type SearchItem } from '../search'; import { FuzzySearch } from '../search';
import { fetchData, debounce, escapeHtml, getGitHubUrl } from '../utils'; import { fetchData, debounce, escapeHtml, getGitHubUrl } from '../utils';
import { setupModal, openFileModal } from '../modal'; import { setupModal, openFileModal } from '../modal';
interface Collection { interface Collection {
id: string; id: string;
name: string; name: string;
title: string;
description?: string; description?: string;
path: string; path: string;
tags?: string[]; tags?: string[];
@@ -25,7 +26,7 @@ interface CollectionsData {
const resourceType = 'collection'; const resourceType = 'collection';
let allItems: Collection[] = []; let allItems: Collection[] = [];
let search = new FuzzySearch(); let search = new FuzzySearch<Collection>();
let tagSelect: Choices; let tagSelect: Choices;
let currentFilters = { let currentFilters = {
tags: [] as string[], tags: [] as string[],

View File

@@ -48,7 +48,7 @@ export async function initHomepage(): Promise<void> {
// Load search index // Load search index
const searchIndex = await fetchData<SearchItem[]>('search-index.json'); const searchIndex = await fetchData<SearchItem[]>('search-index.json');
if (searchIndex) { if (searchIndex) {
const search = new FuzzySearch(); const search = new FuzzySearch<SearchItem>();
search.setItems(searchIndex); search.setItems(searchIndex);
const searchInput = document.getElementById('global-search') as HTMLInputElement; const searchInput = document.getElementById('global-search') as HTMLInputElement;

View File

@@ -23,7 +23,7 @@ interface InstructionsData {
const resourceType = 'instruction'; const resourceType = 'instruction';
let allItems: Instruction[] = []; let allItems: Instruction[] = [];
let search = new FuzzySearch(); let search = new FuzzySearch<Instruction>();
let extensionSelect: Choices; let extensionSelect: Choices;
let currentFilters = { extensions: [] as string[] }; let currentFilters = { extensions: [] as string[] };

View File

@@ -22,7 +22,7 @@ interface PromptsData {
const resourceType = 'prompt'; const resourceType = 'prompt';
let allItems: Prompt[] = []; let allItems: Prompt[] = [];
let search = new FuzzySearch(); let search = new FuzzySearch<Prompt>();
let toolSelect: Choices; let toolSelect: Choices;
let currentFilters = { tools: [] as string[] }; let currentFilters = { tools: [] as string[] };

View File

@@ -33,7 +33,7 @@ interface SkillsData {
const resourceType = 'skill'; const resourceType = 'skill';
let allItems: Skill[] = []; let allItems: Skill[] = [];
let search = new FuzzySearch(); let search = new FuzzySearch<Skill>();
let categorySelect: Choices; let categorySelect: Choices;
let currentFilters = { let currentFilters = {
categories: [] as string[], categories: [] as string[],

View File

@@ -14,30 +14,36 @@ export interface SearchItem {
[key: string]: unknown; [key: string]: unknown;
} }
export interface SearchableItem {
title: string;
description?: string;
[key: string]: unknown;
}
export interface SearchOptions { export interface SearchOptions {
fields?: string[]; fields?: string[];
limit?: number; limit?: number;
minScore?: number; minScore?: number;
} }
export class FuzzySearch { export class FuzzySearch<T extends SearchableItem = SearchItem> {
private items: SearchItem[] = []; private items: T[] = [];
constructor(items: SearchItem[] = []) { constructor(items: T[] = []) {
this.items = items; this.items = items;
} }
/** /**
* Update the items to search * Update the items to search
*/ */
setItems(items: SearchItem[]): void { setItems(items: T[]): void {
this.items = items; this.items = items;
} }
/** /**
* Search items with fuzzy matching * Search items with fuzzy matching
*/ */
search(query: string, options: SearchOptions = {}): SearchItem[] { search(query: string, options: SearchOptions = {}): T[] {
const { const {
fields = ['title', 'description', 'searchText'], fields = ['title', 'description', 'searchText'],
limit = 50, limit = 50,
@@ -50,7 +56,7 @@ export class FuzzySearch {
const normalizedQuery = query.toLowerCase().trim(); const normalizedQuery = query.toLowerCase().trim();
const queryWords = normalizedQuery.split(/\s+/); const queryWords = normalizedQuery.split(/\s+/);
const results: Array<{ item: SearchItem; score: number }> = []; const results: Array<{ item: T; score: number }> = [];
for (const item of this.items) { for (const item of this.items) {
const score = this.calculateScore(item, queryWords, fields); const score = this.calculateScore(item, queryWords, fields);
@@ -68,14 +74,14 @@ export class FuzzySearch {
/** /**
* Calculate match score for an item * Calculate match score for an item
*/ */
private calculateScore(item: SearchItem, queryWords: string[], fields: string[]): number { private calculateScore(item: T, queryWords: string[], fields: string[]): number {
let totalScore = 0; let totalScore = 0;
for (const word of queryWords) { for (const word of queryWords) {
let wordScore = 0; let wordScore = 0;
for (const field of fields) { for (const field of fields) {
const value = item[field]; const value = (item as Record<string, unknown>)[field];
if (!value) continue; if (!value) continue;
const normalizedValue = String(value).toLowerCase(); const normalizedValue = String(value).toLowerCase();
@@ -108,7 +114,7 @@ export class FuzzySearch {
// Bonus for matching all words // Bonus for matching all words
const matchesAllWords = queryWords.every(word => const matchesAllWords = queryWords.every(word =>
fields.some(field => { fields.some(field => {
const value = item[field]; const value = (item as Record<string, unknown>)[field];
return value && String(value).toLowerCase().includes(word); return value && String(value).toLowerCase().includes(word);
}) })
); );
@@ -140,13 +146,13 @@ export class FuzzySearch {
} }
} }
// Global search instance // Global search instance (uses SearchItem for the global search index)
export const globalSearch = new FuzzySearch(); export const globalSearch = new FuzzySearch<SearchItem>();
/** /**
* Initialize global search with search index * Initialize global search with search index
*/ */
export async function initGlobalSearch(): Promise<FuzzySearch> { export async function initGlobalSearch(): Promise<FuzzySearch<SearchItem>> {
const searchIndex = await fetchData<SearchItem[]>('search-index.json'); const searchIndex = await fetchData<SearchItem[]>('search-index.json');
if (searchIndex) { if (searchIndex) {
globalSearch.setItems(searchIndex); globalSearch.setItems(searchIndex);