Files
trueref/src/routes/+page.svelte
2026-03-27 03:01:37 +01:00

181 lines
5.4 KiB
Svelte

<script lang="ts">
import type { PageData } from './$types';
import RepositoryCard from '$lib/components/RepositoryCard.svelte';
import AddRepositoryModal from '$lib/components/AddRepositoryModal.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import IndexingProgress from '$lib/components/IndexingProgress.svelte';
let { data }: { data: PageData } = $props();
type Repository = NonNullable<PageData['repositories']>[number];
// Syncs from data prop on navigation while still allowing temporary local reassignment.
let repositories: Repository[] = $derived(data.repositories ?? []);
let showAddModal = $state(false);
let confirmDeleteId = $state<string | null>(null);
let activeJobIds = $state<Record<string, string>>({});
let errorMessage = $state<string | null>(null);
const confirmDeleteRepo = $derived(
confirmDeleteId ? repositories.find((r) => r.id === confirmDeleteId) : null
);
async function refreshRepositories() {
try {
const res = await fetch('/api/v1/libs');
const fetched = (await res.json()) as { libraries?: Repository[] };
repositories = fetched.libraries ?? [];
} catch {
// keep existing list on network error
}
}
async function handleReindex(id: string) {
errorMessage = null;
try {
const res = await fetch(`/api/v1/libs/${encodeURIComponent(id)}/index`, {
method: 'POST'
});
if (!res.ok) {
const fetched = await res.json();
throw new Error(fetched.error ?? 'Failed to trigger re-indexing');
}
const fetched = await res.json();
if (fetched.job?.id) {
activeJobIds = { ...activeJobIds, [id]: fetched.job.id };
}
await refreshRepositories();
} catch (e) {
errorMessage = (e as Error).message;
}
}
function handleDeleteRequest(id: string) {
confirmDeleteId = id;
}
async function handleDeleteConfirm() {
if (!confirmDeleteId) return;
const id = confirmDeleteId;
confirmDeleteId = null;
errorMessage = null;
try {
const res = await fetch(`/api/v1/libs/${encodeURIComponent(id)}`, {
method: 'DELETE'
});
if (!res.ok && res.status !== 204) {
const fetched = await res.json();
throw new Error(fetched.error ?? 'Failed to delete repository');
}
repositories = repositories.filter((r) => r.id !== id);
const updated = { ...activeJobIds };
delete updated[id];
activeJobIds = updated;
} catch (e) {
errorMessage = (e as Error).message;
}
}
function handleDeleteCancel() {
confirmDeleteId = null;
}
async function handleRepoAdded() {
await refreshRepositories();
}
</script>
<svelte:head>
<title>Repositories — TrueRef</title>
</svelte:head>
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Repositories</h1>
<p class="mt-0.5 text-sm text-gray-500">
{repositories.length}
{repositories.length === 1 ? 'repository' : 'repositories'} indexed
</p>
</div>
<button
type="button"
onclick={() => (showAddModal = true)}
class="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
Add Repository
</button>
</div>
{#if errorMessage}
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3">
<p class="text-sm text-red-700">{errorMessage}</p>
</div>
{/if}
{#if repositories.length === 0}
<div class="mt-16 flex flex-col items-center text-center">
<svg
class="h-16 w-16 text-gray-300"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<h2 class="mt-4 text-lg font-medium text-gray-700">No repositories yet</h2>
<p class="mt-2 max-w-sm text-sm text-gray-500">
Add your first GitHub or local repository to start indexing documentation for AI-powered
retrieval.
</p>
<button
type="button"
onclick={() => (showAddModal = true)}
class="mt-6 flex items-center gap-1.5 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
Add Repository
</button>
</div>
{:else}
<div class="mt-6 grid gap-4 sm:grid-cols-1 lg:grid-cols-2">
{#each repositories as repo (repo.id)}
<div>
<RepositoryCard {repo} onReindex={handleReindex} onDelete={handleDeleteRequest} />
{#if activeJobIds[repo.id]}
<div class="px-5">
<IndexingProgress jobId={activeJobIds[repo.id]} />
</div>
{/if}
</div>
{/each}
</div>
{/if}
{#if showAddModal}
<AddRepositoryModal onClose={() => (showAddModal = false)} onAdded={handleRepoAdded} />
{/if}
{#if confirmDeleteId && confirmDeleteRepo}
<ConfirmDialog
title="Delete Repository"
message="Are you sure you want to delete {confirmDeleteRepo.title}? This will remove all indexed data and cannot be undone."
confirmLabel="Delete"
danger={true}
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
/>
{/if}