-
Notifications
You must be signed in to change notification settings - Fork 6
add client-side embeddings cached in IndexedDB #2432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 06-18-improve_component_search_relevance_and_ai_reranking
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ import { Skeleton } from "@/components/ui/skeleton"; | |
| import { Spinner } from "@/components/ui/spinner"; | ||
| import { QuickTooltip } from "@/components/ui/tooltip"; | ||
| import { Heading, Paragraph, Text } from "@/components/ui/typography"; | ||
| import { useAiProviderSettings } from "@/hooks/useAiProviderSettings"; | ||
| import { getComponentQueryKey } from "@/hooks/useHydrateComponentReference"; | ||
| import { | ||
| useComponentAiDescription, | ||
|
|
@@ -38,6 +39,7 @@ import { | |
| LibraryDB, | ||
| type StoredLibrary, | ||
| } from "@/providers/ComponentLibraryProvider/libraries/storage"; | ||
| import { rankComponentMatchesByEmbeddings } from "@/services/componentSearchEmbeddings"; | ||
| import { | ||
| buildSearchIndex, | ||
| type ComponentSearchSource, | ||
|
|
@@ -437,6 +439,23 @@ function sampleEvenly<T>(entries: T[], limit: number): T[] { | |
| return sampled; | ||
| } | ||
|
|
||
| function mergeUniqueMatches( | ||
| primary: LexicalMatch[], | ||
| secondary: LexicalMatch[], | ||
| fallback: LexicalMatch[], | ||
| limit: number, | ||
| ): LexicalMatch[] { | ||
| const merged: LexicalMatch[] = []; | ||
| const seen = new Set<string>(); | ||
| for (const match of [...primary, ...secondary, ...fallback]) { | ||
| if (seen.has(match.digest)) continue; | ||
| seen.add(match.digest); | ||
| merged.push(match); | ||
| if (merged.length >= limit) return merged; | ||
| } | ||
| return merged; | ||
| } | ||
|
|
||
| function collectAllSourcedReferences({ | ||
| standardLibrary, | ||
| publishedRefs, | ||
|
|
@@ -485,6 +504,7 @@ export const DashboardComponentsV2View = () => { | |
| "component-search-v2-ai-descriptions", | ||
| ); | ||
| const { backendUrl, configured, available } = useBackend(); | ||
| const { config: aiConfig } = useAiProviderSettings(); | ||
| const [query, setQuery] = useState(""); | ||
| const [disabledSourceKeys, setDisabledSourceKeys] = useState<string[]>([]); | ||
|
|
||
|
|
@@ -770,6 +790,8 @@ export const DashboardComponentsV2View = () => { | |
| const [rerankBaseMatches, setRerankBaseMatches] = useState<LexicalMatch[]>( | ||
| [], | ||
| ); | ||
| const [isEmbeddingSearchPending, setIsEmbeddingSearchPending] = | ||
| useState(false); | ||
|
|
||
| const handleQueryChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
| setQuery(event.target.value); | ||
|
|
@@ -780,27 +802,57 @@ export const DashboardComponentsV2View = () => { | |
| } | ||
| }; | ||
|
|
||
| const startAiSearch = (matches: LexicalMatch[]) => { | ||
| const buildEmbeddingMatches = async ( | ||
| trimmed: string, | ||
| limit: number, | ||
| ): Promise<LexicalMatch[]> => { | ||
| if (!aiConfig.apiBase.trim()) return []; | ||
| setIsEmbeddingSearchPending(true); | ||
| try { | ||
| return await rankComponentMatchesByEmbeddings( | ||
| filteredIndex, | ||
| trimmed, | ||
| { apiBase: aiConfig.apiBase, apiKey: aiConfig.apiKey }, | ||
| { limit }, | ||
| ); | ||
| } catch { | ||
| return []; | ||
| } finally { | ||
| setIsEmbeddingSearchPending(false); | ||
| } | ||
| }; | ||
|
|
||
| const startAiSearch = async (matches: LexicalMatch[], limit: number) => { | ||
| const trimmed = query.trim(); | ||
| if (trimmed.length === 0 || matches.length === 0) return; | ||
|
|
||
| const candidates = matches | ||
| const embeddingMatches = aiConfig.apiBase.trim() | ||
| ? await buildEmbeddingMatches(trimmed, limit) | ||
| : []; | ||
| const rerankMatches = mergeUniqueMatches( | ||
| matches.slice(0, 60), | ||
| embeddingMatches, | ||
| matches, | ||
| limit, | ||
| ); | ||
|
|
||
| const candidates = rerankMatches | ||
| .map((m) => componentReferenceToCandidate(m.reference, m.source)) | ||
| .filter((c): c is NonNullable<typeof c> => c !== null); | ||
|
|
||
| if (candidates.length === 0) return; | ||
|
|
||
| setRerankBaseMatches(matches); | ||
| setRerankBaseMatches(rerankMatches); | ||
| setRerankedFor(trimmed); | ||
| rerank({ query: trimmed, candidates }); | ||
| }; | ||
|
|
||
| const handleSmartSearch = () => { | ||
| startAiSearch(aiCandidateMatches); | ||
| void startAiSearch(aiCandidateMatches, aiCandidateMatches.length); | ||
| }; | ||
|
|
||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const handleDeepAiSearch = () => { | ||
| startAiSearch(deepAiCandidateMatches); | ||
| void startAiSearch(deepAiCandidateMatches, deepAiCandidateMatches.length); | ||
| }; | ||
|
|
||
| const handleSourceToggle = (sourceKey: string) => { | ||
|
|
@@ -845,7 +897,8 @@ export const DashboardComponentsV2View = () => { | |
| rerankData !== undefined && | ||
| rerankData.matches.length > 0 && | ||
| rerankBaseMatches.length > 0 && | ||
| !isReranking; | ||
| !isReranking && | ||
| !isEmbeddingSearchPending; | ||
|
|
||
| // What we actually render. Rerank wins when active; otherwise lexical. | ||
| const displayedResults = rerankActive | ||
|
|
@@ -1033,20 +1086,30 @@ export const DashboardComponentsV2View = () => { | |
| onClick={handleSmartSearch} | ||
| disabled={ | ||
| isReranking || | ||
| isEmbeddingSearchPending || | ||
| isEmpty || | ||
| aiCandidateMatches.length === 0 || | ||
| !isConfigured | ||
| } | ||
| aria-label={isReranking ? "AI search in progress" : "AI search"} | ||
| aria-label={ | ||
| isReranking || isEmbeddingSearchPending | ||
| ? "AI search in progress" | ||
| : "AI search" | ||
| } | ||
| title="AI search — rerank these results with an LLM" | ||
| > | ||
| {isReranking ? <Spinner size={16} /> : <Icon name="Sparkles" />} | ||
| {isReranking || isEmbeddingSearchPending ? ( | ||
| <Spinner size={16} /> | ||
| ) : ( | ||
| <Icon name="Sparkles" /> | ||
| )} | ||
| </Button> | ||
| <Button | ||
| variant="outline" | ||
| onClick={handleDeepAiSearch} | ||
| disabled={ | ||
| isReranking || | ||
| isEmbeddingSearchPending || | ||
| isEmpty || | ||
| deepAiCandidateMatches.length === 0 || | ||
| !isConfigured | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[LOW] This
mergeUniqueMatches(dedupe-by-digest) is duplicated verbatim inuseComponentSearchV2State.ts(~line 53). Consider exporting it once (e.g. fromcomponentSearchIndex.ts) and importing it in both hosts.