import { useEffect, useState, useMemo, useCallback, lazy, Suspense } from "react"; import { validateGraph } from "@understand-anything/core/schema"; import type { GraphIssue } from "@understand-anything/core/schema"; import { useDashboardStore } from "./store"; import GraphView from "./components/GraphView"; import DomainGraphView from "./components/DomainGraphView"; import KnowledgeGraphView from "./components/KnowledgeGraphView"; import SearchBar from "./components/SearchBar"; import NodeInfo from "./components/NodeInfo"; import LayerLegend from "./components/LayerLegend"; import DiffToggle from "./components/DiffToggle"; import FilterPanel from "./components/FilterPanel"; import ExportMenu from "./components/ExportMenu"; import PersonaSelector from "./components/PersonaSelector"; import ProjectOverview from "./components/ProjectOverview"; import FileExplorer from "./components/FileExplorer"; import WarningBanner from "./components/WarningBanner"; import TokenGate from "./components/TokenGate"; import MobileLayout from "./components/MobileLayout"; import { useIsMobile } from "./hooks/useIsMobile"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; import { ThemeProvider } from "./themes/index.ts"; import { ThemePicker } from "./components/ThemePicker.tsx"; import type { ThemeConfig } from "./themes/index.ts"; import { I18nProvider, useI18n } from "./contexts/I18nContext.tsx"; // Lazy-load heavy / optional components so they ship in separate chunks. const CodeViewer = lazy(() => import("./components/CodeViewer")); const LearnPanel = lazy(() => import("./components/LearnPanel")); const PathFinderModal = lazy(() => import("./components/PathFinderModal")); const KeyboardShortcutsHelp = lazy( () => import("./components/KeyboardShortcutsHelp"), ); const OnboardingOverlay = lazy(() => import("./components/OnboardingOverlay")); const DEMO_MODE = import.meta.env.VITE_DEMO_MODE === "true"; const SESSION_TOKEN_KEY = "understand-anything-token"; const ONBOARDING_DISMISSED_KEY = "ua-onboarding-dismissed-v1"; type SidebarTab = "info" | "files"; function shouldShowOnboarding(): boolean { if (typeof window === "undefined") return false; const params = new URLSearchParams(window.location.search); if (params.get("onboard") === "force") return true; return window.localStorage.getItem(ONBOARDING_DISMISSED_KEY) !== "1"; } /** Resolve data file URL — in demo mode, use env var URLs; otherwise use local paths with token. */ function dataUrl(fileName: string, token: string | null): string { if (DEMO_MODE) { const envMap: Record = { "knowledge-graph.json": import.meta.env.VITE_GRAPH_URL, "domain-graph.json": import.meta.env.VITE_DOMAIN_GRAPH_URL, "meta.json": import.meta.env.VITE_META_URL, "diff-overlay.json": import.meta.env.VITE_DIFF_OVERLAY_URL, "config.json": import.meta.env.VITE_CONFIG_URL, }; const url = envMap[fileName]; if (url) return url; } const path = `/${fileName}`; return token ? `${path}?token=${encodeURIComponent(token)}` : path; } /** * Resolve the access token from the URL query string or sessionStorage. * If found in the URL, persist to sessionStorage and strip the param from the address bar. */ function resolveInitialToken(): string | null { if (DEMO_MODE) return "__demo__"; const params = new URLSearchParams(window.location.search); const urlToken = params.get("token"); if (urlToken) { sessionStorage.setItem(SESSION_TOKEN_KEY, urlToken); // Clean the URL params.delete("token"); const cleanSearch = params.toString(); const newUrl = window.location.pathname + (cleanSearch ? `?${cleanSearch}` : "") + window.location.hash; window.history.replaceState(null, "", newUrl); return urlToken; } return sessionStorage.getItem(SESSION_TOKEN_KEY); } function App() { const [accessToken, setAccessToken] = useState(resolveInitialToken); const handleTokenValid = useCallback((token: string) => { sessionStorage.setItem(SESSION_TOKEN_KEY, token); setAccessToken(token); }, []); // In demo mode, skip token gate entirely if (DEMO_MODE) { return ; } // Show the token gate when no token is available if (accessToken === null) { return ; } return ; } function Dashboard({ accessToken }: { accessToken: string }) { const setGraph = useDashboardStore((s) => s.setGraph); const setDomainGraph = useDashboardStore((s) => s.setDomainGraph); const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); const [loadError, setLoadError] = useState(null); const [graphIssues, setGraphIssues] = useState([]); const [metaTheme, setMetaTheme] = useState(null); const [outputLanguage, setOutputLanguage] = useState(); useEffect(() => { fetch(dataUrl("meta.json", accessToken)) .then((r) => (r.ok ? r.json() : null)) .then((meta) => { if (meta?.theme) setMetaTheme(meta.theme); }) .catch(() => {}); fetch(dataUrl("config.json", accessToken)) .then((r) => (r.ok ? r.json() : null)) .then((config) => { if (config?.outputLanguage) setOutputLanguage(config.outputLanguage); }) .catch(() => {}); }, []); useEffect(() => { fetch(dataUrl("knowledge-graph.json", accessToken)) .then((res) => res.json()) .then((data: unknown) => { const result = validateGraph(data); if (result.success && result.data) { setGraph(result.data); setGraphIssues(result.issues); if ((data as Record).kind === "knowledge") { useDashboardStore.getState().setViewMode("knowledge"); useDashboardStore.getState().setIsKnowledgeGraph(true); } for (const issue of result.issues) { if (issue.level === "auto-corrected") { console.warn(`[graph] auto-corrected: ${issue.message}`); } else if (issue.level === "dropped") { console.error(`[graph] dropped: ${issue.message}`); } } } else if (result.fatal) { console.error("Knowledge graph validation failed:", result.fatal); setLoadError(`Invalid knowledge graph: ${result.fatal}`); } else { console.error("Knowledge graph validation failed: unknown error"); setLoadError("Invalid knowledge graph: unknown validation error"); } }) .catch((err) => { console.error("Failed to load knowledge graph:", err); setLoadError(`Failed to load knowledge graph: ${err instanceof Error ? err.message : String(err)}`); }); }, [setGraph]); useEffect(() => { fetch(dataUrl("diff-overlay.json", accessToken)) .then((res) => { if (!res.ok) return null; return res.json(); }) .then((data: unknown) => { if ( data && typeof data === "object" && "changedNodeIds" in data && "affectedNodeIds" in data && Array.isArray((data as Record).changedNodeIds) && Array.isArray((data as Record).affectedNodeIds) ) { const d = data as { changedNodeIds: string[]; affectedNodeIds: string[] }; if (d.changedNodeIds.length > 0) { setDiffOverlay(d.changedNodeIds, d.affectedNodeIds); } } }) .catch(() => {}); }, [setDiffOverlay]); useEffect(() => { fetch(dataUrl("domain-graph.json", accessToken)) .then((res) => { if (!res.ok) return null; return res.json(); }) .then((data: unknown) => { if (!data) return; const result = validateGraph(data); if (result.success && result.data) { setDomainGraph(result.data); } else if (result.fatal) { console.warn(`[domain-graph] validation failed: ${result.fatal}`); } }) .catch(() => {}); }, [setDomainGraph]); return ( ); } function DashboardContent({ accessToken, loadError, graphIssues, }: { accessToken: string; loadError: string | null; graphIssues: GraphIssue[]; }) { const graph = useDashboardStore((s) => s.graph); const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); const tourActive = useDashboardStore((s) => s.tourActive); const persona = useDashboardStore((s) => s.persona); const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen); const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded); const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer); const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer); const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen); const togglePathFinder = useDashboardStore((s) => s.togglePathFinder); const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters); const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter); const detailLevel = useDashboardStore((s) => s.detailLevel); const setDetailLevel = useDashboardStore((s) => s.setDetailLevel); const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView); const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [sidebarTab, setSidebarTab] = useState("info"); const [showOnboarding, setShowOnboarding] = useState(shouldShowOnboarding); const dismissOnboarding = useCallback((remember: boolean) => { if (remember && typeof window !== "undefined") { window.localStorage.setItem(ONBOARDING_DISMISSED_KEY, "1"); } setShowOnboarding(false); }, []); const viewMode = useDashboardStore((s) => s.viewMode); const setViewMode = useDashboardStore((s) => s.setViewMode); const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); const domainGraph = useDashboardStore((s) => s.domainGraph); const layoutIssues = useDashboardStore((s) => s.layoutIssues); const isMobile = useIsMobile(); const { t } = useI18n(); const allIssues = useMemo( () => [...graphIssues, ...layoutIssues], [graphIssues, layoutIssues], ); useEffect(() => { if (selectedNodeId) setSidebarTab("info"); }, [selectedNodeId]); // Define keyboard shortcuts const shortcuts = useMemo( () => [ // Help { key: "?", shiftKey: true, description: t.keyboardShortcuts.showHelp, action: () => setShowKeyboardHelp((prev) => !prev), category: "General", }, // Navigation { key: "Escape", description: t.keyboardShortcuts.escapeDesc, action: () => { // Read from store at invocation time to avoid stale closures const state = useDashboardStore.getState(); if (state.pathFinderOpen) { state.togglePathFinder(); } else if (state.filterPanelOpen) { state.toggleFilterPanel(); } else if (state.exportMenuOpen) { state.toggleExportMenu(); } else if (state.codeViewerExpanded) { state.collapseCodeViewer(); } else if (state.codeViewerOpen) { state.closeCodeViewer(); } else if (state.selectedNodeId) { state.selectNode(null); } else if (state.navigationLevel === "layer-detail") { state.navigateToOverview(); } else if (state.tourActive) { state.stopTour(); } else { setShowKeyboardHelp(false); } }, category: "Navigation", }, { key: "/", description: t.keyboardShortcuts.focusSearch, action: () => { const searchInput = document.querySelector( '[data-testid="search-input"]' ); searchInput?.focus(); }, category: "Navigation", }, // Tour controls { key: "ArrowRight", description: t.keyboardShortcuts.nextStep, action: () => { const state = useDashboardStore.getState(); if (state.tourActive) { state.nextTourStep(); } }, category: "Tour", }, { key: "ArrowLeft", description: t.keyboardShortcuts.prevStep, action: () => { const state = useDashboardStore.getState(); if (state.tourActive) { state.prevTourStep(); } }, category: "Tour", }, // View toggles { key: "d", description: t.keyboardShortcuts.toggleDiff, action: () => { const state = useDashboardStore.getState(); state.toggleDiffMode(); }, category: "View", }, { key: "f", description: t.keyboardShortcuts.toggleFilter, action: () => { const state = useDashboardStore.getState(); state.toggleFilterPanel(); }, category: "View", }, { key: "e", description: t.keyboardShortcuts.toggleExport, action: () => { const state = useDashboardStore.getState(); state.toggleExportMenu(); }, category: "View", }, { key: "p", description: t.keyboardShortcuts.openPathFinder, action: () => { const state = useDashboardStore.getState(); state.togglePathFinder(); }, category: "View", }, ], [t] ); // Register keyboard shortcuts useKeyboardShortcuts(shortcuts); // Determine sidebar content // NodeInfo always takes priority when a node is selected. // Learn mode adds LearnPanel below it; otherwise ProjectOverview shows when idle. const isLearnMode = tourActive || persona === "junior"; const infoSidebarContent = ( <> {selectedNodeId && } {isLearnMode && ( )} {!selectedNodeId && !isLearnMode && } ); const sidebarContent = (
{(["info", "files"] as const).map((tab) => ( ))}
{sidebarTab === "files" ? : infoSidebarContent}
); if (isMobile) { return ( ); } return (
{/* Header */}
{/* Left — fixed */}

{graph?.project.name ?? t.common.appName}

{graph && !isKnowledgeGraph && domainGraph && ( <>
)}
{/* Middle — scrollable legends */}
{/* Detail level: file view (architecture) / class view (code structure) */} {!isKnowledgeGraph && viewMode !== "domain" && ( <>
{detailLevel === "class" && ( )} )}
{(isKnowledgeGraph ? [ { key: "knowledge" as const, label: t.nodeTypeLabels.all, color: "var(--color-node-article)" }, ] : [ { key: "code" as const, label: t.nodeTypeLabels.code, color: "var(--color-node-file)" }, { key: "config" as const, label: t.nodeTypeLabels.config, color: "var(--color-node-config)" }, { key: "docs" as const, label: t.nodeTypeLabels.docs, color: "var(--color-node-document)" }, { key: "infra" as const, label: t.nodeTypeLabels.infra, color: "var(--color-node-service)" }, { key: "data" as const, label: t.nodeTypeLabels.data, color: "var(--color-node-table)" }, { key: "domain" as const, label: t.nodeTypeLabels.domain, color: "var(--color-node-concept)" }, { key: "knowledge" as const, label: t.nodeTypeLabels.knowledge, color: "var(--color-node-article)" }, ]).map((cat) => ( ))}
{/* Right — fixed actions */}
{/* Search */} {/* Validation warning banner */} {allIssues.length > 0 && !loadError && ( )} {/* Error banner */} {loadError && (
{loadError}
)} {/* Main content: Graph + Sidebar */}
{/* Graph area */}
{viewMode === "knowledge" ? ( ) : viewMode === "domain" && domainGraph ? ( ) : ( )}
{t.common.pressKeyboard}
{/* Right sidebar — telescopes at narrower widths */} {/* Code viewer slide-up overlay (collapsed state) */} {codeViewerOpen && !codeViewerExpanded && (
)}
{/* Expanded code viewer modal */} {codeViewerOpen && codeViewerExpanded && (
event.stopPropagation()} >
)} {/* Keyboard shortcuts help modal */} {showKeyboardHelp && ( setShowKeyboardHelp(false)} /> )} {/* Path Finder Modal — only mounted when open so its chunk is lazy-loaded on demand. */} {pathFinderOpen && ( )} {/* First-visit onboarding overlay — only mounted when needed so its chunk is lazy-loaded on demand. */} {showOnboarding && ( )}
); } export default App;