diff --git a/backend/local_rag.py b/backend/local_rag.py index ef4b905..2392b89 100644 --- a/backend/local_rag.py +++ b/backend/local_rag.py @@ -984,6 +984,48 @@ def create_library(req: CreateLibraryRequest): return library_payload(data) +@router.post("/libraries/purge") +def purge_libraries(): + active_jobs = [ + job for job in JOBS.values() + if job["status"] in {"queued", "running"} + ] + if active_jobs: + active_slugs = sorted({str(job.get("slug") or "") for job in active_jobs if job.get("slug")}) + detail = "Cannot purge databases while library sync jobs are still running." + if active_slugs: + detail = f"{detail} Active databases: {', '.join(active_slugs)}." + raise HTTPException(status_code=409, detail=detail) + + removed: List[str] = [] + failures: List[str] = [] + + for path in list(LIB_ROOT.iterdir()): + if not path.is_dir(): + continue + try: + shutil.rmtree(path) + removed.append(path.name) + except Exception as exc: + failures.append(f"{path.name}: {type(exc).__name__}: {exc}") + + LIB_ROOT.mkdir(parents=True, exist_ok=True) + JOBS.clear() + LIB_LOCKS.clear() + + if failures: + preview = "; ".join(failures[:3]) + if len(failures) > 3: + preview = f"{preview}; ..." + raise HTTPException(status_code=500, detail=f"Failed to purge some databases. {preview}") + + return { + "ok": True, + "count": len(removed), + "removed": removed, + } + + @router.get("/libraries/{slug}") def get_library(slug: str): return library_payload(read_library(slug)) diff --git a/src/App.jsx b/src/App.jsx index 2138a75..59b15db 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -808,6 +808,17 @@ async function regenerateFromIndex(index, overrideUserText = null) { } } + async function handleLibrariesPurged() { + setLibraries([]) + setLibraryJobs([]) + setActiveLibrarySlug(null) + setEditingLibrarySlug(null) + setIsDbPickerOpen(false) + setChatLibraryBySession({}) + await refreshLibraries() + await refreshLibraryJobs() + } + // Load chat sessions from backend on initial render useEffect(() => { if (!backendApiUrl) return; diff --git a/src/GeneralSettings.jsx b/src/GeneralSettings.jsx index 99018a5..f5b6f90 100644 --- a/src/GeneralSettings.jsx +++ b/src/GeneralSettings.jsx @@ -29,7 +29,7 @@ function getStatusTone(state) { return 'neutral'; } -export default function GeneralSettings({ onModelChange, onStreamOutputChange }) { +export default function GeneralSettings({ onModelChange, onStreamOutputChange, onLibrariesPurged }) { const [backendApiUrl, setBackendApiUrl] = useState(''); const [ollamaApiUrl, setOllamaApiUrl] = useState(''); const [models, setModels] = useState([]); @@ -37,6 +37,8 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange }) const [streamOutput, setStreamOutput] = useState(false); const [updateStatus, setUpdateStatus] = useState(DEFAULT_UPDATE_STATUS); const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false); + const [isPurgingLibraries, setIsPurgingLibraries] = useState(false); + const [libraryPurgeStatus, setLibraryPurgeStatus] = useState({ tone: 'neutral', message: '' }); useEffect(() => { let cancelled = false; @@ -127,6 +129,48 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange }) } }; + const handlePurgeLibraries = async () => { + const confirmed = window.confirm( + 'Delete all Heimgeist databases, staged files, and indexes from local storage? Chat history will be kept.' + ); + if (!confirmed) { + return; + } + + setIsPurgingLibraries(true); + setLibraryPurgeStatus({ tone: 'neutral', message: '' }); + + try { + const response = await fetch(`${backendApiUrl}/libraries/purge`, { + method: 'POST', + }); + const data = await response.json().catch(() => null); + + if (!response.ok) { + throw new Error(data?.detail || `HTTP ${response.status}`); + } + + const count = Number(data?.count) || 0; + setLibraryPurgeStatus({ + tone: 'success', + message: count > 0 + ? `Removed ${count} database${count === 1 ? '' : 's'} from local storage.` + : 'No local databases were found to remove.', + }); + + if (onLibrariesPurged) { + await Promise.resolve(onLibrariesPurged()); + } + } catch (error) { + setLibraryPurgeStatus({ + tone: 'error', + message: `Database purge failed: ${error.message || String(error)}`, + }); + } finally { + setIsPurgingLibraries(false); + } + }; + const updateCheckedAtLabel = updateStatus.checkedAt ? new Date(updateStatus.checkedAt).toLocaleString() : null; @@ -205,6 +249,27 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange }) )} +
+

Purge Databases

+
+ +
+

+ Removes every local Heimgeist database, including staged files, corpora, and indexes. This is meant as a recovery action when the DB panel becomes unusable. Chat history stays intact. +

+ {libraryPurgeStatus.message && ( +

+ {libraryPurgeStatus.message} +

+ )} +
); }