import { BookOpen, CheckCircle2Icon, CpuIcon, Database, DownloadIcon, FolderHeartIcon, FolderIcon, GitForkIcon, HistoryIcon, Layers, Loader2Icon, MessageSquare, PenIcon, RefreshCwIcon, Search, StarIcon, } from "lucide-react"; import { lazy, Suspense, useCallback, useEffect, useState } from "react"; import type { AppStatusResponse, HealthActionKind } from "../../status-model"; import { CaptureButton } from "../components/CaptureButton"; import { GnoLogo } from "../components/GnoLogo"; import { Button } from "../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, } from "../components/ui/card"; import { Progress } from "../components/ui/progress"; import { apiFetch } from "../hooks/use-api"; import { useCaptureModal } from "../hooks/useCaptureModal"; import { loadFavoriteCollections, loadFavoriteDocuments, loadRecentDocuments, toggleFavoriteCollection, type FavoriteCollection, type FavoriteDoc, type RecentDoc, } from "../lib/navigation-state"; interface SyncResponse { jobId: string; } interface EmbedResponse { embedded?: number; errors?: number; running?: boolean; pendingCount?: number; note?: string; } interface SyncResultSummary { collections: Array<{ collection: string; filesProcessed: number; filesAdded: number; filesUpdated: number; filesUnchanged: number; filesErrored: number; durationMs: number; }>; totalDurationMs: number; totalFilesProcessed: number; totalFilesAdded: number; totalFilesUpdated: number; totalFilesErrored: number; totalFilesSkipped: number; } interface DownloadProgressState { downloadedBytes: number; totalBytes: number; percent: number; } interface ModelDownloadStatus { active: boolean; currentType: string | null; progress: DownloadProgressState | null; completed: string[]; failed: Array<{ type: string; error: string }>; startedAt: number | null; } const AddCollectionDialog = lazy(() => import("../components/AddCollectionDialog").then((module) => ({ default: module.AddCollectionDialog, })) ); const AIModelSelector = lazy(() => import("../components/AIModelSelector").then((module) => ({ default: module.AIModelSelector, })) ); const BootstrapStatus = lazy(() => import("../components/BootstrapStatus").then((module) => ({ default: module.BootstrapStatus, })) ); const FirstRunWizard = lazy(() => import("../components/FirstRunWizard").then((module) => ({ default: module.FirstRunWizard, })) ); const HealthCenter = lazy(() => import("../components/HealthCenter").then((module) => ({ default: module.HealthCenter, })) ); const IndexingProgress = lazy(() => import("../components/IndexingProgress").then((module) => ({ default: module.IndexingProgress, })) ); interface PageProps { navigate: (to: string | number) => void; } export default function Dashboard({ navigate }: PageProps) { const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [syncing, setSyncing] = useState(false); const [syncJobId, setSyncJobId] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); const [initialCollectionPath, setInitialCollectionPath] = useState< string | undefined >(undefined); const [busyAction, setBusyAction] = useState(null); const [recentDocs, setRecentDocs] = useState([]); const [favoriteDocs, setFavoriteDocs] = useState([]); const [favoriteCollections, setFavoriteCollections] = useState< FavoriteCollection[] >([]); const [modelDownloadStatus, setModelDownloadStatus] = useState(null); const { openCapture } = useCaptureModal(); const openCollections = () => navigate("/collections"); const loadStatus = useCallback(async () => { const { data, error: err } = await apiFetch("/api/status"); if (err) { setError(err); return; } setStatus(data); setError(null); }, []); useEffect(() => { void loadStatus(); }, [loadStatus]); useEffect(() => { setRecentDocs(loadRecentDocuments()); setFavoriteDocs(loadFavoriteDocuments()); setFavoriteCollections(loadFavoriteCollections()); }, []); useEffect(() => { let cancelled = false; let intervalId: ReturnType | null = null; async function pollModelStatus(): Promise { const { data } = await apiFetch("/api/models/status"); if (cancelled || !data) { return; } setModelDownloadStatus(data); if (!data.active && intervalId) { clearInterval(intervalId); intervalId = null; void loadStatus(); } } void pollModelStatus(); if ( busyAction === "download-models" || status?.bootstrap.models.downloading || modelDownloadStatus?.active ) { intervalId = setInterval(() => { void pollModelStatus(); }, 1000); } return () => { cancelled = true; if (intervalId) { clearInterval(intervalId); } }; }, [ busyAction, loadStatus, modelDownloadStatus?.active, status?.bootstrap.models.downloading, ]); const handleSync = useCallback(async () => { setSyncing(true); setSyncJobId(null); const { data, error: err } = await apiFetch("/api/sync", { method: "POST", }); if (err) { setSyncing(false); setError(err); return; } if (data?.jobId) { setSyncJobId(data.jobId); } }, []); const handleSyncComplete = (result?: SyncResultSummary) => { setSyncing(false); void loadStatus(); const isNoop = !!result && result.totalFilesProcessed === 0 && result.totalFilesAdded === 0 && result.totalFilesUpdated === 0 && result.totalFilesErrored === 0; window.setTimeout( () => { setSyncJobId(null); }, isNoop ? 2500 : 1200 ); }; const handleOpenAddCollection = (path?: string) => { setInitialCollectionPath(path); setAddDialogOpen(true); }; const handleDownloadModels = useCallback(async () => { setBusyAction("download-models"); const { error: err } = await apiFetch("/api/models/pull", { method: "POST", }); setBusyAction(null); if (err) { setError(err); return; } const { data } = await apiFetch("/api/models/status"); if (data) { setModelDownloadStatus(data); } void loadStatus(); }, [loadStatus]); const handleEmbedNow = useCallback(async () => { setBusyAction("embed"); const { data, error: err } = await apiFetch("/api/embed", { method: "POST", }); setBusyAction(null); if (err) { setError(err); return; } if (data?.errors && data.errors > 0) { setError(`Embedding failed for ${data.errors} chunks.`); } void loadStatus(); }, [loadStatus]); const isModelDownloadBlocking = busyAction === "download-models" || modelDownloadStatus?.active; const handleHealthAction = (action: HealthActionKind) => { if (action === "add-collection") { handleOpenAddCollection(); return; } if (action === "open-collections") { openCollections(); return; } if (action === "sync") { void handleSync(); return; } if (action === "embed") { void handleEmbedNow(); return; } if (action === "download-models") { void handleDownloadModels(); } }; const handleToggleFavoriteCollection = ( collection: AppStatusResponse["collections"][number] ) => { setFavoriteCollections( toggleFavoriteCollection({ name: collection.name, href: `/browse?collection=${encodeURIComponent(collection.name)}`, label: collection.name, }) ); }; return (
{isModelDownloadBlocking && (
Downloading local models
GNO is preparing the active preset. Keep this page open until the download finishes.
{modelDownloadStatus?.currentType ? `Current step: ${modelDownloadStatus.currentType}` : "Preparing download"} {modelDownloadStatus?.progress ? `${modelDownloadStatus.progress.percent.toFixed(0)}%` : "Starting..."}
Completed

{modelDownloadStatus?.completed.length ? modelDownloadStatus.completed.join(", ") : "Nothing completed yet."}

Why this blocks

Search can partially work while files stream in, but results and first-run status are clearer once the preset download finishes.

)}

