import MiniSearch from "minisearch"; import { ref, shallowRef } from "vue"; import type { Template } from "../types/template"; import type { Stack } from "../types/stack"; // Singleton state (module-level) let searchIndex: MiniSearch | null = null; let isIndexed = false; export type SearchableItem = (Template | Stack) & { itemType: "template" | "stack"; id: string; tags?: string[]; category?: string; }; export interface SearchResult { item: SearchableItem; score: number; match: Record; } const query = ref(""); const results = shallowRef([]); const isSearching = ref(false); function initializeIndex(templates: Template[], stacks: Stack[]) { if (isIndexed) return; searchIndex = new MiniSearch({ fields: ["name", "description", "tags", "category", "type"], storeFields: [ "name", "type", "description", "icon", "category", "itemType", ], searchOptions: { boost: { name: 3, // Highest priority for name matches tags: 2, // Stack tags, template keywords category: 1.5, // Type categorization type: 1.5, // Template type description: 1, // Base priority }, fuzzy: 0.2, prefix: true, combineWith: "AND", }, }); // Combine templates + stacks with unique IDs const searchableItems: SearchableItem[] = [ ...templates.map((t, i) => ({ ...t, id: `template-${i}`, itemType: "template" as const, tags: (t as any).tags || [], category: t.type, })), ...stacks.map((s, i) => ({ ...s, id: `stack-${i}`, itemType: "stack" as const, type: "stack", tags: s.tags || [], category: "stack", })), ]; searchIndex.addAll(searchableItems); isIndexed = true; } function search(q: string, allItems: SearchableItem[]): SearchResult[] { if (!q || q.length < 2) return []; const fuzzy = q.length <= 3 ? 0.3 : 0.2; return searchIndex! .search(q, { fuzzy, prefix: true, combineWith: "AND", }) .slice(0, 8) .map((r) => ({ item: allItems.find((item) => item.id === r.id)!, score: r.score, match: r.match, })); } export function useSearch() { function performSearch( q: string, templates: Template[], stacks: Stack[] = [], ) { query.value = q; isSearching.value = true; try { // Initialize index with both templates and stacks if (!isIndexed) initializeIndex(templates, stacks); const allItems: SearchableItem[] = [ ...templates.map((t, i) => ({ ...t, id: `template-${i}`, itemType: "template" as const, tags: (t as any).tags || [], category: t.type, })), ...stacks.map((s, i) => ({ ...s, id: `stack-${i}`, itemType: "stack" as const, type: "stack", tags: s.tags || [], category: "stack", })), ]; results.value = search(q, allItems); } finally { isSearching.value = false; } } function clearSearch() { query.value = ""; results.value = []; } return { query, results, isSearching, performSearch, clearSearch, initializeIndex, }; }