Prepare text search during media setup and retain query sessions through promotion
This commit is contained in:
1 parent
af6ce29125
commit
9aae3f8216
15 files changed
+1235
-65
No files matched your search
+41
-11
@@ -16,6 +16,13 @@ import {
|
||||
SEMANTIC_SETTLE_MS,
|
||||
} from "./search-state";
|
||||
import type { SemanticPhase, SearchFilters } from "./search-state";
|
||||
import {
|
||||
semanticIndexReady,
|
||||
semanticIndexKey,
|
||||
semanticRetryVisible,
|
||||
semanticMediaReady,
|
||||
} from "./semantic-setup-state";
|
||||
import type { SemanticCoverage } from "./semantic-setup-state";
|
||||
import { fileIcon, icon } from "./icons";
|
||||
import {
|
||||
SearchMeasurement,
|
||||
@@ -102,6 +109,12 @@ interface SemanticStatus {
|
||||
stale: boolean;
|
||||
pending: number;
|
||||
warming: boolean;
|
||||
text_model_ready?: boolean;
|
||||
media_model_ready?: boolean;
|
||||
media_model_error?: string | null;
|
||||
indexing_ready?: boolean;
|
||||
requested_coverage?: SemanticCoverage;
|
||||
coverage?: SemanticCoverage | null;
|
||||
media_tools_ready?: boolean;
|
||||
media_setup_error?: string | null;
|
||||
media_failures?: { path: string; message: string }[];
|
||||
@@ -160,6 +173,7 @@ let latestSearch: SearchRequest | null = null;
|
||||
let shownLane: "lexical" | "hybrid" = "lexical";
|
||||
let folderRevision = 0;
|
||||
let localSemanticError: string | null = null;
|
||||
let localModelSetupError: string | null = null;
|
||||
let querySemanticFailed = false;
|
||||
let previewPath: string | null = null;
|
||||
const previewRevision = new PreviewRevision();
|
||||
@@ -297,6 +311,7 @@ function queueSemanticRefresh(delay = 1000) {
|
||||
|
||||
async function buildSemanticIndex() {
|
||||
if (semanticJobActive || indexingActive) return;
|
||||
if (semanticStatus && !semanticIndexReady(semanticStatus)) return;
|
||||
semanticJobActive = true;
|
||||
localSemanticError = null;
|
||||
element("semantic-status").textContent =
|
||||
@@ -330,7 +345,9 @@ async function refreshSemanticStatus() {
|
||||
const status = await invoke<SemanticStatus>("semantic_status");
|
||||
semanticStatus = status;
|
||||
refreshEmptySearchState();
|
||||
const error = localSemanticError || status.error;
|
||||
const setupError =
|
||||
status.ready && status.media_model_error ? null : localModelSetupError;
|
||||
const error = localSemanticError || status.error || setupError;
|
||||
if (error && error !== expandedError) {
|
||||
element<HTMLDetailsElement>("semantic-panel").open = true;
|
||||
expandedError = error;
|
||||
@@ -363,13 +380,15 @@ async function refreshSemanticStatus() {
|
||||
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`;
|
||||
status.ready && status.text_model_ready
|
||||
? `${semanticAvailable(status) ? "Text search ready" : "Text model ready"} · Preparing media model · ${Math.round(status.downloaded_bytes / 1_000_000)} of ${Math.round(status.total_bytes / 1_000_000)} MB`
|
||||
: 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)
|
||||
else if (semanticAvailable(status) && status.chunks > 0)
|
||||
message = `Semantic search ready · ${status.chunks.toLocaleString()} content segments${status.stale ? " · Updates pending" : ""}${status.media_failures?.length ? ` · ${status.media_failures.length} files could not be indexed` : ""}`;
|
||||
else if (status.ready)
|
||||
message = "Google model ready. Index a folder to search by meaning.";
|
||||
@@ -385,20 +404,29 @@ async function refreshSemanticStatus() {
|
||||
? `Media decoder setup needs attention: ${status.media_setup_error}. Text search remains available.`
|
||||
: status.worker_info && !status.worker_info.media_supported
|
||||
? "The installed worker needs a media-capable runtime update. Text search remains available."
|
||||
: status.media_tools_ready && status.worker_info?.media_supported
|
||||
: semanticMediaReady(status) &&
|
||||
status.media_tools_ready &&
|
||||
status.worker_info?.media_supported
|
||||
? "Image, audio, and video encoders are available locally."
|
||||
: "Preparing local image, audio, and video support.";
|
||||
element("semantic-detail").textContent += ` ${mediaDetail}`;
|
||||
if (status.media_model_error)
|
||||
element("semantic-detail").textContent +=
|
||||
` Media model setup needs attention: ${status.media_model_error}. Text search remains available.`;
|
||||
if (status.media_failures?.length) {
|
||||
element("semantic-detail").textContent +=
|
||||
` ${status.media_failures.length} files could not be indexed. The first issue: ${status.media_failures[0].path}: ${status.media_failures[0].message}. Filename search remains available; rebuild after fixing the file.`;
|
||||
}
|
||||
const retry = element<HTMLButtonElement>("retry-model");
|
||||
retry.hidden = !error || status.ready;
|
||||
retry.hidden = !semanticRetryVisible({ ...status, error });
|
||||
retry.disabled = working || isCaptureSurface();
|
||||
const rebuild = element<HTMLButtonElement>("rebuild-semantic");
|
||||
rebuild.hidden = !status.ready || (!status.chunks && !status.pending);
|
||||
rebuild.disabled = working || indexingActive || isCaptureSurface();
|
||||
rebuild.disabled =
|
||||
!semanticIndexReady(status) ||
|
||||
semanticJobActive ||
|
||||
indexingActive ||
|
||||
isCaptureSurface();
|
||||
element<HTMLSelectElement>("cpu-threads").disabled =
|
||||
working || indexingActive || isCaptureSurface();
|
||||
if (
|
||||
@@ -406,10 +434,11 @@ async function refreshSemanticStatus() {
|
||||
status.ready &&
|
||||
status.stale &&
|
||||
status.pending > 0 &&
|
||||
!working &&
|
||||
semanticIndexReady(status) &&
|
||||
!semanticJobActive &&
|
||||
!indexingActive
|
||||
) {
|
||||
const key = `${status.generation}:${status.pending}:${folderRevision}`;
|
||||
const key = semanticIndexKey(status, folderRevision);
|
||||
if (automaticIndex.claim(key)) {
|
||||
void buildSemanticIndex();
|
||||
return;
|
||||
@@ -1276,12 +1305,13 @@ element("retry-model").addEventListener("click", async () => {
|
||||
const button = element<HTMLButtonElement>("retry-model");
|
||||
button.disabled = true;
|
||||
localSemanticError = null;
|
||||
localModelSetupError = null;
|
||||
element("semantic-status").textContent = "Retrying Google model setup…";
|
||||
queueSemanticRefresh();
|
||||
try {
|
||||
await invoke("setup_model");
|
||||
} catch (error) {
|
||||
localSemanticError = String(error);
|
||||
localModelSetupError = String(error);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
queueSemanticRefresh(0);
|
||||
|
||||
@@ -17,9 +17,15 @@ export const SEMANTIC_SETTLE_MS = 120;
|
||||
/** A rebuild retains the previous committed generation. Backend readiness is
|
||||
* withdrawn only while a replacement query graph is being prepared. */
|
||||
export function semanticAvailable(
|
||||
status: { ready: boolean; generation: number | null } | null,
|
||||
status: {
|
||||
ready: boolean;
|
||||
generation: number | null;
|
||||
coverage?: string | null;
|
||||
} | null,
|
||||
) {
|
||||
return Boolean(status?.ready && status.generation !== null);
|
||||
return Boolean(
|
||||
status?.ready && status.generation !== null && status.coverage !== null,
|
||||
);
|
||||
}
|
||||
|
||||
/** One active request per lane; intermediate keystrokes collapse to the latest.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Setup and search readiness are independent: media downloads need not block text. */
|
||||
export type SemanticCoverage = "text_only" | "full_media";
|
||||
|
||||
export interface SemanticSetupStatus {
|
||||
ready: boolean;
|
||||
generation: number | null;
|
||||
pending: number;
|
||||
downloading: boolean;
|
||||
warming: boolean;
|
||||
embedding: boolean;
|
||||
error: string | null;
|
||||
indexing_ready?: boolean;
|
||||
text_model_ready?: boolean;
|
||||
media_model_ready?: boolean;
|
||||
media_model_error?: string | null;
|
||||
requested_coverage?: SemanticCoverage;
|
||||
}
|
||||
|
||||
export function semanticIndexReady(status: SemanticSetupStatus) {
|
||||
return Boolean(
|
||||
status.ready &&
|
||||
!status.warming &&
|
||||
!status.embedding &&
|
||||
(status.indexing_ready ?? !status.downloading),
|
||||
);
|
||||
}
|
||||
|
||||
/** Promotion must receive its own automatic attempt even with unchanged counts. */
|
||||
export function semanticIndexKey(
|
||||
status: SemanticSetupStatus,
|
||||
folderRevision: number,
|
||||
) {
|
||||
return `${status.generation}:${status.pending}:${folderRevision}:${status.requested_coverage ?? "full_media"}`;
|
||||
}
|
||||
|
||||
export function semanticRetryVisible(status: SemanticSetupStatus) {
|
||||
return Boolean(status.media_model_error || (status.error && !status.ready));
|
||||
}
|
||||
|
||||
export function semanticMediaReady(status: SemanticSetupStatus) {
|
||||
// Legacy capture fixtures and older full-pair status did not expose this field.
|
||||
return status.media_model_ready ?? (status.ready && !status.downloading);
|
||||
}
|
||||
Reference in new issue
Block a user