181 lines
5.4 KiB
Svelte
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}
|