import { lazy, Suspense, useCallback, useEffect, useMemo, useState, } from "react"; import { createRoot } from "react-dom/client"; import { HelpButton } from "./components/HelpButton"; import { WorkspaceTabs } from "./components/WorkspaceTabs"; import { CaptureModalProvider, useCaptureModal } from "./hooks/useCaptureModal"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import { WorkspaceProvider } from "./hooks/useWorkspace"; import { consumeClipperPairLaunch } from "./lib/clipper-approval"; import { parseDocumentDeepLink } from "./lib/deep-links"; import { saveRecentDocument } from "./lib/navigation-state"; import { activateWorkspaceTab, closeWorkspaceTab, createWorkspaceTab, loadWorkspaceState, saveWorkspaceState, updateActiveTabLocation, updateActiveTabBrowseState, type WorkspaceState, } from "./lib/workspace-tabs"; import ClipperPairing from "./pages/ClipperPairing"; import Dashboard from "./pages/Dashboard"; const QuickSwitcher = lazy(() => import("./components/QuickSwitcher").then((module) => ({ default: module.QuickSwitcher, })) ); const ShortcutHelpModal = lazy(() => import("./components/ShortcutHelpModal").then((module) => ({ default: module.ShortcutHelpModal, })) ); const Search = lazy(() => import("./pages/Search")); const Browse = lazy(() => import("./pages/Browse")); const DocView = lazy(() => import("./pages/DocView")); const DocumentEditor = lazy(() => import("./pages/DocumentEditor")); const Collections = lazy(() => import("./pages/Collections")); const Connectors = lazy(() => import("./pages/Connectors")); const Ask = lazy(() => import("./pages/Ask")); const GraphView = lazy(() => import("./pages/GraphView")); const TraceHistory = lazy(() => import("./pages/TraceHistory")); type Route = | "/" | "/search" | "/browse" | "/doc" | "/ask" | "/edit" | "/collections" | "/graph" | "/connectors" | "/traces"; type Navigate = (to: string | number) => void; interface RoutePageProps { navigate: Navigate; location?: string; } const routes: Record> = { "/": Dashboard, "/search": Search, "/browse": Browse, "/doc": DocView, "/edit": DocumentEditor, "/collections": Collections, "/connectors": Connectors, "/ask": Ask, "/graph": GraphView, "/traces": TraceHistory, }; interface AppContentProps { location: string; navigate: Navigate; shortcutHelpOpen: boolean; setShortcutHelpOpen: (open: boolean) => void; workspace: WorkspaceState; onActivateTab: (tabId: string) => void; onCloseTab: (tabId: string) => void; onNewTab: () => void; } function AppContent({ location, navigate, onActivateTab, onCloseTab, onNewTab, shortcutHelpOpen, setShortcutHelpOpen, workspace, }: AppContentProps) { const { openCapture } = useCaptureModal(); const [quickSwitcherOpen, setQuickSwitcherOpen] = useState(false); useEffect(() => { const basePath = location.split("?")[0]; if (basePath !== "/doc" && basePath !== "/edit") { return; } const search = location.includes("?") ? `?${location.split("?")[1] ?? ""}` : ""; const target = parseDocumentDeepLink(search); if (!target.uri) { return; } saveRecentDocument({ uri: target.uri, href: location, label: decodeURIComponent(target.uri.split("/").pop() ?? target.uri), }); }, [location]); const shortcuts = useMemo( () => [ { key: "/", action: () => { const searchInput = document.querySelector( 'input[type="search"], input[placeholder*="Search"], input[id*="search"]' ); if (searchInput) { searchInput.focus(); searchInput.select(); } else { navigate("/search"); } }, }, { key: "?", action: () => setShortcutHelpOpen(true), }, { key: "k", meta: true, action: () => setQuickSwitcherOpen(true), }, ], [navigate, setShortcutHelpOpen] ); useKeyboardShortcuts(shortcuts); const basePath = location.split("?")[0] as Route; const Page = routes[basePath] || Dashboard; const pageKey = basePath === "/browse" ? basePath : location; return ( <>
setShortcutHelpOpen(true)} /> navigate(to)} onCreateNote={openCapture} onOpenChange={setQuickSwitcherOpen} open={quickSwitcherOpen} /> ); } function App() { const initialLocation = window.location.pathname + window.location.search + window.location.hash; const [workspace, setWorkspace] = useState(() => loadWorkspaceState(initialLocation) ); const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false); const activeTab = workspace.tabs.find((tab) => tab.id === workspace.activeTabId) ?? workspace.tabs[0]; const location = activeTab?.location ?? "/"; useEffect(() => { const currentLocation = window.location.pathname + window.location.search + window.location.hash; if (location !== currentLocation) { window.history.replaceState({}, "", location); } }, [location]); useEffect(() => { const handlePopState = () => { setWorkspace((current) => updateActiveTabLocation( current, window.location.pathname + window.location.search + window.location.hash ) ); }; window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, []); useEffect(() => { saveWorkspaceState(workspace); }, [workspace]); const navigate = useCallback((to: string | number) => { if (typeof to === "number") { window.history.go(to); return; } window.history.pushState({}, "", to); setWorkspace((current) => updateActiveTabLocation(current, to)); }, []); const activateTab = useCallback((tabId: string) => { setWorkspace((current) => { const next = activateWorkspaceTab(current, tabId); const tab = next.tabs.find((entry) => entry.id === next.activeTabId) ?? next.tabs[0]; if (tab) { window.history.pushState({}, "", tab.location); } return next; }); }, []); const closeTab = useCallback((tabId: string) => { setWorkspace((current) => { const next = closeWorkspaceTab(current, tabId); const tab = next.tabs.find((entry) => entry.id === next.activeTabId) ?? next.tabs[0]; if (tab) { window.history.replaceState({}, "", tab.location); } return next; }); }, []); const openNewTab = useCallback(() => { setWorkspace((current) => { const next = createWorkspaceTab(current, "/search"); const tab = next.tabs.find((entry) => entry.id === next.activeTabId) ?? next.tabs[0]; if (tab) { window.history.pushState({}, "", tab.location); } return next; }); }, []); const updateActiveBrowseState = useCallback( (nextBrowseState: Parameters[1]) => { setWorkspace((current) => updateActiveTabBrowseState(current, nextBrowseState) ); }, [] ); return ( ); } const rootElement = document.getElementById("root"); if (!rootElement) { throw new Error("Root element not found"); } const root = createRoot(rootElement); if (window.location.pathname === "/clipper/pair") { const launch = consumeClipperPairLaunch(window.location, window.history); root.render(); } else { root.render(); }