summaryrefslogtreecommitdiffhomepage
path: root/src/features/search-settings/ui/SearchBackendField.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-09-24 19:20:37 +0900
committerAdam Malczewski <[email protected]>2026-09-24 19:20:37 +0900
commit591e2dbe5cbf2283d58debdf92943a075edc4569 (patch)
tree11ee2481ed79f257cd6303f44992471ec9282237 /src/features/search-settings/ui/SearchBackendField.svelte
parent6556e70e396ec7cc9b2311ef60e00c81722777c7 (diff)
downloaddispatch-web-591e2dbe5cbf2283d58debdf92943a075edc4569.tar.gz
dispatch-web-591e2dbe5cbf2283d58debdf92943a075edc4569.zip
feat(search-backend-selection): Search backend dropdown in Settings
- New features/search-settings: pure view-model (options = available backends ∪ 'None (search disabled)', unconfigured entries disabled with a suffix, unknown persisted selection prepended, network-seam normalization) + the SearchBackendField dropdown (save-on-change, spinner/saved/error states, revert-to-persisted on failure, 503 -> disabled field with a 'search not available' hint, loading skeleton, help text noting search-mode-only scope). - store.svelte.ts: searchBackendConfig() / setSearchBackend() over the global GET/PUT /search/config routes (503 marked unavailable on the failure variant). - SettingsPane renders the field under the chat limit; ExtensionsPane lists the new module manifest. - .dispatch mirrors: wire + transport-contract deltas for the new types/routes.
Diffstat (limited to 'src/features/search-settings/ui/SearchBackendField.svelte')
-rw-r--r--src/features/search-settings/ui/SearchBackendField.svelte142
1 files changed, 142 insertions, 0 deletions
diff --git a/src/features/search-settings/ui/SearchBackendField.svelte b/src/features/search-settings/ui/SearchBackendField.svelte
new file mode 100644
index 0000000..c1ae450
--- /dev/null
+++ b/src/features/search-settings/ui/SearchBackendField.svelte
@@ -0,0 +1,142 @@
+<script lang="ts">
+ /**
+ * The "Search backend" dropdown (Settings pane) — picks the engine backing
+ * the `web_search` tool's SEARCH mode. GLOBAL setting persisted on the
+ * backend (`GET/PUT /search/config`); SAVE ON CHANGE (no Set button).
+ * Unconfigured backends stay listed but disabled; "None (search disabled)"
+ * is the off state. The config affects search mode only — scrape/crawl
+ * always route to the self-hosted Firecrawl backend.
+ *
+ * Thin shell: seeds/reverts the select from the last PERSISTED value, shows
+ * saving/saved/error feedback, and degrades to a disabled field with a
+ * "search not available" hint when the backend's search-config service is
+ * not loaded (503) — matching heartbeat's degrade philosophy.
+ */
+ import { untrack } from "svelte";
+ import {
+ buildSearchBackendOptions,
+ type LoadSearchBackendConfig,
+ NONE_BACKEND_ID,
+ type SaveSearchBackend,
+ searchBackendChanged,
+ searchBackendLabel,
+ type SearchBackendConfig,
+ } from "../logic/view-model";
+
+ let {
+ load,
+ save,
+ }: {
+ /** Fetch the persisted global config (called once on mount). */
+ load: LoadSearchBackendConfig;
+ /** Persist a new backend selection (save-on-change). */
+ save: SaveSearchBackend;
+ } = $props();
+
+ let config = $state<SearchBackendConfig | null>(null);
+ let loading = $state(true);
+ let loadUnavailable = $state(false);
+ let loadError = $state<string | null>(null);
+ let selected = $state("");
+ let lastSeed = $state("");
+ let saving = $state(false);
+ let error = $state<string | null>(null);
+ let justSaved = $state<string | null>(null);
+
+ $effect(() => {
+ // Load ONCE on first mount (the setting is GLOBAL — no workspace
+ // re-seeding); guard against strict-mode double effects via the flag.
+ untrack(async () => {
+ if (config !== null || loadUnavailable) return;
+ loading = true;
+ loadError = null;
+ const result = await load();
+ loading = false;
+ if (result.ok) {
+ config = result.config;
+ selected = result.config.backend;
+ lastSeed = result.config.backend;
+ } else {
+ loadUnavailable = result.unavailable === true;
+ loadError = result.error;
+ }
+ });
+ });
+
+ const options = $derived(config === null ? [] : buildSearchBackendOptions(config));
+ const persisted = $derived(lastSeed);
+
+ async function handleChange(): Promise<void> {
+ if (saving || config === null) return;
+ const next = selected;
+ if (!searchBackendChanged(next, persisted)) return;
+ saving = true;
+ error = null;
+ justSaved = null;
+ const result = await save(next);
+ saving = false;
+ if (result.ok) {
+ config = result.config;
+ // Reflect the persisted value back (the backend is authoritative).
+ selected = result.config.backend;
+ lastSeed = result.config.backend;
+ justSaved = searchBackendLabel(result.config, result.config.backend);
+ } else {
+ error = result.error;
+ // Revert the select to the last persisted value on failure.
+ selected = persisted;
+ }
+ }
+
+ function onSelectChange(): void {
+ error = null;
+ justSaved = null;
+ void handleChange();
+ }
+</script>
+
+<div class="flex flex-col gap-1">
+ <span class="text-xs font-semibold uppercase opacity-60">Search backend</span>
+ {#if loading}
+ <div class="skeleton h-7 w-full" aria-label="Search backend loading"></div>
+ {:else if config === null}
+ <select
+ class="select select-bordered select-sm w-full text-xs"
+ disabled
+ aria-label="Search backend"
+ >
+ <option>{loadUnavailable ? "Search not available" : (loadError ?? "Unavailable")}</option>
+ </select>
+ {#if loadError && !loadUnavailable}
+ <p class="text-xs text-error">{loadError}</p>
+ {/if}
+ {:else}
+ <select
+ class="select select-bordered select-sm w-full text-xs"
+ bind:value={selected}
+ disabled={saving}
+ onchange={onSelectChange}
+ aria-label="Search backend"
+ >
+ {#each options as option (option.id)}
+ <option value={option.id} disabled={option.disabled}>{option.label}</option>
+ {/each}
+ </select>
+ {#if saving}
+ <p class="text-xs opacity-60 flex items-center gap-1">
+ <span class="loading loading-spinner loading-xs"></span>
+ Saving…
+ </p>
+ {:else if error}
+ <p class="text-xs text-error">{error}</p>
+ {:else if justSaved !== null}
+ <p class="text-xs text-success">Saved: {justSaved}.</p>
+ {/if}
+ {/if}
+ {#if !loadUnavailable}
+ <p class="text-xs opacity-50">
+ Affects the web_search tool's <em>search</em> mode only — scrape and crawl always use the
+ self-hosted Firecrawl backend, and the tool's map mode has been removed.
+ </p>
+ {/if}
+</div>