fix(svelte): replace $effect with onMount for side effects
$effect runs during SSR and re-runs on every reactive dependency change, causing polling loops and URL reads to fire at the wrong time. onMount runs once on the client after first render, which is the correct lifecycle for polling, URL param reads, and async data loads. - IndexingProgress: polling loop now starts on mount, not on reactive trigger - search/+page.svelte: URL param init moved to onMount; use window.location directly instead of the page store to avoid reactive re-runs - settings/+page.svelte: config load and local provider probe moved to onMount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import type { IndexingJob } from '$lib/types';
|
import type { IndexingJob } from '$lib/types';
|
||||||
|
|
||||||
let { jobId }: { jobId: string } = $props();
|
let { jobId }: { jobId: string } = $props();
|
||||||
|
|
||||||
let job = $state<IndexingJob | null>(null);
|
let job = $state<IndexingJob | null>(null);
|
||||||
|
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
// Reset and restart polling whenever jobId changes.
|
|
||||||
job = null;
|
job = null;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
|
||||||
@@ -23,13 +23,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
poll();
|
void poll();
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (job?.status === 'done' || job?.status === 'failed') {
|
if (job?.status === 'done' || job?.status === 'failed') {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
poll();
|
void poll();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
|
||||||
import LibraryResult from '$lib/components/search/LibraryResult.svelte';
|
import LibraryResult from '$lib/components/search/LibraryResult.svelte';
|
||||||
import SnippetCard from '$lib/components/search/SnippetCard.svelte';
|
import SnippetCard from '$lib/components/search/SnippetCard.svelte';
|
||||||
import SearchInput from '$lib/components/search/SearchInput.svelte';
|
import SearchInput from '$lib/components/search/SearchInput.svelte';
|
||||||
import { copyToClipboard } from '$lib/utils/copy-to-clipboard';
|
import { copyToClipboard } from '$lib/utils/copy-to-clipboard';
|
||||||
import type { LibrarySearchJsonResult, SnippetJson } from '$lib/server/api/formatters';
|
import type { LibrarySearchJsonResult, SnippetJson } from '$lib/server/api/formatters';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// State
|
// State
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
// Initialise from URL params on mount
|
// Initialise from URL params on mount
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
const libParam = page.url.searchParams.get('lib');
|
const libParam = new URL(window.location.href).searchParams.get('lib');
|
||||||
const qParam = page.url.searchParams.get('q');
|
const qParam = new URL(window.location.href).searchParams.get('q');
|
||||||
|
|
||||||
if (libParam) {
|
if (libParam) {
|
||||||
selectedLibraryId = libParam;
|
selectedLibraryId = libParam;
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
query = qParam;
|
query = qParam;
|
||||||
}
|
}
|
||||||
if (libParam && qParam) {
|
if (libParam && qParam) {
|
||||||
searchDocs();
|
void searchDocs(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchDocs() {
|
async function searchDocs(syncUrl = true) {
|
||||||
if (!selectedLibraryId || !query.trim()) return;
|
if (!selectedLibraryId || !query.trim()) return;
|
||||||
loadingSnippets = true;
|
loadingSnippets = true;
|
||||||
snippetError = null;
|
snippetError = null;
|
||||||
@@ -89,10 +89,11 @@
|
|||||||
totalTokens = 0;
|
totalTokens = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL('/api/v1/context', window.location.origin);
|
const params = new URLSearchParams({
|
||||||
url.searchParams.set('libraryId', selectedLibraryId);
|
libraryId: selectedLibraryId,
|
||||||
url.searchParams.set('query', query);
|
query
|
||||||
const res = await fetch(url);
|
});
|
||||||
|
const res = await fetch(`/api/v1/context?${params.toString()}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
throw new Error(data.error ?? `Request failed (${res.status})`);
|
throw new Error(data.error ?? `Request failed (${res.status})`);
|
||||||
@@ -101,11 +102,12 @@
|
|||||||
snippets = data.snippets ?? [];
|
snippets = data.snippets ?? [];
|
||||||
totalTokens = data.totalTokens ?? 0;
|
totalTokens = data.totalTokens ?? 0;
|
||||||
|
|
||||||
// Sync URL state.
|
if (syncUrl) {
|
||||||
goto(
|
goto(
|
||||||
`/search?lib=${encodeURIComponent(selectedLibraryId)}&q=${encodeURIComponent(query)}`,
|
`/search?lib=${encodeURIComponent(selectedLibraryId)}&q=${encodeURIComponent(query)}`,
|
||||||
{ replaceState: true, keepFocus: true }
|
{ replaceState: true, keepFocus: true }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
snippetError = (e as Error).message;
|
snippetError = (e as Error).message;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import StatBadge from '$lib/components/StatBadge.svelte';
|
import StatBadge from '$lib/components/StatBadge.svelte';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -43,15 +44,16 @@
|
|||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let saveStatus = $state<'idle' | 'ok' | 'error'>('idle');
|
let saveStatus = $state<'idle' | 'ok' | 'error'>('idle');
|
||||||
let saveError = $state<string | null>(null);
|
let saveError = $state<string | null>(null);
|
||||||
|
let saveStatusTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
let localAvailable = $state<boolean | null>(null);
|
let localAvailable = $state<boolean | null>(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Load current config + probe local provider on mount (Svelte 5 $effect)
|
// Load current config + probe local provider on mount
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -75,12 +77,11 @@
|
|||||||
|
|
||||||
// Probe whether the local provider is available
|
// Probe whether the local provider is available
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/embedding/test', {
|
const res = await fetch('/api/v1/settings/embedding/test');
|
||||||
method: 'POST',
|
if (!cancelled && res.ok) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const data = await res.json();
|
||||||
body: JSON.stringify({ provider: 'local' })
|
localAvailable = data.available ?? false;
|
||||||
});
|
}
|
||||||
if (!cancelled) localAvailable = res.ok;
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) localAvailable = false;
|
if (!cancelled) localAvailable = false;
|
||||||
}
|
}
|
||||||
@@ -88,19 +89,10 @@
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
if (saveStatusTimer) clearTimeout(saveStatusTimer);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-dismiss save success banner after 3 seconds
|
|
||||||
$effect(() => {
|
|
||||||
if (saveStatus === 'ok') {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
saveStatus = 'idle';
|
|
||||||
}, 3000);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Actions
|
// Actions
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -151,6 +143,11 @@
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
saveStatus = 'ok';
|
saveStatus = 'ok';
|
||||||
|
if (saveStatusTimer) clearTimeout(saveStatusTimer);
|
||||||
|
saveStatusTimer = setTimeout(() => {
|
||||||
|
saveStatus = 'idle';
|
||||||
|
saveStatusTimer = null;
|
||||||
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
saveStatus = 'error';
|
saveStatus = 'error';
|
||||||
@@ -163,6 +160,11 @@
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
void save();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -184,10 +186,12 @@
|
|||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="text-sm text-gray-400">Loading current configuration…</p>
|
<p class="text-sm text-gray-400">Loading current configuration…</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
<form class="space-y-4" onsubmit={handleSubmit}>
|
||||||
<!-- Provider selector -->
|
<!-- Provider selector -->
|
||||||
<div class="mb-4 flex gap-2">
|
<div class="mb-4 flex gap-2">
|
||||||
{#each ['none', 'openai', 'local'] as p}
|
{#each ['none', 'openai', 'local'] as p}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
provider = p as 'none' | 'openai' | 'local';
|
provider = p as 'none' | 'openai' | 'local';
|
||||||
testStatus = 'idle';
|
testStatus = 'idle';
|
||||||
@@ -219,6 +223,7 @@
|
|||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each PROVIDER_PRESETS as preset}
|
{#each PROVIDER_PRESETS as preset}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onclick={() => applyPreset(preset)}
|
onclick={() => applyPreset(preset)}
|
||||||
class="rounded border border-gray-200 px-2.5 py-1 text-xs text-gray-600 hover:bg-gray-50"
|
class="rounded border border-gray-200 px-2.5 py-1 text-xs text-gray-600 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
@@ -227,38 +232,50 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="block">
|
<label class="block" for="embedding-base-url">
|
||||||
<span class="text-sm font-medium text-gray-700">Base URL</span>
|
<span class="text-sm font-medium text-gray-700">Base URL</span>
|
||||||
<input
|
<input
|
||||||
|
id="embedding-base-url"
|
||||||
|
name="baseUrl"
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="url"
|
||||||
bind:value={baseUrl}
|
bind:value={baseUrl}
|
||||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="block">
|
<label class="block" for="embedding-api-key">
|
||||||
<span class="text-sm font-medium text-gray-700">API Key</span>
|
<span class="text-sm font-medium text-gray-700">API Key</span>
|
||||||
<input
|
<input
|
||||||
|
id="embedding-api-key"
|
||||||
|
name="apiKey"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
bind:value={apiKey}
|
bind:value={apiKey}
|
||||||
placeholder="sk-…"
|
placeholder="sk-…"
|
||||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="block">
|
<label class="block" for="embedding-model">
|
||||||
<span class="text-sm font-medium text-gray-700">Model</span>
|
<span class="text-sm font-medium text-gray-700">Model</span>
|
||||||
<input
|
<input
|
||||||
|
id="embedding-model"
|
||||||
|
name="model"
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
bind:value={model}
|
bind:value={model}
|
||||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="block">
|
<label class="block" for="embedding-dimensions">
|
||||||
<span class="text-sm font-medium text-gray-700">Dimensions (optional override)</span>
|
<span class="text-sm font-medium text-gray-700">Dimensions (optional override)</span>
|
||||||
<input
|
<input
|
||||||
|
id="embedding-dimensions"
|
||||||
|
name="dimensions"
|
||||||
type="number"
|
type="number"
|
||||||
|
inputmode="numeric"
|
||||||
bind:value={dimensions}
|
bind:value={dimensions}
|
||||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
@@ -267,6 +284,7 @@
|
|||||||
<!-- Test connection row -->
|
<!-- Test connection row -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onclick={testConnection}
|
onclick={testConnection}
|
||||||
disabled={testStatus === 'testing'}
|
disabled={testStatus === 'testing'}
|
||||||
class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50 disabled:opacity-50"
|
class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50 disabled:opacity-50"
|
||||||
@@ -353,13 +371,14 @@
|
|||||||
<!-- Save row -->
|
<!-- Save row -->
|
||||||
<div class="mt-4 flex items-center justify-end">
|
<div class="mt-4 flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
onclick={save}
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? 'Saving…' : 'Save Settings'}
|
{saving ? 'Saving…' : 'Save Settings'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user