import { invoke, isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { AutomaticIndexGate, emptySearchState, LatestLane, } from "./search-state"; import type { SemanticPhase } from "./search-state"; import { fileIcon, icon } from "./icons"; import "@fontsource-variable/inter/wght.css"; import "./style.css"; interface SearchResult { path: string; filename: string; snippet: string; score: number; match_kind: string; source?: { kind: string; number: number }; extraction_error?: string; } interface IndexReport { discovered: number; indexed: number; removed: number; skipped: number; } interface IndexStats { files: number; content_files: number; } interface Settings { version: number; roots: string[]; cpu_threads: number; } interface DesktopStatus { settings: Settings; indexing: boolean; shortcut_status: string; } interface SemanticStatus { model: string; ready: boolean; model_directory: string; worker_ready: boolean; downloading: boolean; embedding: boolean; device_policy: "auto" | "cpu"; error: string | null; downloaded_bytes: number; total_bytes: number; generation: number | null; chunks: number; stale: boolean; pending: number; warming: boolean; worker_info?: { model?: string; device?: string; runtime?: string; threads?: number; } | null; } interface SearchRequest { id: number; query: string; filters: Record; started: number; } document.querySelector("#app")!.innerHTML = `
searchi
Loading your index…
${icon("filter")}All files${icon("chevron")}

Filter your search

${icon("settings")}

Folders & resources

Search automatically uses supported acceleration with CPU fallback.

    ${icon("sparkles")}Preparing local semantic search…Local model${icon("chevron")}
    Google EmbeddingGemma 2

    The verified local model downloads automatically on first launch (865 MB). Your files stay on this device.

    READY WHEN YOU ARE

      ${icon("search")}

      Find what matters. Locally.

      Index a folder, then search a filename, phrase, or what you remember.

      ${icon("lock")}Private search. Everything stays on your device.Local${icon("arrowUp")}${icon("arrowDown")}NavigateEnterOpenEscClear
      `; const element = (id: string) => document.getElementById(id) as T; const query = element("query"); const list = element("results"); const notice = element("notice"); let results: SearchResult[] = []; let selected = -1; let requestId = 0; let timer: ReturnType; let indexingActive = false; let semanticStatus: SemanticStatus | null = null; let semanticJobActive = false; let semanticRefreshActive = false; let semanticTimer: ReturnType | undefined; let hybridTimer: ReturnType | undefined; let latestSearch: SearchRequest | null = null; let shownLane: "lexical" | "hybrid" = "lexical"; let folderRevision = 0; let localSemanticError: string | null = null; let querySemanticFailed = false; let previewPath: string | null = null; let displayedRequestId: number | null = null; let expandedError: string | null = null; const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)"); const resultRows = new Map< string, { row: HTMLLIElement; button: HTMLButtonElement; title: HTMLElement; path: HTMLElement; snippet: HTMLElement; kind: HTMLElement; file: HTMLElement; } >(); const automaticIndex = new AutomaticIndexGate(); function refreshEmptySearchState() { if (!latestSearch || latestSearch.id !== requestId || results.length) return; let phase: SemanticPhase; if (querySemanticFailed || localSemanticError || semanticStatus?.error) phase = "unavailable"; else if ( semanticStatus?.embedding || semanticJobActive || indexingActive || (semanticStatus?.ready && semanticStatus.stale && semanticStatus.pending > 0) ) phase = "indexing"; else if (!semanticStatus?.ready) phase = "preparing"; else if (semanticStatus.generation === null) phase = "no-content"; else phase = shownLane === "hybrid" ? "complete" : "pending"; const state = emptySearchState(shownLane, phase); element("empty").querySelector("h2")!.textContent = state.title; element("empty").querySelector("p")!.textContent = state.detail; } function canSearchSemantically() { return Boolean( semanticStatus?.ready && semanticStatus.generation !== null && !semanticStatus.embedding && !semanticJobActive, ); } function queueSemanticRefresh(delay = 1000) { clearTimeout(semanticTimer); semanticTimer = setTimeout(() => { void refreshSemanticStatus(); }, delay); } async function buildSemanticIndex() { if (semanticJobActive || indexingActive) return; semanticJobActive = true; localSemanticError = null; element("semantic-status").textContent = "Building your semantic index… Keyword search remains available."; element("semantic-progress").removeAttribute("value"); element("semantic-progress").hidden = false; queueSemanticRefresh(); try { const report = await invoke<{ passages: number; chunks: number }>( "index_embeddings", ); element("semantic-status").textContent = `Semantic search indexed ${report.chunks.toLocaleString()} passages.`; } catch (error) { localSemanticError = String(error); } finally { semanticJobActive = false; queueSemanticRefresh(0); } } async function refreshSemanticStatus() { if (semanticRefreshActive) { queueSemanticRefresh(); return; } semanticRefreshActive = true; try { const previousReady = canSearchSemantically(); const previousGeneration = semanticStatus?.generation; const status = await invoke("semantic_status"); semanticStatus = status; refreshEmptySearchState(); const error = localSemanticError || status.error; if (error && error !== expandedError) { element("semantic-panel").open = true; expandedError = error; } if (!error) expandedError = null; const progress = element("semantic-progress"); const working = status.downloading || status.warming || status.embedding || semanticJobActive; progress.hidden = !working; progress.removeAttribute("value"); if ( status.downloading && status.total_bytes > 0 && status.downloaded_bytes < status.total_bytes ) { progress.max = status.total_bytes; progress.value = status.downloaded_bytes; } const actualDevice = status.worker_info?.device || (status.device_policy === "cpu" ? "CPU" : "Automatic acceleration"); element("privacy").textContent = `Local · ${actualDevice}`; element("semantic-device").textContent = actualDevice; let message: string; if (error) message = error; else if (status.warming) message = "Preparing Google semantic search on your device…"; else if (status.downloading) message = status.downloaded_bytes >= status.total_bytes ? "Verifying the downloaded Google model…" : `Downloading Google model · ${Math.round(status.downloaded_bytes / 1_000_000)} of ${Math.round(status.total_bytes / 1_000_000)} MB`; else if (status.embedding || semanticJobActive) message = "Building your semantic index… Keyword search remains available."; else if (status.ready && status.chunks > 0) message = `Semantic search ready · ${status.chunks.toLocaleString()} passages${status.stale ? " · Updates pending" : ""}`; else if (status.ready) message = "Google model ready. Index a folder to search by meaning."; else message = "Preparing local semantic search… Keyword search remains available."; element("semantic-status").textContent = message; element("semantic-status").classList.toggle("error", Boolean(error)); element("semantic-detail").textContent = status.worker_info ? `${status.model} · ${status.worker_info.runtime || "Local inference"} · Your files stay on this device.` : "The verified Google model downloads automatically once (865 MB). After setup, inference runs locally."; const retry = element("retry-model"); retry.hidden = !error || status.ready; retry.disabled = working; const rebuild = element("rebuild-semantic"); rebuild.hidden = !status.ready || (!status.chunks && !status.pending); rebuild.disabled = working || indexingActive; element("cpu-threads").disabled = working || indexingActive; if ( status.ready && status.stale && status.pending > 0 && !working && !indexingActive ) { const key = `${status.generation}:${status.pending}:${folderRevision}`; if (automaticIndex.claim(key)) { void buildSemanticIndex(); return; } } if ( (!previousReady || previousGeneration !== status.generation) && canSearchSemantically() && latestSearch?.id === requestId ) hybridLane.submit(latestSearch); if (working || (!status.ready && !error)) queueSemanticRefresh(); } catch (error) { element("semantic-status").textContent = `Semantic search unavailable: ${String(error)}`; element("semantic-status").classList.add("error"); element("retry-model").hidden = false; } finally { semanticRefreshActive = false; } } async function refreshDesktopStatus() { const status = await invoke("desktop_status"); indexingActive = status.indexing; element("shortcut-status").textContent = status.shortcut_status; element("focus-shortcut").textContent = status.shortcut_status.includes( "Super", ) ? "Cmd K" : "Ctrl K"; const threads = element("cpu-threads"); threads.value = String(status.settings.cpu_threads); threads.disabled = status.indexing || semanticJobActive || Boolean( semanticStatus?.downloading || semanticStatus?.warming || semanticStatus?.embedding, ); element("add-folder").disabled = status.indexing; const roots = element("roots"); roots.replaceChildren(); for (const root of status.settings.roots) { const item = document.createElement("li"); const path = document.createElement("span"); path.textContent = root; path.className = "path"; const reindex = document.createElement("button"); reindex.type = "button"; reindex.textContent = "Reindex"; reindex.disabled = status.indexing; reindex.addEventListener("click", () => { element("folder-form").hidden = false; element("folder").value = root; element("folder").focus(); }); item.append(path, reindex); roots.append(item); } if (!status.settings.roots.length) { const item = document.createElement("li"); item.textContent = "No folders selected yet."; roots.append(item); } if (status.indexing) element("index-status").textContent = "Indexing active · Search remains available"; return status; } function showError(error: unknown) { notice.textContent = String(error); notice.className = "error"; } function select(index: number, focus = false) { if (!results.length) return; selected = Math.max(0, Math.min(index, results.length - 1)); const result = results[selected]; list.querySelectorAll("button").forEach((button, i) => { button.classList.toggle("selected", i === selected); button.setAttribute("aria-current", String(i === selected)); button.tabIndex = i === selected ? 0 : -1; if (i === selected && focus) button.focus(); }); element("preview").hidden = false; const selectedIcon = fileIcon(result.filename); element("preview-icon").innerHTML = selectedIcon.html; element("preview-icon").dataset.type = selectedIcon.type; element("preview-title").textContent = result.filename; element("preview-path").textContent = result.path; element("preview-kind").textContent = `${result.match_kind} match${result.source ? ` · ${result.source.kind} ${result.source.number}` : ""}`; element("preview-text").textContent = result.snippet || (result.extraction_error ? `Content could not be extracted: ${result.extraction_error}` : "No extracted text is available for this file."); element("open-result").title = result.source ? `Opens this file in your default application. ${result.source.kind} ${result.source.number} is shown here; exact-location opening is not supported by the default application interface.` : "Open in your default application"; if (previewPath !== result.path) { const passage = document.querySelector(".passage")!; passage.scrollTop = 0; if (!reducedMotion.matches) { passage.getAnimations().forEach((animation) => animation.cancel()); passage.animate( [ { opacity: 0.65, transform: "translateY(3px)" }, { opacity: 1, transform: "translateY(0)" }, ], { duration: 150, easing: "ease-out" }, ); } previewPath = result.path; } } function render() { const scrollTop = list.scrollTop; const retained = new Set(results.map((result) => result.path)); for (const [key, entry] of resultRows) { if (!retained.has(key)) { entry.row.remove(); resultRows.delete(key); } } element("empty").hidden = results.length > 0; if (!results.length) { element("preview").hidden = true; previewPath = null; } results.forEach((result, i) => { let entry = resultRows.get(result.path); if (!entry) { const row = document.createElement("li"); if (i < 8) row.className = "new-row"; const button = document.createElement("button"); button.className = "result"; button.type = "button"; const file = document.createElement("span"); file.className = "file-icon"; const title = document.createElement("strong"); const path = document.createElement("span"); path.className = "path"; const snippet = document.createElement("span"); snippet.className = "snippet"; const kind = document.createElement("span"); kind.className = "kind"; button.append(file, title, kind, snippet, path); button.addEventListener("click", () => select(Number(button.dataset.index)), ); row.append(button); entry = { row, button, file, title, path, snippet, kind }; resultRows.set(result.path, entry); } entry.button.dataset.index = String(i); entry.button.title = result.path; entry.title.textContent = result.filename; entry.path.textContent = result.path; entry.snippet.textContent = result.snippet; entry.kind.textContent = result.source ? `${result.source.kind} ${result.source.number}` : result.match_kind; const asset = fileIcon(result.filename); if (entry.file.dataset.type !== asset.type) { entry.file.innerHTML = asset.html; entry.file.dataset.type = asset.type; } const positioned = list.children.item(i); if (positioned !== entry.row) list.insertBefore(entry.row, positioned); }); list.scrollTop = scrollTop; } function captureFilters() { const input = (id: string) => element(id).value.trim(); const size = (id: string) => { const text = input(id); if (!text) return null; const value = Number(text); if (!Number.isSafeInteger(value) || value < 0) throw new Error("File size must be a nonnegative whole number of bytes."); return value; }; // Date controls represent the user's local calendar day, including its end. const date = (id: string, end: boolean) => { const text = input(id); if (!text) return null; const stamp = new Date( `${text}T${end ? "23:59:59.999" : "00:00:00"}`, ).getTime(); if (!Number.isFinite(stamp)) throw new Error("Enter a valid modification date."); return stamp; }; return { folder: input("filter-folder") || null, extension: input("filter-extension") || null, min_size: size("filter-min"), max_size: size("filter-max"), modified_after_unix_ms: date("filter-after", false), modified_before_unix_ms: date("filter-before", true), }; } function applyResults( found: SearchResult[], request: SearchRequest, lane: "lexical" | "hybrid", ) { if ( request.id !== requestId || (lane === "lexical" && shownLane === "hybrid") ) return; const selectedPath = results[selected]?.path; const resultsHadFocus = list.contains(document.activeElement); shownLane = lane; displayedRequestId = request.id; results = found; selected = -1; render(); const preserved = results.findIndex((result) => result.path === selectedPath); if (preserved >= 0) select(preserved, resultsHadFocus); else if (results.length) select(0, resultsHadFocus); element("result-summary").textContent = `${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · ${Math.round(performance.now() - request.started)} MS`; if (!found.length) refreshEmptySearchState(); } const isCurrent = (request: SearchRequest) => request.id === requestId; const lexicalLane = new LatestLane( async (request) => { const found = await invoke("search", { query: request.query, filters: request.filters, }); applyResults(found, request, "lexical"); }, isCurrent, (_request, error) => { if (shownLane !== "hybrid") { showError(error); element("result-summary").textContent = "KEYWORD SEARCH UNAVAILABLE"; } }, ); const hybridLane = new LatestLane( async (request) => { if (!canSearchSemantically()) return; const found = await invoke("hybrid_search", { query: request.query, filters: request.filters, }); applyResults(found, request, "hybrid"); }, isCurrent, (_request, error) => { querySemanticFailed = true; refreshEmptySearchState(); element("semantic-status").textContent = `Semantic results unavailable: ${String(error)}. Keyword results remain available.`; queueSemanticRefresh(); }, ); function invalidateSearch() { ++requestId; clearTimeout(timer); clearTimeout(hybridTimer); lexicalLane.clear(); hybridLane.clear(); latestSearch = null; } async function search() { invalidateSearch(); querySemanticFailed = false; shownLane = "lexical"; const value = query.value.trim(); if (!value) { results = []; displayedRequestId = null; selected = -1; render(); element("result-summary").textContent = "READY WHEN YOU ARE"; element("empty").querySelector("h2")!.textContent = "Find what matters. Locally."; element("empty").querySelector("p")!.textContent = "Index a folder, then search a filename, phrase, or what you remember."; return; } try { const request: SearchRequest = { id: requestId, query: value, filters: captureFilters(), started: performance.now(), }; latestSearch = request; element("result-summary").textContent = "SEARCHING…"; lexicalLane.submit(request); hybridTimer = setTimeout(() => { if (isCurrent(request) && canSearchSemantically()) hybridLane.submit(request); }, 350); } catch (error) { showError(error); } } async function refreshStats() { const stats = await invoke("stats"); element("index-status").textContent = `${stats.files.toLocaleString()} files · ${stats.content_files.toLocaleString()} with searchable content`; } query.addEventListener("input", () => { invalidateSearch(); timer = setTimeout(search, 60); }); element("search-form").addEventListener("submit", (event) => { event.preventDefault(); clearTimeout(timer); void search(); }); element("clear-query").addEventListener("click", () => { query.value = ""; void search(); query.focus(); }); document.querySelectorAll(".popover").forEach((popover) => { popover.addEventListener("toggle", () => { if (popover.open) document .querySelectorAll(".popover") .forEach((other) => { if (other !== popover) other.open = false; }); }); }); document.addEventListener("pointerdown", (event) => { document .querySelectorAll(".popover[open]") .forEach((popover) => { if (!popover.contains(event.target as Node)) popover.open = false; }); }); element("add-folder").addEventListener("click", () => { element("folder-form").hidden = false; element("folder").focus(); }); element("cancel-folder").addEventListener("click", () => { element("folder-form").hidden = true; query.focus(); }); element("browse-folder").addEventListener("click", async () => { try { const folder = await invoke("pick_folder"); if (folder) element("folder").value = folder; } catch (error) { showError(error); } }); element("filters").addEventListener("change", () => { void search(); }); element("retry-model").addEventListener("click", async () => { const button = element("retry-model"); button.disabled = true; localSemanticError = null; element("semantic-status").textContent = "Retrying Google model setup…"; queueSemanticRefresh(); try { await invoke("setup_model"); } catch (error) { localSemanticError = String(error); } finally { button.disabled = false; queueSemanticRefresh(0); } }); element("rebuild-semantic").addEventListener("click", () => { void buildSemanticIndex(); }); async function openSelected(reveal: boolean) { if (selected < 0 || !results[selected]) return; try { await invoke("open_result", { path: results[selected].path, reveal }); } catch (error) { showError(error); } } element("open-result").addEventListener("click", () => { void openSelected(false); }); element("reveal-result").addEventListener("click", () => { void openSelected(true); }); element("cpu-threads").addEventListener("change", async () => { try { await invoke("set_cpu_threads", { threads: Number(element("cpu-threads").value), }); await refreshDesktopStatus(); queueSemanticRefresh(0); } catch (error) { showError(error); await refreshDesktopStatus().catch(showError); } }); element("folder-form").addEventListener("submit", async (event) => { event.preventDefault(); const input = element("folder"); const buttons = element("folder-form").querySelectorAll("button"); buttons.forEach((button) => { button.disabled = true; }); element("add-folder").disabled = true; element("cpu-threads").disabled = true; indexingActive = true; element("index-status").textContent = "Indexing active · Search remains available"; notice.className = ""; notice.textContent = "Indexing folder… Search remains available."; try { const report = await invoke("index_folder", { root: input.value.trim(), }); notice.textContent = `Indexed ${report.indexed} files. ${report.removed} removed; ${report.skipped} skipped.`; element("folder-form").hidden = true; folderRevision++; localSemanticError = null; await refreshStats(); await search(); } catch (error) { showError(error); } finally { buttons.forEach((button) => { button.disabled = false; }); element("add-folder").disabled = false; await refreshDesktopStatus().catch(showError); await refreshStats().catch(showError); queueSemanticRefresh(0); } }); document.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") { event.preventDefault(); query.focus(); query.select(); } if (event.key === "Escape") { document .querySelectorAll(".popover[open]") .forEach((popover) => { popover.open = false; }); ++requestId; query.value = ""; void search(); query.focus(); } if (event.target === query || list.contains(event.target as Node)) { if ( event.key === "Enter" && results[selected] && displayedRequestId === requestId ) { event.preventDefault(); void openSelected(false); } if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); select(selected + (event.key === "ArrowDown" ? 1 : -1), true); } } }); if (isTauri()) { void refreshStats().catch(showError); void refreshDesktopStatus().catch(showError); void refreshSemanticStatus(); void listen("focus-search", () => { query.focus(); query.select(); void refreshDesktopStatus().catch(showError); queueSemanticRefresh(0); }).catch(showError); // Poll only during a user-triggered job; idle indexing status does no work. setInterval(() => { if (indexingActive) void refreshDesktopStatus().catch(showError); }, 2000); query.focus(); } else { element("index-status").textContent = "Desktop preview"; element("semantic-status").textContent = "Native semantic search runs in the desktop app."; element("semantic-progress").hidden = true; element("semantic-detail").textContent = "This browser preview does not download models or return simulated search results."; showError("Run the Tauri desktop app to index and search real files."); }