feat(TRUEREF-0023): add sqlite-vec search pipeline
This commit is contained in:
19
src/lib/components/admin/JobSkeleton.svelte
Normal file
19
src/lib/components/admin/JobSkeleton.svelte
Normal 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}
|
||||
@@ -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>
|
||||
|
||||
77
src/lib/components/admin/Toast.svelte
Normal file
77
src/lib/components/admin/Toast.svelte
Normal 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>
|
||||
81
src/lib/components/admin/WorkerStatusPanel.svelte
Normal file
81
src/lib/components/admin/WorkerStatusPanel.svelte
Normal 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}
|
||||
Reference in New Issue
Block a user