forked from Goutam/lynkeduppro-crm
feat: add core domain types for Photo Gallery SDK including media items, albums, and annotations
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { cosineSimilarity, type AIProvider } from '../ai/types';
|
||||
import { useGallery, useGalleryStoreApi } from '../store/context';
|
||||
import { liveMedia } from '../store/selectors';
|
||||
|
||||
const DEBOUNCE_MS = 350;
|
||||
const MIN_QUERY_LEN = 2;
|
||||
/** Cosine-similarity floor for a photo to count as a semantic match. CLIP ViT-B/16
|
||||
* scores strong matches ~0.25+, unrelated ~0.15 — 0.22 keeps it crisp. */
|
||||
const MATCH_THRESHOLD = 0.22;
|
||||
const MAX_RESULTS = 60;
|
||||
|
||||
/**
|
||||
* Headless worker: when the user types a query, embeds it with the provider's
|
||||
* text encoder (CLIP) and ranks every photo that has an image embedding by
|
||||
* cosine similarity, writing the ordered ids to the store. The selector blends
|
||||
* these "looks like" matches with the keyword results. Runs only if the provider
|
||||
* supports embedText; otherwise search stays purely keyword-based.
|
||||
*/
|
||||
export function SemanticSearch({ provider }: { provider: AIProvider }) {
|
||||
const api = useGalleryStoreApi();
|
||||
const query = useGallery((s) => s.searchQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider.embedText) return;
|
||||
const q = query.trim();
|
||||
if (q.length < MIN_QUERY_LEN) {
|
||||
api.getState().setSemanticResults(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(async () => {
|
||||
const embedded = liveMedia(api.getState().media).filter((m) => (m.embedding?.length ?? 0) > 0);
|
||||
if (embedded.length === 0) {
|
||||
if (!cancelled) api.getState().setSemanticResults(null);
|
||||
return;
|
||||
}
|
||||
const qvec = await provider.embedText!(q).catch(() => [] as number[]);
|
||||
if (cancelled || qvec.length === 0) return;
|
||||
const ranked = embedded
|
||||
.map((m) => ({ id: m.id, score: cosineSimilarity(qvec, m.embedding!) }))
|
||||
.filter((r) => r.score >= MATCH_THRESHOLD)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, MAX_RESULTS)
|
||||
.map((r) => r.id);
|
||||
if (!cancelled) api.getState().setSemanticResults(ranked);
|
||||
}, DEBOUNCE_MS);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [query, provider, api]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user