import React, { useEffect, useState } from 'react' function statusLabel(job) { if (!job) return null const type = job.type === 'prepare' ? 'prepare' : job.type const progress = typeof job.progress === 'number' ? `${job.progress.toFixed(0)}%` : null const detail = job.detail ? ` ${job.detail}` : '' return `${type} · ${job.status}${progress ? ` · ${progress}` : ''}${detail}` } function fileSyncMeta(file) { const sync = file?.sync || {} const status = String(sync.status || 'pending') const progress = Math.max(0, Math.min(100, Number(sync.progress) || 0)) const detail = String(sync.detail || '').trim() const error = String(sync.error || '').trim() const enrichEnabled = !!file?.enrich_enabled if (status === 'ready') { return { status, progress: 100, label: 'Available', detail: detail || (enrichEnabled ? 'Ready in chat with enrichment enabled.' : 'Ready in chat with raw indexing only.') } } if (status === 'failed') { return { status, progress: 100, label: 'Sync failed', detail: error || detail || 'Heimgeist could not finish syncing this file.' } } if (status === 'syncing') { return { status, progress, label: progress > 0 ? `Syncing ${Math.round(progress)}%` : 'Syncing', detail: detail || 'Building corpus, enrichment, embeddings, and indexes.' } } return { status: 'pending', progress: 6, label: 'Queued', detail: 'Waiting to start the full sync pipeline.' } } export default function LibraryManager({ apiBase, library, jobs, onRefresh, onDeleted }) { const [busy, setBusy] = useState(false) const [confirmDelete, setConfirmDelete] = useState(false) const [errorMessage, setErrorMessage] = useState('') useEffect(() => { 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 updateFileEnrichment(rel, enabled) { if (!library) return try { await runAction(async () => { const response = await fetch(`${apiBase}/libraries/${library.slug}/files/enrichment`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rel, enabled }) }) await expectOk(response) }) } catch (error) { setErrorMessage(String(error?.message || error)) } } 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 retrySync() { if (!library) return try { await runAction(async () => { const response = await fetch(`${apiBase}/libraries/${library.slug}/jobs/prepare`, { method: 'POST' }) await expectOk(response) }) } catch (error) { setErrorMessage(String(error?.message || error)) } } if (!library) { return (

Create a database and add files. Heimgeist will keep its retrieval pipeline updated automatically.

) } const activeJobs = (jobs || []).filter(job => job.slug === library.slug && (job.status === 'queued' || job.status === 'running')) const isSyncing = activeJobs.length > 0 const isReadyForChat = !!library.states?.is_indexed const hasFailedFiles = (library.files || []).some(file => file?.sync?.status === 'failed') return (
{confirmDelete && (
Delete "{library.name}"? This removes the registered files and local retrieval data for this database.
)} {errorMessage &&
{errorMessage}
}
{library.files?.length > 0 && !isSyncing && !isReadyForChat && ( )}
Files: {library.files?.length || 0}
0 ? 'ready' : ''}`}> Enrich: {library.states?.enrichment_enabled_files || 0}
{isSyncing ? 'Syncing' : isReadyForChat ? 'Ready' : library.files?.length ? 'Needs sync' : 'No data yet'}
{isSyncing && (
Syncing this database. Heimgeist is rebuilding the corpus, enrichment, embeddings, and indexes automatically.
)} {!library.files?.length && !isSyncing && (
Add files to make this database available in chat.
)} {!!library.files?.length && !isSyncing && (
Raw indexing is the default fast path. Turn on enrichment only for files that need better summaries, entities, and semantic recall.
)} {hasFailedFiles && !isSyncing && (
Some files did not finish syncing. Their tiles show the failure state and error details.
)} {activeJobs.length > 0 && (
{activeJobs.map(job => (
{statusLabel(job)}
))}
)}

Files

{library.files?.length ? (
{library.files.map(file => { const sync = fileSyncMeta(file) return (
{file.name || file.path}
{file.path}
{file.enrich_enabled ? 'Enrichment on' : 'Raw only'}
{sync.label} {sync.detail}
) })}
) : (

No files registered yet.

)}
) }