feat: highlight search matches and document the working UI
Core CI / core (ubuntu-latest) (push) Failing after 3m29s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 7m23s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Failing after 3m29s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 7m23s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
b8400c4fbf
commit
79553ce921
26 files changed
+4124
-50
No files matched your search
+140
-45
@@ -1,4 +1,11 @@
|
||||
import { invoke, isTauri, convertFileSrc } from "@tauri-apps/api/core";
|
||||
import {
|
||||
invoke,
|
||||
initializeDesktopSurface,
|
||||
isNativeDesktop,
|
||||
isDesktopSurface,
|
||||
isCaptureSurface,
|
||||
prepareMediaURL,
|
||||
} from "./desktop-api";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import {
|
||||
AutomaticIndexGate,
|
||||
@@ -14,6 +21,13 @@ import {
|
||||
briefMs,
|
||||
} from "./search-performance";
|
||||
import type { MeasuredResponse, SearchLane } from "./search-performance";
|
||||
import { displayPath } from "./path-display";
|
||||
import {
|
||||
literalQuery,
|
||||
renderHighlighted,
|
||||
displayExcerpt,
|
||||
matchExplanation,
|
||||
} from "./match-display";
|
||||
import "@fontsource-variable/inter/wght.css";
|
||||
import "./style.css";
|
||||
import {
|
||||
@@ -95,7 +109,7 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
||||
<form id="search-form" role="search"><label class="sr-only" for="query">Search your files</label>${icon("search")}<input id="query" type="search" placeholder="Search a filename, phrase, or what you remember…" autocomplete="off" spellcheck="false" maxlength="1000"><kbd id="focus-shortcut">Ctrl K</kbd><button id="clear-query" class="icon-button" type="button" aria-label="Clear search" title="Clear search">${icon("dismiss")}</button></form>
|
||||
<details id="semantic-panel"><summary>${icon("sparkles")}<span id="semantic-status" role="status" aria-live="polite">Preparing local semantic search…</span><span id="semantic-device" class="kind">Local model</span>${icon("chevron")}</summary><div class="semantic-content"><strong>Google EmbeddingGemma 2</strong><progress id="semantic-progress" aria-label="Local model setup progress"></progress><p id="semantic-detail" class="hint">The verified local model downloads automatically on first launch (865 MB). Your files stay on this device.</p><div class="semantic-actions"><button id="retry-model" type="button" hidden>Retry model setup</button><button id="rebuild-semantic" type="button" hidden>Rebuild semantic index</button></div></div></details>
|
||||
<form id="folder-form" hidden><label for="folder">Folder to index</label><div class="folder-row"><input id="folder" placeholder="Absolute folder path" required><button id="browse-folder" type="button">Browse…</button><button type="submit">Index folder</button><button id="cancel-folder" type="button">Cancel</button></div><p class="hint">Only choose folders you want searched. Large folders may take a while.</p></form>
|
||||
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button">${icon("open")}<span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder">${icon("folder")}</button></div></div><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
|
||||
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button">${icon("open")}<span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder">${icon("folder")}</button></div></div><details id="match-details" class="match-details"><summary>Why this result?</summary><p id="match-reason"></p><p id="match-legend"></p></details><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
|
||||
</main><footer><span class="footer-privacy">${icon("lock")}<span>Private search. Everything stays on your device.</span><span id="privacy" class="privacy">Local</span></span><span class="keyboard-hints"><kbd>${icon("arrowUp")}</kbd><kbd>${icon("arrowDown")}</kbd><span>Navigate</span><kbd>Enter</kbd><span>Open</span><kbd>Esc</kbd><span>Clear</span></span></footer>`;
|
||||
|
||||
const element = <T extends HTMLElement>(id: string) =>
|
||||
@@ -110,6 +124,8 @@ try {
|
||||
/* Diagnostics remain opt-in when storage is unavailable. */
|
||||
}
|
||||
let results: SearchResult[] = [];
|
||||
let displayedLiteralQuery = literalQuery("");
|
||||
let explainedSelection = "";
|
||||
let selected = -1;
|
||||
let requestId = 0;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
@@ -166,7 +182,7 @@ const mediaLane = new LatestLane<PreviewRequest>(
|
||||
const image = document.createElement("img");
|
||||
image.alt = request.result.filename;
|
||||
image.addEventListener("error", failed, { once: true });
|
||||
image.src = convertFileSrc(prepared.path);
|
||||
image.src = prepareMediaURL(prepared.path);
|
||||
host.append(image);
|
||||
} else {
|
||||
const player = document.createElement(prepared.kind);
|
||||
@@ -194,7 +210,7 @@ const mediaLane = new LatestLane<PreviewRequest>(
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
player.src = convertFileSrc(prepared.path);
|
||||
player.src = prepareMediaURL(prepared.path);
|
||||
host.append(player);
|
||||
}
|
||||
host.hidden = false;
|
||||
@@ -363,13 +379,14 @@ async function refreshSemanticStatus() {
|
||||
}
|
||||
const retry = element<HTMLButtonElement>("retry-model");
|
||||
retry.hidden = !error || status.ready;
|
||||
retry.disabled = working;
|
||||
retry.disabled = working || isCaptureSurface();
|
||||
const rebuild = element<HTMLButtonElement>("rebuild-semantic");
|
||||
rebuild.hidden = !status.ready || (!status.chunks && !status.pending);
|
||||
rebuild.disabled = working || indexingActive;
|
||||
rebuild.disabled = working || indexingActive || isCaptureSurface();
|
||||
element<HTMLSelectElement>("cpu-threads").disabled =
|
||||
working || indexingActive;
|
||||
working || indexingActive || isCaptureSurface();
|
||||
if (
|
||||
!isCaptureSurface() &&
|
||||
status.ready &&
|
||||
status.stale &&
|
||||
status.pending > 0 &&
|
||||
@@ -411,6 +428,7 @@ async function refreshDesktopStatus() {
|
||||
const threads = element<HTMLSelectElement>("cpu-threads");
|
||||
threads.value = String(status.settings.cpu_threads);
|
||||
threads.disabled =
|
||||
isCaptureSurface() ||
|
||||
status.indexing ||
|
||||
semanticJobActive ||
|
||||
Boolean(
|
||||
@@ -418,18 +436,19 @@ async function refreshDesktopStatus() {
|
||||
semanticStatus?.warming ||
|
||||
semanticStatus?.embedding,
|
||||
);
|
||||
element<HTMLButtonElement>("add-folder").disabled = status.indexing;
|
||||
element<HTMLButtonElement>("add-folder").disabled =
|
||||
status.indexing || isCaptureSurface();
|
||||
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.textContent = displayPath(root);
|
||||
path.className = "path";
|
||||
const reindex = document.createElement("button");
|
||||
reindex.type = "button";
|
||||
reindex.textContent = "Reindex";
|
||||
reindex.disabled = status.indexing;
|
||||
reindex.disabled = status.indexing || isCaptureSurface();
|
||||
reindex.addEventListener("click", () => {
|
||||
element("folder-form").hidden = false;
|
||||
element<HTMLInputElement>("folder").value = root;
|
||||
@@ -467,21 +486,57 @@ function select(index: number, focus = 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;
|
||||
renderHighlighted(
|
||||
element("preview-title"),
|
||||
result.filename,
|
||||
displayedLiteralQuery,
|
||||
);
|
||||
renderHighlighted(
|
||||
element("preview-path"),
|
||||
displayPath(result.path),
|
||||
displayedLiteralQuery,
|
||||
);
|
||||
element("preview-kind").textContent =
|
||||
`${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
|
||||
const media = mediaKind(result.filename);
|
||||
element("passage-label").textContent = media
|
||||
? "MATCHED CONTENT"
|
||||
: "EXTRACTED PASSAGE";
|
||||
element("preview-text").textContent =
|
||||
const explanation = matchExplanation(result);
|
||||
const semanticMedia = [
|
||||
"semantic_image",
|
||||
"semantic_video",
|
||||
"semantic_audio",
|
||||
].includes(result.match_kind);
|
||||
element("passage-label").textContent = semanticMedia
|
||||
? "MATCHED MEDIA"
|
||||
: explanation.semanticPassage
|
||||
? "MATCHED PASSAGE"
|
||||
: "EXTRACTED PASSAGE";
|
||||
const passageText =
|
||||
result.snippet ||
|
||||
(result.extraction_error
|
||||
? `Content could not be extracted: ${result.extraction_error}`
|
||||
: media
|
||||
? result.source?.matched_query || "Preview the matched media below."
|
||||
? "Preview the matched media below."
|
||||
: "No extracted text is available for this file.");
|
||||
renderHighlighted(
|
||||
element("preview-text"),
|
||||
passageText,
|
||||
semanticMedia ? literalQuery("") : displayedLiteralQuery,
|
||||
);
|
||||
document
|
||||
.querySelector<HTMLElement>(".passage")!
|
||||
.classList.toggle("semantic-passage", explanation.semanticPassage);
|
||||
element("match-reason").textContent = explanation.reason;
|
||||
element("match-legend").textContent = explanation.legend;
|
||||
const explanationKey = JSON.stringify([
|
||||
result.path,
|
||||
result.match_kind,
|
||||
result.source?.kind,
|
||||
result.source?.number,
|
||||
displayedLiteralQuery.query,
|
||||
]);
|
||||
if (explanationKey !== explainedSelection)
|
||||
element<HTMLDetailsElement>("match-details").open = false;
|
||||
explainedSelection = explanationKey;
|
||||
element<HTMLButtonElement>("open-result").title = result.source
|
||||
? `Open in your default application. The matched location ${sourceLabel(result.source)} is shown in Searchi; default application opening starts at its own location.`
|
||||
: "Open in your default application";
|
||||
@@ -491,7 +546,7 @@ function select(index: number, focus = false) {
|
||||
if (selection.changed) {
|
||||
mediaLane.clear();
|
||||
unloadMedia();
|
||||
if (media && isTauri()) {
|
||||
if (media && isDesktopSurface()) {
|
||||
element("media-preview-status").hidden = false;
|
||||
element("media-preview-status").textContent = "Preparing media preview…";
|
||||
mediaLane.submit({ revision: selection.revision, result });
|
||||
@@ -555,10 +610,23 @@ function render() {
|
||||
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.button.title = displayPath(result.path);
|
||||
renderHighlighted(entry.title, result.filename, displayedLiteralQuery);
|
||||
renderHighlighted(
|
||||
entry.path,
|
||||
displayPath(result.path),
|
||||
displayedLiteralQuery,
|
||||
);
|
||||
const semanticMedia = [
|
||||
"semantic_image",
|
||||
"semantic_video",
|
||||
"semantic_audio",
|
||||
].includes(result.match_kind);
|
||||
renderHighlighted(
|
||||
entry.snippet,
|
||||
displayExcerpt(result.snippet, displayedLiteralQuery),
|
||||
semanticMedia ? literalQuery("") : displayedLiteralQuery,
|
||||
);
|
||||
entry.kind.textContent = `${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
|
||||
const asset = fileIcon(result.filename);
|
||||
if (entry.file.dataset.type !== asset.type) {
|
||||
@@ -627,6 +695,7 @@ function applyResults(
|
||||
const resultsHadFocus = list.contains(document.activeElement);
|
||||
shownLane = lane;
|
||||
displayedRequestId = request.id;
|
||||
displayedLiteralQuery = literalQuery(request.query);
|
||||
results = found;
|
||||
selected = -1;
|
||||
render();
|
||||
@@ -643,7 +712,7 @@ function applyResults(
|
||||
const metric = request.measurement.latestNative(lane);
|
||||
const trace = request.measurement.export();
|
||||
element("result-summary").textContent =
|
||||
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · NATIVE ${briefMs(metric?.total_ms ?? 0)} MS · ${lane === "hybrid" ? "FINAL" : "FIRST RESPONSE"} ${Math.round(lane === "hybrid" ? (trace.hybrid_final_results_ms ?? 0) : (trace.first_results_ms ?? 0))} MS`;
|
||||
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · SEARCH ${briefMs(metric?.total_ms ?? 0)} MS · SHOWN ${briefMs(trace.latest_displayed_ms ?? 0)} MS`;
|
||||
refreshPerformance(request);
|
||||
}
|
||||
|
||||
@@ -801,6 +870,7 @@ async function search(inputAt = performance.now()) {
|
||||
if (!value) {
|
||||
element("performance-panel").hidden = true;
|
||||
results = [];
|
||||
displayedLiteralQuery = literalQuery("");
|
||||
displayedRequestId = null;
|
||||
selected = -1;
|
||||
render();
|
||||
@@ -1044,27 +1114,52 @@ document.addEventListener("keydown", (event) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (isTauri()) {
|
||||
void refreshStats().catch(showError);
|
||||
void refreshDesktopStatus().catch(showError);
|
||||
void refreshSemanticStatus();
|
||||
void listen("focus-search", () => {
|
||||
query.focus();
|
||||
query.select();
|
||||
async function initializeSurface() {
|
||||
await initializeDesktopSurface();
|
||||
if (isDesktopSurface()) {
|
||||
if (isCaptureSurface()) {
|
||||
for (const id of [
|
||||
"add-folder",
|
||||
"open-result",
|
||||
"reveal-result",
|
||||
"browse-folder",
|
||||
"retry-model",
|
||||
"rebuild-semantic",
|
||||
"cpu-threads",
|
||||
]) {
|
||||
const control = element<HTMLButtonElement>(id);
|
||||
control.disabled = true;
|
||||
control.title = "Unavailable in the read-only documentation capture";
|
||||
}
|
||||
}
|
||||
void refreshStats().catch(showError);
|
||||
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.");
|
||||
void refreshSemanticStatus();
|
||||
if (isNativeDesktop())
|
||||
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.");
|
||||
}
|
||||
}
|
||||
void initializeSurface().catch((error) => {
|
||||
element("index-status").textContent = "Capture unavailable";
|
||||
element("semantic-status").textContent =
|
||||
"The real local capture engine could not initialize.";
|
||||
showError(error);
|
||||
});
|
||||
Reference in new issue
Block a user