import React, { useEffect, useState } from 'react' function statusLabel(job) { if (!job) return null const progress = typeof job.progress === 'number' ? `${job.progress.toFixed(0)}%` : null const detail = job.detail ? ` ${job.detail}` : '' return `${job.type} · ${job.status}${progress ? ` · ${progress}` : ''}${detail}` } export default function LibraryManager({ apiBase, library, jobs, chatLibrarySlug, onRefresh, onToggleChatLibrary, onDeleted }) { const [busy, setBusy] = useState(false) const [isRenaming, setIsRenaming] = useState(false) const [renameValue, setRenameValue] = useState('') const [confirmDelete, setConfirmDelete] = useState(false) const [errorMessage, setErrorMessage] = useState('') useEffect(() => { setIsRenaming(false) setRenameValue(library?.name || '') setConfirmDelete(false) setErrorMessage('') }, [library?.slug, library?.name]) async function expectOk(response) { if (response.ok) return response const detail = await response.text() throw new Error(detail || `HTTP ${response.status}`) } async function runAction(fn) { setBusy(true) try { setErrorMessage('') await fn() setConfirmDelete(false) } finally { setBusy(false) await onRefresh() } } async function addPaths() { if (!library) return const paths = await window.electronAPI?.pickPaths?.() if (!Array.isArray(paths) || paths.length === 0) return try { await runAction(async () => { const response = await fetch(`${apiBase}/libraries/${library.slug}/files/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }) }) await expectOk(response) }) } catch (error) { setErrorMessage(String(error?.message || error)) } } async function removeFile(rel) { if (!library) return try { await runAction(async () => { const response = await fetch(`${apiBase}/libraries/${library.slug}/files`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rel }) }) await expectOk(response) }) } catch (error) { setErrorMessage(String(error?.message || error)) } } async function renameLibrary() { if (!library) return const name = renameValue.trim() if (!name || name === library.name) { setIsRenaming(false) setRenameValue(library.name || '') return } await runAction(async () => { const response = await fetch(`${apiBase}/libraries/${library.slug}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }) await expectOk(response) }) setIsRenaming(false) } async function deleteLibrary() { if (!library) return await runAction(async () => { const response = await fetch(`${apiBase}/libraries/${library.slug}`, { method: 'DELETE' }) await expectOk(response) }) onDeleted?.(library.slug) } async function startJob(kind) { if (!library) return try { await runAction(async () => { const endpoint = `${apiBase}/libraries/${library.slug}/jobs/${kind}` const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } } if (kind === 'embed') { options.body = JSON.stringify({}) } const response = await fetch(endpoint, options) await expectOk(response) }) } catch (error) { setErrorMessage(String(error?.message || error)) } } if (!library) { return (

Create a database, add files or folders, then build and index it for local RAG.

) } const activeJobs = (jobs || []).filter(job => job.slug === library.slug && (job.status === 'queued' || job.status === 'running')) const usingInChat = chatLibrarySlug === library.slug const canStartRename = () => { setRenameValue(library.name || '') setErrorMessage('') setIsRenaming(true) setConfirmDelete(false) } return (
{isRenaming && (
setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { renameLibrary().catch((error) => setErrorMessage(String(error?.message || error))) } else if (e.key === 'Escape') { setIsRenaming(false) setRenameValue(library.name || '') } }} autoFocus />
)} {confirmDelete && (
Delete "{library.name}"? This removes the local index and metadata for this database.
)} {errorMessage &&
{errorMessage}
}
Files: {library.files?.length || 0}
Corpus: {library.artifacts?.corpus_records || 0}
Enriched: {library.artifacts?.enhanced_records || 0}
Indexed
{usingInChat && (
This database will be queried before each chat request and its context will be appended to the prompt.
)} {activeJobs.length > 0 && (
{activeJobs.map(job => (
{statusLabel(job)}
))}
)}

Files

{library.files?.length ? (
{library.files.map(file => (
{file.name || file.path}
{file.path}
))}
) : (

No files registered yet.

)}
) }