diff --git a/src/plugins/built-in/globalSearch/src/components/SearchBar.svelte b/src/plugins/built-in/globalSearch/src/components/SearchBar.svelte index 82c7811f..c0acdd89 100644 --- a/src/plugins/built-in/globalSearch/src/components/SearchBar.svelte +++ b/src/plugins/built-in/globalSearch/src/components/SearchBar.svelte @@ -6,13 +6,13 @@ import { type StaticCommandItem } from '../core/commands'; import type { CombinedResult } from '../core/types'; import { createSearchIndexes, performSearch as doSearch } from '../search/searchUtils'; - import { highlightMatch, highlightSnippet, stripHtmlButKeepHighlights } from '../utils/highlight'; import Fuse from 'fuse.js'; import Calculator from './Calculator.svelte'; import { actionMap } from '../indexing/actions'; import type { IndexItem } from '../indexing/types'; import debounce from 'lodash/debounce'; import { renderComponentMap } from '../indexing/renderComponents'; + import HighlightedText from '../utils/HighlightedText.svelte'; const { transparencyEffects, @@ -279,7 +279,7 @@ >
{staticItem.icon}
- {@html highlightMatch(staticItem.text, searchTerm, result.matches)} + {#if staticItem.keybindLabel}
@@ -310,7 +310,7 @@
{dynamicItem.metadata?.icon || '\ue924'}
- {@html stripHtmlButKeepHighlights(highlightMatch(dynamicItem.text, searchTerm, result.matches))} + {dynamicItem.category} @@ -318,7 +318,7 @@
{#if dynamicItem.content}
- {@html stripHtmlButKeepHighlights(highlightSnippet(dynamicItem.content, searchTerm, result.matches))} +
{/if} diff --git a/src/plugins/built-in/globalSearch/src/components/items/AssessmentItem.svelte b/src/plugins/built-in/globalSearch/src/components/items/AssessmentItem.svelte index 2c3bff15..55b88777 100644 --- a/src/plugins/built-in/globalSearch/src/components/items/AssessmentItem.svelte +++ b/src/plugins/built-in/globalSearch/src/components/items/AssessmentItem.svelte @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/plugins/built-in/globalSearch/src/indexing/jobs/subjects.ts b/src/plugins/built-in/globalSearch/src/indexing/jobs/subjects.ts index d5d8909d..e480db49 100755 --- a/src/plugins/built-in/globalSearch/src/indexing/jobs/subjects.ts +++ b/src/plugins/built-in/globalSearch/src/indexing/jobs/subjects.ts @@ -28,11 +28,10 @@ export const subjectsJob: Job = { // Boost for active subjects if (item.metadata?.isActive) { score += 15; // Boost for active subjects + console.log("active subject:", item.metadata.subjectName); + } else { + console.log("inactive subject:", item.metadata.subjectName); } - - // Boost for year level - const yearLevel = item.metadata?.yearLevel || 0; - score += yearLevel; return score; }, @@ -81,9 +80,7 @@ export const subjectsJob: Job = { const id = `${semester.code}-${subject.code}-${subject.metaclass}`; if (existingIds.has(id)) continue; - // Extract year level from subject code (assuming format like "YEAR10" or "10ENG") - const yearLevel = subject.code.match(/^YEAR(\d+)|^(\d+)/i)?.[1] || subject.code.match(/^(\d+)/)?.[1] || 0; - const isActive = subject.active === 1; + const isActive = semester.active === 1; // Create two items for each subject - one for assessments and one for course items.push( @@ -101,7 +98,6 @@ export const subjectsJob: Job = { semesterCode: semester.code, semesterDescription: semester.description, type: "assessments", - yearLevel: yearLevel ? Number(yearLevel) : 0, isActive }, actionId: "subjectassessment", @@ -121,7 +117,6 @@ export const subjectsJob: Job = { semesterCode: semester.code, semesterDescription: semester.description, type: "course", - yearLevel: yearLevel ? Number(yearLevel) : 0, isActive }, actionId: "subjectcourse", diff --git a/src/plugins/built-in/globalSearch/src/indexing/renderComponents.ts b/src/plugins/built-in/globalSearch/src/indexing/renderComponents.ts index 7c958e7b..633fa49c 100644 --- a/src/plugins/built-in/globalSearch/src/indexing/renderComponents.ts +++ b/src/plugins/built-in/globalSearch/src/indexing/renderComponents.ts @@ -2,15 +2,10 @@ import type { SvelteComponent } from "svelte"; import AssessmentItem from "../components/items/AssessmentItem.svelte"; import ForumItem from "../components/items/ForumItem.svelte"; import SubjectItem from "../components/items/SubjectItem.svelte"; -import type { IndexItem } from "./types"; -import { highlightMatch } from "../utils/highlight"; -// import other components as needed export const renderComponentMap: Record = { assessment: AssessmentItem as unknown as typeof SvelteComponent, message: AssessmentItem as unknown as typeof SvelteComponent, forum: ForumItem as unknown as typeof SvelteComponent, subject: SubjectItem as unknown as typeof SvelteComponent, - // subject: SubjectComponent, - // etc... }; \ No newline at end of file diff --git a/src/plugins/built-in/globalSearch/src/search/searchUtils.ts b/src/plugins/built-in/globalSearch/src/search/searchUtils.ts index ee9ec6ee..1b286875 100644 --- a/src/plugins/built-in/globalSearch/src/search/searchUtils.ts +++ b/src/plugins/built-in/globalSearch/src/search/searchUtils.ts @@ -25,10 +25,6 @@ export function createSearchIndexes() { { name: "text", weight: 2 }, { name: "content", weight: 1 }, { name: "category", weight: 1 }, - { name: "metadata.subjectName", weight: 3 }, - { name: "metadata.subjectCode", weight: 2.5 }, - { name: "metadata.semesterDescription", weight: 1 }, - { name: "metadata.yearLevel", weight: 1.5 } ], includeScore: true, includeMatches: true, @@ -117,12 +113,6 @@ export function searchDynamicItems( let score = fuseScore; - // apply boost criteria if it exists - const boost = jobs[item.category].boostCriteria?.(item, query); - if (boost) { - score += boost; - } - const ageInDays = (now - item.dateAdded) / (1000 * 60 * 60 * 24); const recencyBoost = sortByRecent ? 1 / (ageInDays + 1) : 0; score += recencyBoost; @@ -196,10 +186,18 @@ export async function performSearch( if (!seenIds.has(id)) { // This is a semantic match that Fuse missed - add it with the vector similarity as score + let score = v.similarity * 0.5; // High base score for semantic matches + const job = jobs[v.object.category]; + if (job && typeof job.boostCriteria === 'function') { + const boost = job.boostCriteria(v.object, query); + if (boost) { + score += boost; + } + } resultMap.set(id, { id, type: "dynamic" as const, - score: v.similarity * 0.9, // High base score for semantic matches + score, item: v.object, }); } diff --git a/src/plugins/built-in/globalSearch/src/utils/HighlightedText.svelte b/src/plugins/built-in/globalSearch/src/utils/HighlightedText.svelte new file mode 100644 index 00000000..ce4410cd --- /dev/null +++ b/src/plugins/built-in/globalSearch/src/utils/HighlightedText.svelte @@ -0,0 +1,80 @@ + + + + {#each segments as segment} + {#if segment.highlight} + {segment.text} + {:else} + {segment.text} + {/if} + {/each} + \ No newline at end of file diff --git a/src/plugins/built-in/globalSearch/src/utils/highlight.ts b/src/plugins/built-in/globalSearch/src/utils/highlight.ts deleted file mode 100644 index 9c2e3ca9..00000000 --- a/src/plugins/built-in/globalSearch/src/utils/highlight.ts +++ /dev/null @@ -1,274 +0,0 @@ -import type { FuseResultMatch, MatchIndices } from "../core/types"; - -/** - * Simple utility to remove HTML tags from a string. - */ -export function stripHtmlTags(html: string): string { - if (!html) return ""; - return html.replace(/<[^>]*>/g, "").replace("\n", " "); -} - -/** - * Removes HTML tags from a string, but preserves tags. - */ -export function stripHtmlButKeepHighlights(html: string): string { - if (!html) return ""; - // Use a placeholder for highlight tags, strip others, then restore placeholders. - const highlightOpenPlaceholder = "__HIGHLIGHT_OPEN__"; - const highlightClosePlaceholder = "__HIGHLIGHT_CLOSE__"; - - let processed = html.replace( - //g, - highlightOpenPlaceholder, - ); - processed = processed.replace(/<\/span>/g, (match, offset, fullString) => { - // Only replace if it likely corresponds to our highlight span - // This is imperfect but helps avoid replacing unrelated spans. - // Look backwards for the nearest opening placeholder. - const lastPlaceholder = fullString.lastIndexOf( - highlightOpenPlaceholder, - offset, - ); - if (lastPlaceholder !== -1) { - // Check if there's another opening tag between the placeholder and the closing span - const interveningContent = fullString.substring( - lastPlaceholder + highlightOpenPlaceholder.length, - offset, - ); - if (!/ if unsure - }); - - // Strip all remaining HTML tags - processed = processed.replace(/<[^>]*>/g, ""); - - // Restore the highlight tags - processed = processed.replace( - new RegExp(highlightOpenPlaceholder, "g"), - '', - ); - processed = processed.replace( - new RegExp(highlightClosePlaceholder, "g"), - "", - ); - - return processed; -} - -export function highlightMatch( - text: string, - term: string, - matches?: readonly FuseResultMatch[], -): string { - if (!term.trim() || !matches || matches.length === 0) return text; - - try { - // Find matches for the text field or allContent that contains the text - const fieldMatches = matches.find( - (match) => - match.key === "text" || - (match.key === "allContent" && match.value?.includes(text)), - ); - - if ( - !fieldMatches || - !fieldMatches.indices || - fieldMatches.indices.length === 0 - ) { - return text; - } - - // Create a map of character positions to mark which ones need highlighting - const highlightMap = new Array(text.length).fill(false); - - fieldMatches.indices.forEach((indices: MatchIndices) => { - const start = indices[0]; - const end = indices[1]; - - if (fieldMatches.key === "allContent") { - // Find where our text appears in the allContent - const allContent = fieldMatches.value; - const textPos = allContent?.indexOf(text) ?? -1; - - // Only highlight if the match overlaps with our text - if (textPos >= 0) { - // Adjust start and end to be relative to our text field - const relStart = start - textPos; - const relEnd = end - textPos; - - // Only highlight if the match actually overlaps with our text field - if (relEnd >= 0 && relStart < text.length) { - // Mark the overlapping characters - for ( - let i = Math.max(0, relStart); - i <= Math.min(text.length - 1, relEnd); - i++ - ) { - highlightMap[i] = true; - } - } - } - } else { - // Regular text field match - ensure indices are within bounds - if (start >= 0 && end < text.length) { - for (let i = start; i <= end; i++) { - highlightMap[i] = true; - } - } - } - }); - - let result = ""; - let inHighlight = false; - - for (let i = 0; i < text.length; i++) { - if (highlightMap[i] && !inHighlight) { - result += ''; - inHighlight = true; - } else if (!highlightMap[i] && inHighlight) { - result += ""; - inHighlight = false; - } - - result += text.charAt(i); - } - - if (inHighlight) { - result += ""; - } - - return result; - } catch (e) { - console.error("Error highlighting match:", e); - return text; - } -} - -// Function to extract and highlight content snippet using Fuse matches -export function highlightSnippet( - content: string, - term: string, - matches?: readonly FuseResultMatch[], -): string { - if (!content || !term.trim() || !matches || matches.length === 0) - return content; - - try { - // Find matches for content field or allContent that contains the content - const contentMatches = matches.find( - (match) => - match.key === "content" || - (match.key === "allContent" && match.value?.includes(content)), - ); - - if ( - !contentMatches || - !contentMatches.indices || - contentMatches.indices.length === 0 - ) { - // No content matches, return plain content - return content.length > 100 ? content.substring(0, 100) + "..." : content; - } - - // Find the match indices - let allIndices: MatchIndices[] = contentMatches.indices as MatchIndices[]; - - // If matching against allContent, adjust indices to be relative to content - if (contentMatches.key === "allContent") { - const allContent = contentMatches.value; - const contentPos = allContent?.indexOf(content) ?? -1; - - if (contentPos >= 0) { - // Adjust indices to be relative to the content field - allIndices = allIndices - .map( - (indices) => - [ - indices[0] - contentPos, - indices[1] - contentPos, - ] as MatchIndices, - ) - .filter((indices) => indices[1] >= 0 && indices[0] < content.length); - } - } - - if (allIndices.length === 0) { - return content.length > 100 ? content.substring(0, 100) + "..." : content; - } - - // Find a good center point for our snippet (average of first match) - const firstMatch = allIndices[0]; - const matchCenter = Math.floor((firstMatch[0] + firstMatch[1]) / 2); - - // Extract a window around the match - const windowSize = 100; - const start = Math.max(0, matchCenter - windowSize / 2); - const end = Math.min(content.length, matchCenter + windowSize / 2); - - // Create the basic snippet - let snippet = content.substring(start, end); - if (start > 0) snippet = "..." + snippet; - if (end < content.length) snippet += "..."; - - // Create a highlighting map for the snippet - const snippetLength = snippet.length; - const highlightMap = new Array(snippetLength).fill(false); - - // Calculate offset for the highlighting - const startOffset = start > 0 ? start - 3 : start; // Account for '...' if present - - // Mark each matched character in the snippet - allIndices.forEach((indices: MatchIndices) => { - const matchStart = indices[0]; - const matchEnd = indices[1]; - - // Skip matches outside our snippet window - if (matchEnd < start || matchStart > end) return; - - // Adjust match indices to be relative to snippet - const snippetMatchStart = Math.max(0, matchStart - startOffset); - const snippetMatchEnd = Math.min( - snippetLength - 1, - matchEnd - startOffset, - ); - - // Mark characters for highlighting - for (let i = snippetMatchStart; i <= snippetMatchEnd; i++) { - if (i >= 0 && i < snippetLength) { - highlightMap[i] = true; - } - } - }); - - // Build the highlighted snippet - let result = ""; - let inHighlight = false; - - for (let i = 0; i < snippetLength; i++) { - // If highlighting state changes, add appropriate tags - if (highlightMap[i] && !inHighlight) { - result += ''; - inHighlight = true; - } else if (!highlightMap[i] && inHighlight) { - result += ""; - inHighlight = false; - } - - // Add the current character - result += snippet.charAt(i); - } - - // Close highlight tag if we're still in one at the end - if (inHighlight) { - result += ""; - } - - return result; - } catch (e) { - console.error("Error highlighting snippet:", e); - return content.length > 100 ? content.substring(0, 100) + "..." : content; - } -}