'use client' import type { MainNavItem, MainNavProps } from './types' import { cn } from '@langgenius/dify-ui/cn' import { useHotkey } from '@tanstack/react-hotkeys' import { useSuspenseQuery } from '@tanstack/react-query' import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import AppDetailSection from '@/app/components/app-sidebar/app-detail-section' import AppDetailTop from '@/app/components/app-sidebar/app-detail-top' import DatasetDetailSection from '@/app/components/app-sidebar/dataset-detail-section' import DatasetDetailTop from '@/app/components/app-sidebar/dataset-detail-top' import { useStore as useAppStore } from '@/app/components/app/store' import DifyLogo from '@/app/components/base/logo/dify-logo' import EnvNav from '@/app/components/header/env-nav' import { buildIntegrationPath } from '@/app/components/integrations/routes' import { useAppContext } from '@/context/app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' import { usePathname } from '@/next/navigation' import AccountSection from './components/account-section' import HelpMenu from './components/help-menu' import MainNavLink from './components/nav-link' import { MainNavSearchButton } from './components/search-button' import WebAppsSection from './components/web-apps-section' import { WorkspaceCard } from './components/workspace-card' const DATASET_COLLECTION_ROUTES = new Set(['create', 'create-from-pipeline', 'connect']) const DATASET_DOCUMENT_CREATION_ROUTES = new Set(['create', 'create-from-pipeline']) const DETAIL_SIDEBAR_STORAGE_KEY = 'app-detail-collapse-or-expand' const secondarySidebarHelpTriggerIcon = function SecondarySidebarHelpMenu({ triggerClassName, }: { triggerClassName?: string }) { return ( ) } const isDatasetDetailPathname = (pathname: string) => { const [section, datasetId, subSection, action] = pathname.split('/').filter(Boolean) if (section !== 'datasets' || !datasetId) return false if (DATASET_COLLECTION_ROUTES.has(datasetId)) return false if (subSection === 'documents' && action && DATASET_DOCUMENT_CREATION_ROUTES.has(action)) return false return true } const isSnippetDetailPathname = (pathname: string) => { const [section, snippetId] = pathname.split('/').filter(Boolean) return section === 'snippets' && !!snippetId } const MainNav = ({ className, }: MainNavProps) => { const { t } = useTranslation() const pathname = usePathname() const { langGeniusVersionInfo, isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor } = useAppContext() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT' const showAppDetailNavigation = !isCurrentWorkspaceDatasetOperator && pathname.startsWith('/app/') const showDatasetDetailNavigation = isDatasetDetailPathname(pathname) const showSnippetDetailBottomNavigation = isSnippetDetailPathname(pathname) const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation const { hasAppDetail, appSidebarExpand, setAppDetail, setAppSidebarExpand } = useAppStore(useShallow(state => ({ hasAppDetail: !!state.appDetail, appSidebarExpand: state.appSidebarExpand, setAppDetail: state.setAppDetail, setAppSidebarExpand: state.setAppSidebarExpand, }))) const [storedDetailSidebarExpand, setStoredDetailSidebarExpand] = useLocalStorage(DETAIL_SIDEBAR_STORAGE_KEY, 'expand', { raw: true }) const detailNavigationMode = appSidebarExpand === 'collapse' || (!appSidebarExpand && storedDetailSidebarExpand === 'collapse') ? 'collapse' : 'expand' const detailNavigationExpanded = detailNavigationMode === 'expand' const isCollapsedDetailNavigation = showDetailNavigation && !detailNavigationExpanded const [detailNavigationHoverPreviewOpen, setDetailNavigationHoverPreviewOpen] = useState(false) const [detailNavigationTransitionDisabled, setDetailNavigationTransitionDisabled] = useState(false) const closeDetailNavigationHoverPreviewTimerRef = useRef | null>(null) const detailNavigationTransitionTimerRef = useRef | null>(null) const isDetailNavigationHoverPreviewOpen = isCollapsedDetailNavigation && detailNavigationHoverPreviewOpen const detailNavigationVisibleExpanded = detailNavigationExpanded || isDetailNavigationHoverPreviewOpen const bottomNavigationExpanded = showSnippetDetailBottomNavigation ? false : !showDetailNavigation || detailNavigationVisibleExpanded const handleToggleDetailNavigation = useCallback(() => { if (isDetailNavigationHoverPreviewOpen) { if (detailNavigationTransitionTimerRef.current) clearTimeout(detailNavigationTransitionTimerRef.current) setDetailNavigationTransitionDisabled(true) setDetailNavigationHoverPreviewOpen(false) setAppSidebarExpand('expand') detailNavigationTransitionTimerRef.current = setTimeout(() => { setDetailNavigationTransitionDisabled(false) }, 200) return } setDetailNavigationHoverPreviewOpen(false) setAppSidebarExpand(detailNavigationExpanded ? 'collapse' : 'expand') }, [detailNavigationExpanded, isDetailNavigationHoverPreviewOpen, setAppSidebarExpand]) const openDetailNavigationHoverPreview = useCallback(() => { if (!isCollapsedDetailNavigation) return if (closeDetailNavigationHoverPreviewTimerRef.current) clearTimeout(closeDetailNavigationHoverPreviewTimerRef.current) setDetailNavigationHoverPreviewOpen(true) }, [isCollapsedDetailNavigation]) const closeDetailNavigationHoverPreview = useCallback(() => { if (closeDetailNavigationHoverPreviewTimerRef.current) clearTimeout(closeDetailNavigationHoverPreviewTimerRef.current) closeDetailNavigationHoverPreviewTimerRef.current = setTimeout(() => { setDetailNavigationHoverPreviewOpen(false) }, 120) }, []) useEffect(() => { return () => { if (closeDetailNavigationHoverPreviewTimerRef.current) clearTimeout(closeDetailNavigationHoverPreviewTimerRef.current) if (detailNavigationTransitionTimerRef.current) clearTimeout(detailNavigationTransitionTimerRef.current) } }, []) useEffect(() => { if (!showDetailNavigation) return setStoredDetailSidebarExpand(detailNavigationMode) }, [detailNavigationMode, setStoredDetailSidebarExpand, showDetailNavigation]) useEffect(() => { if (pathname.startsWith('/app/') || !hasAppDetail) return setAppDetail() }, [hasAppDetail, pathname, setAppDetail]) useHotkey('Mod+B', (e) => { if (!showDetailNavigation) return e.preventDefault() handleToggleDetailNavigation() }, { ignoreInputs: false, }) const navItems = useMemo(() => [ ...(!isCurrentWorkspaceDatasetOperator ? [ { href: '/', label: t('mainNav.home', { ns: 'common' }), active: (path: string) => path === '/' || path === '/explore/apps', icon: 'i-custom-vender-main-nav-home', activeIcon: 'i-custom-vender-main-nav-home-active', }, { href: '/apps', label: t('menus.apps', { ns: 'common' }), active: (path: string) => path.startsWith('/apps') || path.startsWith('/app/') || path.startsWith('/snippets'), icon: 'i-custom-vender-main-nav-studio', activeIcon: 'i-custom-vender-main-nav-studio-active', }, ] : []), ...((isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) ? [ { href: '/datasets', label: t('menus.datasets', { ns: 'common' }), active: (path: string) => path.startsWith('/datasets'), icon: 'i-custom-vender-main-nav-knowledge', activeIcon: 'i-custom-vender-main-nav-knowledge-active', }, ] : []), ...(!isCurrentWorkspaceDatasetOperator ? [ { href: buildIntegrationPath('provider'), label: t('mainNav.integrations', { ns: 'common' }), active: (path: string) => path.startsWith('/integrations') || path.startsWith('/tools'), icon: 'i-custom-vender-main-nav-integrations', activeIcon: 'i-custom-vender-main-nav-integrations-active', }, ] : []), { href: '/marketplace', label: t('mainNav.marketplace', { ns: 'common' }), active: path => path.startsWith('/marketplace') || path.startsWith('/plugins'), icon: 'i-custom-vender-main-nav-marketplace', activeIcon: 'i-custom-vender-main-nav-marketplace-active', }, ], [isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t]) const renderLogo = () => ( {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo ? ( ) : } ) return ( ) } export default MainNav