feat(TRUEREF-0023): add sqlite-vec search pipeline

This commit is contained in:
Giancarmine Salucci
2026-04-01 14:09:19 +02:00
parent 0752636847
commit 9525c58e9a
45 changed files with 4009 additions and 614 deletions

View File

@@ -0,0 +1,19 @@
<script lang="ts">
let { rows = 5 }: { rows?: number } = $props();
</script>
{#each Array(rows) as _, i (i)}
<tr>
<td class="px-6 py-4">
<div class="h-4 w-48 animate-pulse rounded bg-gray-200"></div>
<div class="mt-1 h-3 w-24 animate-pulse rounded bg-gray-100"></div>
</td>
<td class="px-6 py-4"><div class="h-5 w-16 animate-pulse rounded-full bg-gray-200"></div></td>
<td class="px-6 py-4"><div class="h-4 w-20 animate-pulse rounded bg-gray-200"></div></td>
<td class="px-6 py-4"><div class="h-2 w-32 animate-pulse rounded-full bg-gray-200"></div></td>
<td class="px-6 py-4"><div class="h-4 w-28 animate-pulse rounded bg-gray-200"></div></td>
<td class="px-6 py-4 text-right"
><div class="ml-auto h-7 w-20 animate-pulse rounded bg-gray-200"></div></td
>
</tr>
{/each}

View File

@@ -1,9 +1,10 @@
<script lang="ts">
interface Props {
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
spinning?: boolean;
}
let { status }: Props = $props();
let { status, spinning = false }: Props = $props();
const statusConfig: Record<typeof status, { bg: string; text: string; label: string }> = {
queued: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Queued' },
@@ -21,4 +22,9 @@
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {config.bg} {config.text}"
>
{config.label}
{#if spinning}
<span
class="ml-1 inline-block h-3 w-3 animate-spin rounded-full border-2 border-current border-r-transparent"
></span>
{/if}
</span>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { onDestroy } from 'svelte';
export interface ToastItem {
id: string;
message: string;
type: 'success' | 'error' | 'info';
}
let { toasts = $bindable([]) }: { toasts: ToastItem[] } = $props();
const timers = new Map<string, ReturnType<typeof setTimeout>>();
$effect(() => {
for (const toast of toasts) {
if (timers.has(toast.id)) {
continue;
}
const timer = setTimeout(() => {
dismiss(toast.id);
}, 4000);
timers.set(toast.id, timer);
}
for (const [id, timer] of timers.entries()) {
if (toasts.some((toast) => toast.id === id)) {
continue;
}
clearTimeout(timer);
timers.delete(id);
}
});
onDestroy(() => {
for (const timer of timers.values()) {
clearTimeout(timer);
}
timers.clear();
});
function dismiss(id: string) {
const timer = timers.get(id);
if (timer) {
clearTimeout(timer);
timers.delete(id);
}
toasts = toasts.filter((toast: ToastItem) => toast.id !== id);
}
</script>
<div class="fixed right-4 bottom-4 z-50 flex flex-col gap-2">
{#each toasts as toast (toast.id)}
<div
role="status"
aria-live="polite"
class="flex items-center gap-3 rounded-lg px-4 py-3 shadow-lg {toast.type === 'error'
? 'bg-red-600 text-white'
: toast.type === 'info'
? 'bg-blue-600 text-white'
: 'bg-green-600 text-white'}"
>
<span class="text-sm">{toast.message}</span>
<button
type="button"
aria-label="Dismiss notification"
onclick={() => dismiss(toast.id)}
class="ml-2 text-xs opacity-70 hover:opacity-100"
>
x
</button
>
</div>
{/each}
</div>

View File

@@ -0,0 +1,81 @@
<script lang="ts">
interface WorkerStatus {
index: number;
state: 'idle' | 'running';
jobId: string | null;
repositoryId: string | null;
versionId: string | null;
}
interface WorkersResponse {
concurrency: number;
active: number;
idle: number;
workers: WorkerStatus[];
}
let status = $state<WorkersResponse>({ concurrency: 0, active: 0, idle: 0, workers: [] });
let pollInterval: ReturnType<typeof setInterval> | null = null;
async function fetchStatus() {
try {
const res = await fetch('/api/v1/workers');
if (res.ok) status = await res.json();
} catch {
/* ignore */
}
}
$effect(() => {
void fetchStatus();
const es = new EventSource('/api/v1/jobs/stream');
es.addEventListener('worker-status', (event) => {
try {
status = JSON.parse(event.data);
} catch {
/* ignore */
}
});
es.onerror = () => {
es.close();
if (!pollInterval) {
pollInterval = setInterval(() => void fetchStatus(), 5000);
}
};
return () => {
es.close();
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
};
});
</script>
{#if status.concurrency > 0}
<div class="mb-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div class="mb-2 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-700">Workers</h3>
<span class="text-xs text-gray-500">{status.active} / {status.concurrency} active</span>
</div>
<div class="space-y-1">
{#each status.workers as worker (worker.index)}
<div class="flex items-center gap-2 text-xs">
<span
class="flex h-2 w-2 rounded-full {worker.state === 'running'
? 'animate-pulse bg-green-500'
: 'bg-gray-300'}"
></span>
<span class="text-gray-600">Worker {worker.index}</span>
{#if worker.state === 'running' && worker.repositoryId}
<span class="truncate text-gray-400"
>{worker.repositoryId}{worker.versionId ? ' / ' + worker.versionId : ''}</span
>
{:else}
<span class="text-gray-400">idle</span>
{/if}
</div>
{/each}
</div>
</div>
{/if}