mirror of
https://github.com/langgenius/dify.git
synced 2026-06-17 23:21:12 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: chariri <w@chariri.moe> Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
338 lines
14 KiB
TypeScript
338 lines
14 KiB
TypeScript
'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 = <span aria-hidden className="i-ri-question-line size-4 shrink-0" />
|
|
|
|
function SecondarySidebarHelpMenu({
|
|
triggerClassName,
|
|
}: {
|
|
triggerClassName?: string
|
|
}) {
|
|
return (
|
|
<HelpMenu
|
|
triggerIcon={secondarySidebarHelpTriggerIcon}
|
|
triggerClassName={triggerClassName}
|
|
/>
|
|
)
|
|
}
|
|
|
|
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 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 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<string>(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<ReturnType<typeof setTimeout> | null>(null)
|
|
const detailNavigationTransitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const isDetailNavigationHoverPreviewOpen = isCollapsedDetailNavigation && detailNavigationHoverPreviewOpen
|
|
const detailNavigationVisibleExpanded = detailNavigationExpanded || isDetailNavigationHoverPreviewOpen
|
|
const bottomNavigationExpanded = !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<MainNavItem[]>(() => [
|
|
...(!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 = () => (
|
|
<Link
|
|
href="/"
|
|
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
|
aria-label={systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
|
|
>
|
|
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
|
? (
|
|
<img
|
|
src={systemFeatures.branding.workspace_logo}
|
|
className="block h-5.5 w-auto object-contain"
|
|
alt=""
|
|
/>
|
|
)
|
|
: <DifyLogo alt="" />}
|
|
</Link>
|
|
)
|
|
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
'relative flex h-full shrink-0',
|
|
detailNavigationTransitionDisabled ? 'transition-none' : 'transition-all',
|
|
isDetailNavigationHoverPreviewOpen ? 'overflow-visible' : 'overflow-hidden',
|
|
showDetailNavigation
|
|
? detailNavigationExpanded
|
|
? 'w-[248px] bg-background-body p-1'
|
|
: 'w-16 bg-background-body p-1'
|
|
: 'w-60 flex-col',
|
|
'bg-background-body',
|
|
className,
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'flex min-h-0 flex-1 flex-col',
|
|
showDetailNavigation && (
|
|
isDetailNavigationHoverPreviewOpen
|
|
? 'absolute top-1 bottom-1 left-1 z-40 w-60 overflow-hidden rounded-lg border border-divider-subtle bg-components-panel-bg shadow-lg'
|
|
: 'overflow-hidden rounded-lg bg-components-panel-bg'
|
|
),
|
|
showDetailNavigation && (detailNavigationVisibleExpanded ? 'w-60' : 'w-14'),
|
|
)}
|
|
onMouseEnter={isCollapsedDetailNavigation ? openDetailNavigationHoverPreview : undefined}
|
|
onMouseLeave={isCollapsedDetailNavigation ? closeDetailNavigationHoverPreview : undefined}
|
|
>
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
{showDetailNavigation
|
|
? showAppDetailNavigation
|
|
? (
|
|
<AppDetailTop
|
|
expand={detailNavigationVisibleExpanded}
|
|
onToggle={handleToggleDetailNavigation}
|
|
/>
|
|
)
|
|
: (
|
|
<DatasetDetailTop
|
|
expand={detailNavigationVisibleExpanded}
|
|
onToggle={handleToggleDetailNavigation}
|
|
/>
|
|
)
|
|
: (
|
|
<>
|
|
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
|
|
{renderLogo()}
|
|
<MainNavSearchButton />
|
|
</div>
|
|
<div className="p-2">
|
|
<WorkspaceCard />
|
|
</div>
|
|
</>
|
|
)}
|
|
{showDetailNavigation
|
|
? showAppDetailNavigation
|
|
? <AppDetailSection expand={detailNavigationVisibleExpanded} />
|
|
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
|
|
: (
|
|
<>
|
|
<nav className="flex flex-col gap-px p-2">
|
|
{navItems.map(item => (
|
|
<MainNavLink key={item.href} item={item} pathname={pathname} />
|
|
))}
|
|
</nav>
|
|
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
|
|
</>
|
|
)}
|
|
{showEnvTag && detailNavigationVisibleExpanded && (
|
|
<div className="relative z-30 mt-auto shrink-0 px-3 pb-2">
|
|
<EnvNav />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className={cn(
|
|
!bottomNavigationExpanded
|
|
? 'flex w-full shrink-0 flex-col items-center gap-0.5 rounded-lg px-2 pt-1 pb-3'
|
|
: cn(
|
|
'flex w-60 items-center justify-between py-3 pr-1 pl-3',
|
|
showDetailNavigation
|
|
? 'bg-components-panel-bg'
|
|
: 'bg-gradient-to-b from-background-body-transparent to-background-body to-50% backdrop-blur-[2px]',
|
|
),
|
|
)}
|
|
>
|
|
{!bottomNavigationExpanded
|
|
? (
|
|
<>
|
|
<SecondarySidebarHelpMenu triggerClassName="mb-2" />
|
|
<AccountSection compact />
|
|
</>
|
|
)
|
|
: (
|
|
<>
|
|
<div className="flex min-w-0 items-center gap-1 overflow-hidden">
|
|
<AccountSection />
|
|
</div>
|
|
{(!showDetailNavigation || detailNavigationVisibleExpanded) && (
|
|
<div className="flex shrink-0 items-center justify-center rounded-full p-1">
|
|
{showDetailNavigation ? <SecondarySidebarHelpMenu /> : <HelpMenu />}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|
|
|
|
export default MainNav
|