diff --git a/src/App.tsx b/src/App.tsx index 1ac335c..35493c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { LoginScreen } from './components/LoginScreen' import { DashboardLayout } from './components/DashboardLayout' import { AccessibilityProvider } from './contexts/AccessibilityContext' import { DetailPanelProvider } from './contexts/DetailPanelContext' +import { initModel } from './lib/embedding-model' function SkipButton({ onSkip }: { onSkip: () => void }) { const [visible, setVisible] = useState(false) @@ -47,6 +48,10 @@ function App() { const [phase, setPhase] = useState('login') const cursorPositionRef = useRef<{ x: number; y: number } | null>(null) + useEffect(() => { + initModel() + }, []) + const skipToLogin = () => setPhase('login') return ( diff --git a/src/lib/embedding-model.ts b/src/lib/embedding-model.ts new file mode 100644 index 0000000..1415509 --- /dev/null +++ b/src/lib/embedding-model.ts @@ -0,0 +1,26 @@ +import { pipeline, type FeatureExtractionPipeline } from '@xenova/transformers' + +let extractor: FeatureExtractionPipeline | null = null +let loading = false + +export async function initModel(): Promise { + if (extractor || loading) return + loading = true + try { + extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2') as FeatureExtractionPipeline + } catch { + // Silently swallow — model unavailable, semantic search won't activate + } finally { + loading = false + } +} + +export async function embedQuery(text: string): Promise { + if (!extractor) throw new Error('Model not loaded') + const output = await extractor(text, { pooling: 'mean', normalize: true }) + return Array.from(output.data as Float32Array) +} + +export function isModelReady(): boolean { + return extractor !== null +}