import { ArrowLeft, FilePlus2, FolderOpen, HomeIcon, FolderPlus, RefreshCw, StarIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { createBrowseNodeId, findBrowseNode, getBrowseAncestorIds, getImmediateChildFolders, } from "../../browse-tree"; import { Loader } from "../components/ai-elements/loader"; import { BrowseDetailPane } from "../components/BrowseDetailPane"; import { BrowseOverview } from "../components/BrowseOverview"; import { BrowseTreeSidebar } from "../components/BrowseTreeSidebar"; import { BrowseWorkspaceCard } from "../components/BrowseWorkspaceCard"; import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "../components/ui/dialog"; import { Input } from "../components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../components/ui/select"; import { apiFetch } from "../hooks/use-api"; import { useDocEvents } from "../hooks/use-doc-events"; import { useCaptureModal } from "../hooks/useCaptureModal"; import { useWorkspace } from "../hooks/useWorkspace"; import { buildBrowseCrumbs, buildBrowseLocation, formatDateFieldLabel, parseBrowseLocation, type BrowseDocument, type BrowseTreeResponse, type DocsResponse, } from "../lib/browse"; import { loadFavoriteCollections, loadFavoriteDocuments, toggleFavoriteCollection, toggleFavoriteDocument, } from "../lib/navigation-state"; import { subscribeWorkspaceActionRequest } from "../lib/workspace-events"; interface PageProps { navigate: (to: string | number) => void; location?: string; } interface SyncResponse { jobId: string; } interface CreateFolderResponse { success: boolean; collection: string; folderPath: string; path: string; } type SyncTarget = { kind: "all" } | { kind: "collection"; name: string } | null; export default function Browse({ navigate, location }: PageProps) { const { activeTab, updateActiveTabBrowseState } = useWorkspace(); const { openCapture } = useCaptureModal(); const latestDocEvent = useDocEvents(); const resolvedLocation = location ?? `${window.location.pathname}${window.location.search}`; const selection = useMemo( () => parseBrowseLocation( resolvedLocation.includes("?") ? `?${resolvedLocation.split("?")[1] ?? ""}` : "" ), [resolvedLocation] ); const [tree, setTree] = useState(null); const [docs, setDocs] = useState([]); const [total, setTotal] = useState(0); const [offset, setOffset] = useState(0); const [treeLoading, setTreeLoading] = useState(true); const [docsLoading, setDocsLoading] = useState(false); const [availableDateFields, setAvailableDateFields] = useState([]); const [sortField, setSortField] = useState("modified"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const [syncJobId, setSyncJobId] = useState(null); const [syncTarget, setSyncTarget] = useState(null); const [syncError, setSyncError] = useState(null); const [refreshToken, setRefreshToken] = useState(0); const [favoriteDocHrefs, setFavoriteDocHrefs] = useState([]); const [favoriteCollections, setFavoriteCollections] = useState([]); const [mobileTreeOpen, setMobileTreeOpen] = useState(false); const [createFolderOpen, setCreateFolderOpen] = useState(false); const [createFolderName, setCreateFolderName] = useState(""); const [createFolderError, setCreateFolderError] = useState( null ); const [creatingFolder, setCreatingFolder] = useState(false); const limit = 25; const selectedCollection = selection.collection; const selectedPath = selection.path; const selectedNodeId = selectedCollection ? createBrowseNodeId(selectedCollection, selectedPath) : null; const resolvedSelectedNode = tree ? findBrowseNode(tree.collections, selectedCollection, selectedPath) : null; const selectedNode = resolvedSelectedNode ?? (selectedCollection ? { id: selectedNodeId ?? createBrowseNodeId(selectedCollection, selectedPath), kind: selectedPath ? "folder" : "collection", collection: selectedCollection, path: selectedPath, name: selectedPath.split("/").at(-1) ?? selectedCollection, depth: selectedPath ? selectedPath.split("/").filter(Boolean).length : 0, documentCount: docs.length, directDocumentCount: docs.length, children: [], } : null); const expandedNodeIds = useMemo(() => { const persisted = activeTab?.browseState?.expandedNodeIds ?? []; const ancestors = selectedPath ? getBrowseAncestorIds(selectedCollection, selectedPath) : []; return [...new Set([...persisted, ...ancestors])]; }, [ activeTab?.browseState?.expandedNodeIds, selectedCollection, selectedPath, ]); const childFolders = useMemo(() => { if (!tree) { return []; } if (!selectedCollection) { return tree.collections; } return getImmediateChildFolders( tree.collections, selectedCollection, selectedPath ); }, [selectedCollection, selectedPath, tree]); const crumbs = useMemo( () => selectedCollection ? buildBrowseCrumbs(selectedCollection, selectedPath) : [], [selectedCollection, selectedPath] ); useEffect(() => { setOffset(0); setDocs([]); }, [selectedCollection, selectedPath]); useEffect(() => { setFavoriteDocHrefs(loadFavoriteDocuments().map((entry) => entry.href)); setFavoriteCollections( loadFavoriteCollections().map((entry) => entry.name) ); }, []); useEffect(() => { void apiFetch("/api/browse/tree").then(({ data }) => { setTree( data ?? { collections: [], totalCollections: 0, totalDocuments: 0 } ); setTreeLoading(false); }); }, [refreshToken]); useEffect(() => { if (!tree) { return; } if (!selectedCollection) { return; } const node = findBrowseNode( tree.collections, selectedCollection, selectedPath ); if (node) { return; } if (selectedPath) { return; } const collectionRoot = findBrowseNode(tree.collections, selectedCollection); if (collectionRoot) { navigate(buildBrowseLocation(selectedCollection)); return; } navigate("/browse"); }, [navigate, selectedCollection, selectedPath, tree]); useEffect(() => { if (!selectedCollection) { setDocs([]); setTotal(0); setOffset(0); return; } setDocsLoading(true); const params = new URLSearchParams({ collection: selectedCollection, limit: String(limit), offset: String(offset), sortField, sortOrder, directChildrenOnly: "true", }); if (selectedPath) { params.set("pathPrefix", selectedPath); } void apiFetch(`/api/docs?${params.toString()}`).then( ({ data }) => { setDocsLoading(false); if (!data) { return; } setAvailableDateFields(data.availableDateFields ?? []); setSortField(data.sortField); setSortOrder(data.sortOrder); setDocs((prev) => offset === 0 ? data.documents : [...prev, ...data.documents] ); setTotal(data.total); } ); }, [ limit, offset, refreshToken, selectedCollection, selectedPath, sortField, sortOrder, ]); useEffect(() => { if (!latestDocEvent?.changedAt) { return; } setOffset(0); setDocs([]); setRefreshToken((current) => current + 1); }, [latestDocEvent?.changedAt]); useEffect(() => { return subscribeWorkspaceActionRequest("create-folder-here", () => { if (!selectedCollection) { return; } setCreateFolderError(null); setCreateFolderOpen(true); }); }, [selectedCollection]); useEffect(() => { const persisted = activeTab?.browseState?.expandedNodeIds ?? []; const ancestors = selectedPath ? getBrowseAncestorIds(selectedCollection, selectedPath) : []; const merged = [...new Set([...persisted, ...ancestors])]; if (merged.length === persisted.length) { return; } updateActiveTabBrowseState({ expandedNodeIds: merged, }); }, [ activeTab?.browseState?.expandedNodeIds, selectedCollection, selectedPath, updateActiveTabBrowseState, ]); const handleLoadMore = () => { setOffset((current) => current + limit); }; const handleSelectNode = (collection: string, path?: string) => { setDocs([]); setOffset(0); setMobileTreeOpen(false); navigate(buildBrowseLocation(collection, path)); }; const handleToggleNode = (nodeId: string) => { updateActiveTabBrowseState((current) => { const next = new Set(current.expandedNodeIds); if (next.has(nodeId)) { next.delete(nodeId); } else { next.add(nodeId); } return { expandedNodeIds: [...next], }; }); }; const handleToggleFavoriteCollection = (collection: string) => { const next = toggleFavoriteCollection({ name: collection, href: buildBrowseLocation(collection), label: collection, }); setFavoriteCollections(next.map((entry) => entry.name)); }; const handleSortChange = (value: string) => { const [nextField, nextOrder] = value.split(":"); if (!nextField || (nextOrder !== "asc" && nextOrder !== "desc")) { return; } setSortField(nextField); setSortOrder(nextOrder); setOffset(0); setDocs([]); }; const handleReindex = async () => { setSyncError(null); const body = selectedCollection ? { collection: selectedCollection } : {}; const { data, error } = await apiFetch("/api/sync", { method: "POST", body: JSON.stringify(body), }); if (error) { setSyncError(error); return; } if (data?.jobId) { setSyncJobId(data.jobId); setSyncTarget( selectedCollection ? { kind: "collection", name: selectedCollection } : { kind: "all" } ); } }; const handleCreateFolder = async () => { if (!selectedCollection || !createFolderName.trim()) { return; } setCreatingFolder(true); setCreateFolderError(null); const { data, error } = await apiFetch( "/api/folders", { method: "POST", body: JSON.stringify({ collection: selectedCollection, parentPath: selectedPath || undefined, name: createFolderName.trim(), }), } ); setCreatingFolder(false); if (error) { setCreateFolderError(error); return; } if (data?.folderPath) { setCreateFolderOpen(false); setCreateFolderName(""); navigate(buildBrowseLocation(selectedCollection, data.folderPath)); setRefreshToken((current) => current + 1); } }; const renderSidebar = () => ( ); return (

Browse

{selectedNode?.documentCount ?? tree?.totalDocuments.toLocaleString() ?? 0}{" "} docs {selectedCollection && ( )} {selectedCollection && ( )} {selectedCollection && ( )}
{ setSyncError(null); setSyncJobId(null); setSyncTarget(null); setRefreshToken((current) => current + 1); }} onSyncError={(error) => { setSyncError(error); setSyncJobId(null); setSyncTarget(null); }} selectedCollection={selectedCollection} syncError={syncError} syncJobId={syncJobId} syncTarget={syncTarget} /> {treeLoading ? (

Loading workspace tree...

) : !selectedCollection ? ( handleSelectNode(collection) } onToggleFavoriteCollection={handleToggleFavoriteCollection} /> ) : ( navigate(`/doc?uri=${encodeURIComponent(uri)}`) } onSelectCollection={(collection) => handleSelectNode(collection) } onSelectFolder={handleSelectNode} onToggleFavoriteDocument={(doc) => { const next = toggleFavoriteDocument({ uri: doc.uri, href: `/doc?uri=${encodeURIComponent(doc.uri)}`, label: doc.title || doc.relPath, }); setFavoriteDocHrefs(next.map((entry) => entry.href)); }} selectedNode={selectedNode} selectedPath={selectedPath} total={total} /> )}
Workspace Tree
{treeLoading ? (
) : ( renderSidebar() )}
Create folder
setCreateFolderName(event.target.value)} placeholder="research" value={createFolderName} /> {selectedCollection && (

{selectedCollection} {selectedPath ? ` / ${selectedPath}` : ""}

)} {createFolderError && (

{createFolderError}

)}
); }