import { Layout } from 'antd'; import { useEffect, useMemo, useState } from 'react'; import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useGestureLayer, useGesturePageState, useSearchLayer } from '../../../layer'; import { useAppStore } from '../../../store'; import { useTokenAccess } from '../tokenAccess'; import { syncAppConfigFromServer, useAppConfig } from '../appConfig'; import { ChatRuntimeBridgeV2 } from '../ChatRuntimeBridgeV2'; import { useUnreadCounts } from '../chatV2/hooks/useUnreadCounts'; import { matchesShortcut, isTypingTarget, scrollToElement } from '../mainView/utils'; import { MainContent } from '../MainContent'; import { MainHeader } from '../MainHeader'; import { MainSidebar } from '../MainSidebar'; import { buildSearchOptions } from './buildSearchOptions'; import { MainLayoutContextProvider } from './MainLayoutContext'; import { useMainLayoutData } from './useMainLayoutData'; import '../MainLayout.css'; import { buildApiMenuItems, buildApisPath, buildChatMenuItems, buildChatPath, buildDocsMenuItems, buildDocsPath, buildPlanMenuItems, buildPlansPath, buildPlayMenuItems, buildPlayPath, buildSavedLayoutPath, DOCS_DEFAULT_FOLDER, PLAN_MENU_ANCHOR_IDS, renderSidebarIntro, resolveCurrentPageDescriptor, resolvePlanQuickFilterMenu, resolvePlayOpenKeys, resolveSavedLayoutIdFromMenuKey, resolveTopMenuPath, type ApiSectionKey, type ChatSectionKey, type PlanSectionKey, type PlaySidebarKey, type TopMenuKey, } from '../routes'; function parseRoute(pathname: string): { topMenu: TopMenuKey; docsMenu: string; apiMenu: ApiSectionKey; planMenu: PlanSectionKey; chatMenu: ChatSectionKey; playMenu: PlaySidebarKey; } { const segments = pathname.split('/').filter(Boolean); const [top, first, second] = segments; if (top === 'docs') { return { topMenu: 'docs', docsMenu: first || DOCS_DEFAULT_FOLDER, apiMenu: 'components', planMenu: 'all', chatMenu: 'live', playMenu: 'layout', }; } if (top === 'apis' && (first === 'components' || first === 'widgets')) { return { topMenu: 'apis', docsMenu: DOCS_DEFAULT_FOLDER, apiMenu: first, planMenu: 'all', chatMenu: 'live', playMenu: 'layout', }; } if ( top === 'plans' && (first === 'all' || first === 'in-progress' || first === 'done' || first === 'error' || first === 'release' || first === 'release-review' || first === 'board' || first === 'charts' || first === 'schedule' || first === 'history' || first === 'server-command') ) { return { topMenu: 'plans', docsMenu: DOCS_DEFAULT_FOLDER, apiMenu: 'components', planMenu: first, chatMenu: 'live', playMenu: 'layout', }; } if (top === 'chat' && (first === 'live' || first === 'changes' || first === 'errors' || first === 'manage')) { return { topMenu: 'chat', docsMenu: DOCS_DEFAULT_FOLDER, apiMenu: 'components', planMenu: 'all', chatMenu: first, playMenu: 'layout', }; } if (top === 'play' && first === 'layout') { return { topMenu: 'play', docsMenu: DOCS_DEFAULT_FOLDER, apiMenu: 'components', planMenu: 'all', chatMenu: 'live', playMenu: first, }; } if (top === 'play' && first === 'layout-record' && second) { return { topMenu: 'play', docsMenu: DOCS_DEFAULT_FOLDER, apiMenu: 'components', planMenu: 'all', chatMenu: 'live', playMenu: `layout-record:${second}`, }; } return { topMenu: 'plans', docsMenu: DOCS_DEFAULT_FOLDER, apiMenu: 'components', planMenu: 'all', chatMenu: 'live', playMenu: 'layout', }; } function isRestrictedTopMenu(topMenu: TopMenuKey, hasAccess: boolean) { return !hasAccess && topMenu !== 'docs'; } function resolveSidebarOpenKeys( topMenu: TopMenuKey, hasAccess: boolean, planMenu: PlanSectionKey, chatMenu: ChatSectionKey, ) { if (!hasAccess) { return ['docs-group']; } if (topMenu === 'docs') { return ['docs-group']; } if (topMenu === 'apis') { return ['api-group']; } if (topMenu === 'play') { return resolvePlayOpenKeys(); } if (topMenu === 'plans') { return planMenu === 'server-command' ? ['server-group'] : ['plan-group']; } if (chatMenu === 'errors') { return ['app-log-group']; } return chatMenu === 'manage' ? ['chat-manage-group'] : ['codex-live-group']; } export function MainLayout() { const location = useLocation(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { setCurrentPage, setFocusedComponentId } = useAppStore(); const { hasAccess } = useTokenAccess(); const appConfig = useAppConfig(); const { openSearch, setOptions: setSearchOptions } = useSearchLayer(); const layoutData = useMainLayoutData(); const routeState = useMemo(() => parseRoute(location.pathname), [location.pathname]); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [contentExpanded, setContentExpanded] = useState(false); const [isMobileViewport, setIsMobileViewport] = useState(false); const [sidebarOpenKeys, setSidebarOpenKeys] = useState( resolveSidebarOpenKeys(routeState.topMenu, hasAccess, routeState.planMenu, routeState.chatMenu), ); const [activePlanQuickFilter, setActivePlanQuickFilter] = useState< 'working' | 'release-pending-main' | 'automation-failed' | null >(routeState.planMenu === 'release' ? 'release-pending-main' : null); const [planQuickFilterRequestKey, setPlanQuickFilterRequestKey] = useState(routeState.planMenu === 'release' ? 1 : 0); const { componentSampleEntries, widgetSampleEntries, componentSamples, widgetSamples, docsDocuments, savedLayouts, setSavedLayouts, docFolders } = layoutData; const { chatUnreadCount } = useUnreadCounts(); useEffect(() => { void syncAppConfigFromServer(); }, []); useEffect(() => { const mediaQuery = window.matchMedia('(max-width: 768px)'); const updateViewport = () => { setIsMobileViewport(mediaQuery.matches); }; updateViewport(); mediaQuery.addEventListener('change', updateViewport); return () => { mediaQuery.removeEventListener('change', updateViewport); }; }, []); useEffect(() => { if (!isMobileViewport) { setSidebarCollapsed(false); return; } setSidebarCollapsed(routeState.topMenu !== 'docs'); }, [isMobileViewport, routeState.topMenu]); useEffect(() => { setSidebarOpenKeys(resolveSidebarOpenKeys(routeState.topMenu, hasAccess, routeState.planMenu, routeState.chatMenu)); }, [hasAccess, routeState.chatMenu, routeState.planMenu, routeState.topMenu]); useEffect(() => { if (docFolders.length > 0 && routeState.topMenu === 'docs' && !docFolders.includes(routeState.docsMenu)) { navigate(buildDocsPath(docFolders[0]), { replace: true }); } }, [docFolders, navigate, routeState.docsMenu, routeState.topMenu]); useEffect(() => { const savedLayoutId = resolveSavedLayoutIdFromMenuKey(routeState.playMenu); if (savedLayoutId && !savedLayouts.some((record) => record.id === savedLayoutId)) { navigate(buildPlayPath('layout'), { replace: true }); } }, [navigate, routeState.playMenu, savedLayouts]); useEffect(() => { if (!isRestrictedTopMenu(routeState.topMenu, hasAccess)) { return; } navigate(`${buildDocsPath(DOCS_DEFAULT_FOLDER)}${location.search}`, { replace: true }); }, [hasAccess, location.search, navigate, routeState.topMenu]); useEffect(() => { if (routeState.topMenu !== 'plans') { setActivePlanQuickFilter(null); return; } if (routeState.planMenu === 'release') { setActivePlanQuickFilter('release-pending-main'); return; } setActivePlanQuickFilter((current) => (current === 'working' || current === 'automation-failed' ? current : null)); }, [routeState.planMenu, routeState.topMenu]); useGesturePageState('anyway'); useGestureLayer({ id: 'main-layout', enabled: !(isMobileViewport && routeState.topMenu === 'docs' && routeState.docsMenu === 'worklogs'), gestures: [ { id: 'mobile-top-right-pull-alert', activeStates: ['anyway'], mobileOnly: true, trigger: 'pull-down-top-right', onTrigger: () => { openSearch(); }, }, { id: 'mobile-middle-right-search-window', activeStates: ['anyway'], mobileOnly: true, trigger: 'pull-left-middle-right', onTrigger: () => { openSearch('window'); }, }, ], }); useEffect(() => { const handleWindowKeyDown = (event: KeyboardEvent) => { if (event.repeat || isTypingTarget(event.target)) { return; } if (matchesShortcut(event, appConfig.gestureShortcuts.openWindowSearch)) { event.preventDefault(); openSearch('window'); return; } if (matchesShortcut(event, appConfig.gestureShortcuts.openSearch)) { event.preventDefault(); openSearch(); } }; window.addEventListener('keydown', handleWindowKeyDown); return () => { window.removeEventListener('keydown', handleWindowKeyDown); }; }, [appConfig.gestureShortcuts.openSearch, appConfig.gestureShortcuts.openWindowSearch, openSearch]); const selectedDocs = useMemo( () => docsDocuments.filter((document) => document.folder === routeState.docsMenu), [docsDocuments, routeState.docsMenu], ); const searchOptions = useMemo( () => buildSearchOptions({ componentSamples, widgetSamples, docFolders, docsDocuments, hasAccess, navigateTo: (path) => { navigate(path); }, setFocusedComponentId, requestPlanQuickFilter: (filter) => { setActivePlanQuickFilter(filter); setPlanQuickFilterRequestKey((previous) => previous + 1); }, }), [componentSamples, docFolders, docsDocuments, hasAccess, navigate, setFocusedComponentId, widgetSamples], ); useEffect(() => { setSearchOptions(searchOptions); }, [searchOptions, setSearchOptions]); useEffect(() => { setCurrentPage( resolveCurrentPageDescriptor({ topMenu: routeState.topMenu, docsMenu: routeState.docsMenu, apiMenu: routeState.apiMenu, planMenu: routeState.planMenu, chatMenu: routeState.chatMenu, playMenu: routeState.playMenu, savedLayouts, }), ); }, [routeState, savedLayouts, setCurrentPage]); const currentDocsFolder = docFolders.includes(routeState.docsMenu) ? routeState.docsMenu : docFolders[0] ?? DOCS_DEFAULT_FOLDER; const sidebarIntro = renderSidebarIntro(routeState.topMenu); const apiMenuItems = useMemo(() => buildApiMenuItems(), []); const docsMenuItems = useMemo(() => buildDocsMenuItems(docFolders), [docFolders]); const planMenuItems = useMemo(() => buildPlanMenuItems(hasAccess), [hasAccess]); const chatMenuItems = useMemo(() => buildChatMenuItems(hasAccess, chatUnreadCount), [chatUnreadCount, hasAccess]); const playMenuItems = useMemo(() => buildPlayMenuItems(savedLayouts), [savedLayouts]); const showInlineMobileDocsSidebar = isMobileViewport && routeState.topMenu === 'docs'; const initialSelectedPlanId = Number(searchParams.get('planId')); const initialSelectedWorkId = searchParams.get('workId'); return ( {contentExpanded ? null : ( { setSidebarCollapsed((previous) => !previous); }} onToggleContentExpanded={() => { setContentExpanded((previous) => !previous); }} onChangeTopMenu={(menu) => { navigate(resolveTopMenuPath(menu, currentDocsFolder)); setSidebarCollapsed(false); }} onOpenPlanQuickFilter={(filter) => { const targetPlanMenu = resolvePlanQuickFilterMenu(filter); setActivePlanQuickFilter(filter); setPlanQuickFilterRequestKey((previous) => previous + 1); navigate(buildPlansPath(targetPlanMenu)); setSidebarCollapsed(isMobileViewport); scrollToElement(PLAN_MENU_ANCHOR_IDS[targetPlanMenu] ?? 'plan-menu-all'); }} /> )} {contentExpanded || (isMobileViewport && sidebarCollapsed && !showInlineMobileDocsSidebar) ? null : ( { navigate(buildApisPath(key as ApiSectionKey)); if (isMobileViewport) { setSidebarCollapsed(true); } }} onSelectDocsMenu={(key) => { navigate(buildDocsPath(key)); if (isMobileViewport && !showInlineMobileDocsSidebar) { setSidebarCollapsed(true); } }} onSelectPlanMenu={(key) => { setActivePlanQuickFilter(key === 'release' ? 'release-pending-main' : null); setPlanQuickFilterRequestKey((previous) => previous + 1); navigate(buildPlansPath(key)); if (isMobileViewport) { setSidebarCollapsed(true); } }} onSelectChatMenu={(key) => { navigate(buildChatPath(key)); if (isMobileViewport) { setSidebarCollapsed(true); } }} onSelectPlayMenu={(key) => { const savedLayoutId = resolveSavedLayoutIdFromMenuKey(key); navigate(savedLayoutId ? buildSavedLayoutPath(savedLayoutId) : buildPlayPath(key as 'layout')); if (isMobileViewport) { setSidebarCollapsed(true); } }} introColor={sidebarIntro.color} introTag={sidebarIntro.tag} introDescription={sidebarIntro.description} /> )} {isMobileViewport && !sidebarCollapsed && !showInlineMobileDocsSidebar ? null : ( setContentExpanded((previous) => !previous)}> )} ); }