Expose exact music filters in desktop and local agent clients
This commit is contained in:
1 parent
2eace15a33
commit
c33047998b
6 files changed
+263
-35
No files matched your search
+28
-18
@@ -15,6 +15,7 @@ import {
|
||||
LatestLane,
|
||||
semanticAvailable,
|
||||
scopedFilters,
|
||||
hasSearchInput,
|
||||
SEMANTIC_SETTLE_MS,
|
||||
} from "./search-state";
|
||||
import type { SemanticPhase, SearchFilters } from "./search-state";
|
||||
@@ -146,7 +147,7 @@ interface SearchRequest {
|
||||
}
|
||||
|
||||
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
||||
<header><div class="brand"><img class="brand-symbol" src="/searchi-symbol.png" alt=""><span class="wordmark">searchi</span></div><span id="index-status" class="index-status">Loading your index…</span><div class="header-actions"><button id="add-folder" class="quiet-button" type="button">${icon("folderAdd")}<span>Add folder</span></button><details id="filters" class="popover"><summary>${icon("filter")}<span id="scope-label">All files</span>${icon("chevron")}</summary><div class="popover-panel"><h2>Filter your search</h2><div class="filters-grid"><label>Search scope<select id="filter-scope"><option value="all">All files</option><option value="code">Code</option></select></label><label>Within folder<input id="filter-folder" placeholder="Absolute folder path"></label><label>Extension<input id="filter-extension" placeholder="pdf, md, txt…"></label><label>Minimum size (bytes)<input id="filter-min" type="number" min="0" step="1"></label><label>Maximum size (bytes)<input id="filter-max" type="number" min="0" step="1"></label><label>Minimum duration (seconds)<input id="filter-duration-min" type="number" min="0" step="0.1"></label><label>Maximum duration (seconds)<input id="filter-duration-max" type="number" min="0" step="0.1"></label><label>Modified from<input id="filter-after" type="date"></label><label>Modified through<input id="filter-before" type="date"></label></div></div></details><details id="settings" class="popover"><summary class="icon-button" aria-label="Settings" title="Settings">${icon("settings")}</summary><div class="popover-panel"><h2>Settings</h2><section class="appearance-settings" aria-labelledby="appearance-heading"><h3 id="appearance-heading">Appearance</h3><label for="background-material">Window background</label><select id="background-material"><option value="solid">Solid</option><option value="glass">Glass</option><option value="frosted">Frosted glass</option></select><div class="appearance-opacity"><label for="background-opacity">Background tint</label><output id="background-opacity-value" for="background-opacity">90%</output></div><input id="background-opacity" type="range" min="70" max="100" step="1" value="90" aria-describedby="appearance-status"><p id="appearance-status" class="hint" role="status">A subtle window background. Search results and passages stay clear.</p></section><h3>Folders & resources</h3><p id="shortcut-status" class="hint"></p><label class="resource-label" for="cpu-threads">CPU thread budget</label><select id="cpu-threads">${Array.from({ length: 8 }, (_, i) => `<option value="${i + 1}">${i + 1} ${i === 1 ? "threads (default)" : i ? "threads" : "thread (low power)"}</option>`).join("")}</select><p class="hint">Search automatically uses supported acceleration with CPU fallback. Two or more threads let search run alongside indexing; one thread can delay semantic search during indexing.</p><ul id="roots" aria-label="Selected folders"></ul><label class="performance-toggle"><input id="performance-toggle" type="checkbox">Performance details</label><p class="hint">Local timing diagnostics for new searches. Queries and file paths are excluded from copied reports.</p><details id="ai-settings" class="ai-settings"><summary>Optional AI answers</summary><label class="performance-toggle"><input id="ai-enabled" type="checkbox">Enable summaries and questions</label><p class="hint">Disabled by default. Only an explicit action sends the shown passage to your configured endpoint.</p><label>OpenAI-compatible base URL<input id="ai-endpoint" type="url" placeholder="http://localhost:1234/v1" autocomplete="off" maxlength="2048"></label><label>Model<input id="ai-model" placeholder="Your model name" autocomplete="off" maxlength="200"></label><label>API key (optional, this session only)<input id="ai-key" type="password" autocomplete="off" maxlength="2048"></label><button id="ai-clear-key" type="button">Clear key</button><p class="hint">Endpoint, model and key stay in this app session. The key authenticates requests to your endpoint.</p></details></div></details></div></header>
|
||||
<header><div class="brand"><img class="brand-symbol" src="/searchi-symbol.png" alt=""><span class="wordmark">searchi</span></div><span id="index-status" class="index-status">Loading your index…</span><div class="header-actions"><button id="add-folder" class="quiet-button" type="button">${icon("folderAdd")}<span>Add folder</span></button><details id="filters" class="popover"><summary>${icon("filter")}<span id="scope-label">All files</span>${icon("chevron")}</summary><div class="popover-panel"><h2>Filter your search</h2><div class="filters-grid"><label>Search scope<select id="filter-scope"><option value="all">All files</option><option value="code">Code</option></select></label><label>Within folder<input id="filter-folder" placeholder="Absolute folder path"></label><label>Extension<input id="filter-extension" placeholder="pdf, md, txt…"></label><label>Artist (exact)<input id="filter-artist" placeholder="Artist from music tags" maxlength="256"></label><label>Track title (exact)<input id="filter-title" placeholder="Title from music tags" maxlength="256"></label><label>Minimum size (bytes)<input id="filter-min" type="number" min="0" step="1"></label><label>Maximum size (bytes)<input id="filter-max" type="number" min="0" step="1"></label><label>Minimum duration (seconds)<input id="filter-duration-min" type="number" min="0" step="0.1"></label><label>Maximum duration (seconds)<input id="filter-duration-max" type="number" min="0" step="0.1"></label><label>Modified from<input id="filter-after" type="date"></label><label>Modified through<input id="filter-before" type="date"></label></div></div></details><details id="settings" class="popover"><summary class="icon-button" aria-label="Settings" title="Settings">${icon("settings")}</summary><div class="popover-panel"><h2>Settings</h2><section class="appearance-settings" aria-labelledby="appearance-heading"><h3 id="appearance-heading">Appearance</h3><label for="background-material">Window background</label><select id="background-material"><option value="solid">Solid</option><option value="glass">Glass</option><option value="frosted">Frosted glass</option></select><div class="appearance-opacity"><label for="background-opacity">Background tint</label><output id="background-opacity-value" for="background-opacity">90%</output></div><input id="background-opacity" type="range" min="70" max="100" step="1" value="90" aria-describedby="appearance-status"><p id="appearance-status" class="hint" role="status">A subtle window background. Search results and passages stay clear.</p></section><h3>Folders & resources</h3><p id="shortcut-status" class="hint"></p><label class="resource-label" for="cpu-threads">CPU thread budget</label><select id="cpu-threads">${Array.from({ length: 8 }, (_, i) => `<option value="${i + 1}">${i + 1} ${i === 1 ? "threads (default)" : i ? "threads" : "thread (low power)"}</option>`).join("")}</select><p class="hint">Search automatically uses supported acceleration with CPU fallback. Two or more threads let search run alongside indexing; one thread can delay semantic search during indexing.</p><ul id="roots" aria-label="Selected folders"></ul><label class="performance-toggle"><input id="performance-toggle" type="checkbox">Performance details</label><p class="hint">Local timing diagnostics for new searches. Queries and file paths are excluded from copied reports.</p><details id="ai-settings" class="ai-settings"><summary>Optional AI answers</summary><label class="performance-toggle"><input id="ai-enabled" type="checkbox">Enable summaries and questions</label><p class="hint">Disabled by default. Only an explicit action sends the shown passage to your configured endpoint.</p><label>OpenAI-compatible base URL<input id="ai-endpoint" type="url" placeholder="http://localhost:1234/v1" autocomplete="off" maxlength="2048"></label><label>Model<input id="ai-model" placeholder="Your model name" autocomplete="off" maxlength="200"></label><label>API key (optional, this session only)<input id="ai-key" type="password" autocomplete="off" maxlength="2048"></label><button id="ai-clear-key" type="button">Clear key</button><p class="hint">Endpoint, model and key stay in this app session. The key authenticates requests to your endpoint.</p></details></div></details></div></header>
|
||||
<main>
|
||||
<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>
|
||||
@@ -305,6 +306,13 @@ const automaticIndex = new AutomaticIndexGate();
|
||||
|
||||
function refreshEmptySearchState() {
|
||||
if (!latestSearch || latestSearch.id !== requestId || results.length) return;
|
||||
if (!latestSearch.query.trim()) {
|
||||
element("empty").querySelector("h2")!.textContent =
|
||||
"No matching music tags.";
|
||||
element("empty").querySelector("p")!.textContent =
|
||||
"Try the complete artist or track title from your files. Missing tags cannot match these filters.";
|
||||
return;
|
||||
}
|
||||
let phase: SemanticPhase;
|
||||
if (querySemanticFailed || localSemanticError || semanticStatus?.error)
|
||||
phase = "unavailable";
|
||||
@@ -1042,6 +1050,8 @@ function captureFilters() {
|
||||
return scopedFilters(element<HTMLSelectElement>("filter-scope").value, {
|
||||
folder: input("filter-folder") || null,
|
||||
extension: input("filter-extension") || null,
|
||||
media_artist: input("filter-artist") || null,
|
||||
media_title: input("filter-title") || null,
|
||||
min_duration_ms: duration("filter-duration-min"),
|
||||
max_duration_ms: duration("filter-duration-max"),
|
||||
min_size: size("filter-min"),
|
||||
@@ -1236,7 +1246,7 @@ function refreshIndexedSearch(
|
||||
};
|
||||
latestSearch = refreshed;
|
||||
submitSearch(refreshed, "lexical");
|
||||
if (canSearchSemantically()) {
|
||||
if (refreshed.query.trim() && canSearchSemantically()) {
|
||||
hybridTimer = setTimeout(() => {
|
||||
if (isCurrent(refreshed)) submitSearch(refreshed, "hybrid");
|
||||
}, SEMANTIC_SETTLE_MS);
|
||||
@@ -1287,23 +1297,23 @@ async function search(inputAt = performance.now()) {
|
||||
querySemanticFailed = false;
|
||||
shownLane = "lexical";
|
||||
const value = query.value.trim();
|
||||
if (!value) {
|
||||
resultSelection.begin("");
|
||||
element("performance-panel").hidden = true;
|
||||
results = [];
|
||||
displayedLiteralQuery = literalQuery("");
|
||||
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 filters = captureFilters();
|
||||
if (!hasSearchInput(value, filters)) {
|
||||
resultSelection.begin("");
|
||||
element("performance-panel").hidden = true;
|
||||
results = [];
|
||||
displayedLiteralQuery = literalQuery("");
|
||||
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;
|
||||
}
|
||||
resultSelection.begin(JSON.stringify([value, filters]));
|
||||
const request: SearchRequest = {
|
||||
id: requestId,
|
||||
@@ -1322,7 +1332,7 @@ async function search(inputAt = performance.now()) {
|
||||
element("result-summary").textContent = "SEARCHING…";
|
||||
submitSearch(request, "lexical");
|
||||
hybridTimer = setTimeout(() => {
|
||||
if (isCurrent(request) && canSearchSemantically())
|
||||
if (value && isCurrent(request) && canSearchSemantically())
|
||||
submitSearch(request, "hybrid");
|
||||
}, SEMANTIC_SETTLE_MS);
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,6 +11,16 @@ export function scopedFilters(
|
||||
return Object.freeze({ ...filters, code_only: scope === "code" });
|
||||
}
|
||||
|
||||
/** Exact music metadata can be searched without embedding an empty query. */
|
||||
export function hasSearchInput(query: string, filters: SearchFilters): boolean {
|
||||
return Boolean(
|
||||
query.trim() ||
|
||||
(typeof filters.media_artist === "string" &&
|
||||
filters.media_artist.trim()) ||
|
||||
(typeof filters.media_title === "string" && filters.media_title.trim()),
|
||||
);
|
||||
}
|
||||
|
||||
/** Let rapid typing settle before starting the model lane. */
|
||||
export const SEMANTIC_SETTLE_MS = 120;
|
||||
|
||||
|
||||
Reference in new issue
Block a user