GNO

Your Local Knowledge Index

{syncJobId && (!status || status.onboarding.ready) && (
{ setSyncing(false); setSyncJobId(null); }} />
)}
{status && !status.onboarding.ready && (
void handleDownloadModels()} onEmbed={() => void handleEmbedNow()} onboarding={status.onboarding} onSync={() => void handleSync()} onSyncComplete={handleSyncComplete} syncJobId={syncJobId} syncing={syncing} />
)} {error && ( {error} )} {status && (
)} {status && (
void handleDownloadModels()} />
)} {(recentDocs.length > 0 || favoriteDocs.length > 0 || favoriteCollections.length > 0) && (
Favorite Documents
{favoriteDocs.length === 0 ? (

Favorite a document from Browse to keep it here.

) : ( favoriteDocs.slice(0, 5).map((doc) => ( )) )}
Pinned Collections
{favoriteCollections.length === 0 ? (

Pin collections from the dashboard cards.

) : ( favoriteCollections.slice(0, 5).map((collection) => ( )) )}
Recent Documents
{recentDocs.length === 0 ? (

Open a few notes and they will show up here.

) : ( recentDocs.slice(0, 5).map((doc) => ( )) )}
)} {status && (
Documents
{status.totalDocuments.toLocaleString()}

indexed files

Chunks
{status.totalChunks.toLocaleString()}
{ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openCollections(); } }} role="button" tabIndex={0} > Collections
{status.collections.length} {status.healthy && ( )}

Add folders, re-index after changes, remove old sources.

openCapture()} > Quick Capture
New Note N
)} {status && status.collections.length > 0 && (

Collections

{status.collections.map((collection, index) => ( navigate( `/browse?collection=${encodeURIComponent(collection.name)}` ) } style={{ animationDelay: `${0.4 + index * 0.1}s` }} >
{syncing ? ( ) : collection.embeddedCount >= collection.chunkCount ? ( ) : (
)}
{collection.name}
{collection.path}
{collection.documentCount.toLocaleString()} docs
{collection.embeddedCount === collection.chunkCount ? `${collection.chunkCount.toLocaleString()} chunks` : `${collection.embeddedCount}/${collection.chunkCount} embedded`}
))}
)}
openCapture()} /> void loadStatus()} open={addDialogOpen} />
); }