diff --git a/web/app/components/explore/sidebar/app-nav-item/index.tsx b/web/app/components/explore/sidebar/app-nav-item/index.tsx
index c2156d9dec..6c1e96a393 100644
--- a/web/app/components/explore/sidebar/app-nav-item/index.tsx
+++ b/web/app/components/explore/sidebar/app-nav-item/index.tsx
@@ -11,6 +11,7 @@ import { useRouter } from '@/next/navigation'
type IAppNavItemProps = {
isMobile: boolean
+ variant?: 'default' | 'mainNav'
name: string
id: string
icon_type: AppIconType | null
@@ -26,6 +27,7 @@ type IAppNavItemProps = {
export default function AppNavItem({
isMobile,
+ variant = 'default',
name,
id,
icon_type,
@@ -42,11 +44,21 @@ export default function AppNavItem({
const url = `/explore/installed/${id}`
const ref = useRef(null)
const isHovering = useHover(ref)
+ const isMainNav = variant === 'mainNav'
+
return (
{
router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation().
}}
@@ -54,9 +66,9 @@ export default function AppNavItem({
{isMobile &&
}
{!isMobile && (
<>
-
-
-
{name}
+
e.stopPropagation()}>
{
+ const { userProfile } = useAppContext()
+
+ return (
+ }
+ variant="mainNav"
+ trigger={({ isOpen, ariaLabel }) => (
+
+
+ {userProfile.name}
+
+ )}
+ />
+ )
+}
+
+export default AccountSection
diff --git a/web/app/components/main-nav/components/help-menu.tsx b/web/app/components/main-nav/components/help-menu.tsx
new file mode 100644
index 0000000000..b9e54c2ce7
--- /dev/null
+++ b/web/app/components/main-nav/components/help-menu.tsx
@@ -0,0 +1,117 @@
+'use client'
+
+import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLinkItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
+import { useSuspenseQuery } from '@tanstack/react-query'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import AccountAbout from '@/app/components/header/account-about'
+import Compliance from '@/app/components/header/account-dropdown/compliance'
+import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
+import Support from '@/app/components/header/account-dropdown/support'
+import GithubStar from '@/app/components/header/github-star'
+import Indicator from '@/app/components/header/indicator'
+import { IS_CLOUD_EDITION } from '@/config'
+import { useAppContext } from '@/context/app-context'
+import { useDocLink } from '@/context/i18n'
+import { env } from '@/env'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
+
+const HelpMenu = () => {
+ const { t } = useTranslation()
+ const docLink = useDocLink()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
+ const { langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
+ const [aboutVisible, setAboutVisible] = useState(false)
+ const [open, setOpen] = useState(false)
+
+ return (
+ <>
+
+
+
+
+
+ {!systemFeatures.branding.enabled && (
+ <>
+
+
+ }
+ />
+
+ setOpen(false)} />
+ {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && }
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+ )}
+ />
+
+ {env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
+
{
+ setAboutVisible(true)
+ setOpen(false)
+ }}
+ >
+
+ {t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}
+
+
+ )}
+ />
+
+ )}
+
+ >
+ )}
+
+
+ {aboutVisible &&
setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />}
+ >
+ )
+}
+
+export default HelpMenu
diff --git a/web/app/components/main-nav/components/nav-link.tsx b/web/app/components/main-nav/components/nav-link.tsx
new file mode 100644
index 0000000000..4ae1757573
--- /dev/null
+++ b/web/app/components/main-nav/components/nav-link.tsx
@@ -0,0 +1,54 @@
+'use client'
+
+import type { MainNavItem } from '../types'
+import { cn } from '@langgenius/dify-ui/cn'
+import Link from '@/next/link'
+
+const navItemClassName = 'group relative flex h-9 items-center gap-2 rounded-xl p-2 transition-colors'
+
+const activeNavItemClassName = [
+ 'overflow-hidden border border-transparent',
+ 'bg-[linear-gradient(98.077deg,var(--color-components-main-nav-glass-surface-first)_0%,var(--color-components-main-nav-glass-surface-middle-1)_17.98%,var(--color-components-main-nav-glass-surface-middle-2)_58.75%,var(--color-components-main-nav-glass-surface-end)_101.09%)]',
+ 'system-md-semibold text-components-main-nav-text-active backdrop-blur-[5px]',
+ 'shadow-[0px_4px_8px_0px_var(--color-components-main-nav-glass-shadow-reflection-glow),0px_12px_16px_-4px_var(--color-shadow-shadow-5),0px_4px_6px_-2px_var(--color-shadow-shadow-1),0px_10px_16px_-4px_var(--color-components-main-nav-glass-shadow-reflection)]',
+ 'main-nav-active-edge',
+].join(' ')
+
+const inactiveNavItemClassName = 'system-md-medium bg-components-main-nav-nav-button-bg text-components-main-nav-text hover:bg-state-base-hover hover:text-components-main-nav-text'
+
+const NavIcon = ({
+ icon,
+ className,
+}: {
+ icon: string
+ className?: string
+}) => (
+
+)
+
+type MainNavLinkProps = {
+ item: MainNavItem
+ pathname: string
+}
+
+const MainNavLink = ({
+ item,
+ pathname,
+}: MainNavLinkProps) => {
+ const activated = item.active(pathname)
+
+ return (
+
+
+ {item.label}
+
+ )
+}
+
+export default MainNavLink
diff --git a/web/app/components/main-nav/components/search-button.tsx b/web/app/components/main-nav/components/search-button.tsx
new file mode 100644
index 0000000000..caebb553d6
--- /dev/null
+++ b/web/app/components/main-nav/components/search-button.tsx
@@ -0,0 +1,22 @@
+'use client'
+
+import { useTranslation } from 'react-i18next'
+import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
+
+const MainNavSearchButton = () => {
+ const { t } = useTranslation()
+
+ return (
+ window.dispatchEvent(new Event(GOTO_ANYTHING_OPEN_EVENT))}
+ >
+
+ ⌘K
+
+ )
+}
+
+export default MainNavSearchButton
diff --git a/web/app/components/main-nav/components/web-apps-section.tsx b/web/app/components/main-nav/components/web-apps-section.tsx
new file mode 100644
index 0000000000..8aa897e3be
--- /dev/null
+++ b/web/app/components/main-nav/components/web-apps-section.tsx
@@ -0,0 +1,142 @@
+'use client'
+
+import {
+ AlertDialog,
+ AlertDialogActions,
+ AlertDialogCancelButton,
+ AlertDialogConfirmButton,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogTitle,
+} from '@langgenius/dify-ui/alert-dialog'
+import { cn } from '@langgenius/dify-ui/cn'
+import { toast } from '@langgenius/dify-ui/toast'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import AppNavItem from '@/app/components/explore/sidebar/app-nav-item'
+import { usePathname } from '@/next/navigation'
+import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
+
+const WebAppsSection = () => {
+ const { t } = useTranslation()
+ const pathname = usePathname()
+ const { data, isPending } = useGetInstalledApps()
+ const installedApps = useMemo(() => data?.installed_apps ?? [], [data?.installed_apps])
+ const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp()
+ const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus()
+ const [searchVisible, setSearchVisible] = useState(false)
+ const [searchText, setSearchText] = useState('')
+ const [showConfirm, setShowConfirm] = useState(false)
+ const [currentId, setCurrentId] = useState('')
+
+ const filteredApps = useMemo(() => {
+ const normalizedSearch = searchText.trim().toLowerCase()
+ if (!normalizedSearch)
+ return installedApps
+
+ return installedApps.filter(item => item.app.name.toLowerCase().includes(normalizedSearch))
+ }, [installedApps, searchText])
+
+ const handleDelete = async () => {
+ await uninstallApp(currentId)
+ setShowConfirm(false)
+ toast.success(t('api.remove', { ns: 'common' }))
+ }
+
+ const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
+ await updatePinStatus({ appId: id, isPinned })
+ toast.success(t('api.success', { ns: 'common' }))
+ }
+
+ return (
+
+
+
setSearchVisible(value => !value)}
+ >
+ {t('sidebar.webApps', { ns: 'explore' })}
+
+
+
+ setSearchVisible(value => !value)}
+ >
+
+
+
+
+
+
+ {searchVisible && (
+
+ setSearchText(e.target.value)}
+ placeholder={t('mainNav.webApps.searchPlaceholder', { ns: 'common' })}
+ className="h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 system-sm-regular text-text-secondary outline-none placeholder:text-text-quaternary hover:border-components-input-border-hover focus:border-components-input-border-active"
+ />
+
+ )}
+
+ {isPending && (
+
{t('loading', { ns: 'common' })}
+ )}
+ {!isPending && filteredApps.length === 0 && (
+
+ {searchText ? t('mainNav.webApps.noResults', { ns: 'common' }) : t('sidebar.noApps.title', { ns: 'explore' })}
+
+ )}
+ {filteredApps.map(({ id, is_pinned, uninstallable, app }) => (
+
{
+ void handleUpdatePinStatus(id, !is_pinned)
+ }}
+ uninstallable={uninstallable}
+ onDelete={(id) => {
+ setCurrentId(id)
+ setShowConfirm(true)
+ }}
+ />
+ ))}
+
+
+
+
+
+ {t('sidebar.delete.title', { ns: 'explore' })}
+
+
+ {t('sidebar.delete.content', { ns: 'explore' })}
+
+
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+ {t('operation.confirm', { ns: 'common' })}
+
+
+
+
+
+ )
+}
+
+export default WebAppsSection
diff --git a/web/app/components/main-nav/components/workspace-card.tsx b/web/app/components/main-nav/components/workspace-card.tsx
new file mode 100644
index 0000000000..78f617a62c
--- /dev/null
+++ b/web/app/components/main-nav/components/workspace-card.tsx
@@ -0,0 +1,210 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import { cn } from '@langgenius/dify-ui/cn'
+import { toast } from '@langgenius/dify-ui/toast'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Plan } from '@/app/components/billing/type'
+import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
+import { useAppContext } from '@/context/app-context'
+import { useModalContext } from '@/context/modal-context'
+import { useProviderContext } from '@/context/provider-context'
+import { useWorkspacesContext } from '@/context/workspace-context'
+import { switchWorkspace } from '@/service/common'
+import { basePath } from '@/utils/var'
+import { formatCredits, getRemainingCredits, getWorkspaceInitial } from '../utils'
+import WorkspacePlanBadge from './workspace-plan-badge'
+
+const WorkspaceIcon = ({
+ name,
+ className,
+}: {
+ name?: string
+ className?: string
+}) => (
+
+ {getWorkspaceInitial(name)}
+
+)
+
+const WorkspaceMenuItemContent = ({
+ icon,
+ label,
+ trailing,
+}: {
+ icon: ReactNode
+ label: ReactNode
+ trailing?: ReactNode
+}) => (
+ <>
+ {icon}
+ {label}
+ {trailing}
+ >
+)
+
+const WorkspaceCard = () => {
+ const { t } = useTranslation()
+ const { currentWorkspace } = useAppContext()
+ const { workspaces } = useWorkspacesContext()
+ const { enableBilling, plan } = useProviderContext()
+ const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
+ const [open, setOpen] = useState(false)
+ const credits = getRemainingCredits(currentWorkspace.trial_credits, currentWorkspace.trial_credits_used)
+ const formattedCredits = formatCredits(credits)
+ const workspacePlan = (workspaces.find(workspace => workspace.current)?.plan || currentWorkspace.plan || plan.type) as Plan
+ const isFreePlan = plan.type === Plan.sandbox
+
+ const handlePlanClick = () => {
+ if (isFreePlan)
+ setShowPricingModal()
+ else
+ setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
+ }
+
+ const handleSwitchWorkspace = async (tenant_id: string) => {
+ try {
+ if (currentWorkspace.id === tenant_id)
+ return
+
+ await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } })
+ toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
+ location.assign(`${location.origin}${basePath}`)
+ }
+ catch {
+ toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
+ }
+ }
+
+ return (
+
+
+
setOpen(value => !value)}
+ >
+
+
+
+ {currentWorkspace.name}
+
+
+
+
+
+
+ {
+ e.stopPropagation()
+ setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
+ }}
+ >
+
+ {formattedCredits}
+ {t('mainNav.workspace.creditsUnit', { ns: 'common' })}
+
+ {enableBilling && (
+ {
+ e.stopPropagation()
+ handlePlanClick()
+ }}
+ >
+ {t('upgradeBtn.encourageShort', { ns: 'billing' })}
+
+ )}
+
+
+ {open && (
+
+
+
setOpen(false)}
+ >
+
+
{currentWorkspace.name}
+
+
+
+
+
{
+ setOpen(false)
+ setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
+ }}
+ >
+ } label={t('mainNav.workspace.settings', { ns: 'common' })} />
+
+
{
+ setOpen(false)
+ setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })
+ }}
+ >
+ } label={t('mainNav.workspace.inviteMembers', { ns: 'common' })} />
+
+
+ {workspaces.length > 0 && (
+
+
+ {t('mainNav.workspace.switchWorkspace', { ns: 'common' })}
+
+ {workspaces.map(workspace => (
+
{
+ setOpen(false)
+ void handleSwitchWorkspace(workspace.id)
+ }}
+ >
+ }
+ label={workspace.name}
+ trailing={workspace.current ? : undefined}
+ />
+
+ ))}
+
+ )}
+
+ )}
+
+ )
+}
+
+export default WorkspaceCard
diff --git a/web/app/components/main-nav/components/workspace-plan-badge.tsx b/web/app/components/main-nav/components/workspace-plan-badge.tsx
new file mode 100644
index 0000000000..35b9044578
--- /dev/null
+++ b/web/app/components/main-nav/components/workspace-plan-badge.tsx
@@ -0,0 +1,18 @@
+import Badge from '@/app/components/base/badge'
+import { Plan } from '@/app/components/billing/type'
+
+type WorkspacePlanBadgeProps = {
+ plan: Plan
+}
+
+const WorkspacePlanBadge = ({
+ plan,
+}: WorkspacePlanBadgeProps) => {
+ return (
+
+ {plan === Plan.professional ? 'pro' : plan}
+
+ )
+}
+
+export default WorkspacePlanBadge
diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx
index da9aed1684..9f9c3214d8 100644
--- a/web/app/components/main-nav/index.tsx
+++ b/web/app/components/main-nav/index.tsx
@@ -1,598 +1,31 @@
'use client'
-import type { ReactNode } from 'react'
-import type { InstalledApp } from '@/models/explore'
-import {
- AlertDialog,
- AlertDialogActions,
- AlertDialogCancelButton,
- AlertDialogConfirmButton,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogTitle,
-} from '@langgenius/dify-ui/alert-dialog'
-import { Avatar } from '@langgenius/dify-ui/avatar'
+import type { MainNavItem, MainNavProps } from './types'
+import type { Plan } from '@/app/components/billing/type'
import { cn } from '@langgenius/dify-ui/cn'
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLinkItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from '@langgenius/dify-ui/dropdown-menu'
-import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
-import { useMemo, useState } from 'react'
+import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
-import AppIcon from '@/app/components/base/app-icon'
-import Badge from '@/app/components/base/badge'
import DifyLogo from '@/app/components/base/logo/dify-logo'
-import { Plan } from '@/app/components/billing/type'
-import ItemOperation from '@/app/components/explore/item-operation'
-import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
-import AccountAbout from '@/app/components/header/account-about'
-import AccountDropdown from '@/app/components/header/account-dropdown'
-import Compliance from '@/app/components/header/account-dropdown/compliance'
-import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
-import Support from '@/app/components/header/account-dropdown/support'
-import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
-import GithubStar from '@/app/components/header/github-star'
-import Indicator from '@/app/components/header/indicator'
-import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
-import { useDocLink } from '@/context/i18n'
-import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useWorkspacesContext } from '@/context/workspace-context'
-import { env } from '@/env'
import Link from '@/next/link'
-import { usePathname, useRouter } from '@/next/navigation'
-import { switchWorkspace } from '@/service/common'
+import { usePathname } from '@/next/navigation'
import { systemFeaturesQueryOptions } from '@/service/system-features'
-import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
-import { basePath } from '@/utils/var'
-
-type MainNavProps = {
- className?: string
-}
-
-type MainNavItem = {
- href: string
- label: string
- active: (pathname: string) => boolean
- icon: string
- activeIcon: string
-}
-
-const navItemClassName = 'group relative flex h-9 items-center gap-2 rounded-xl p-2 transition-colors'
-
-const activeNavItemClassName = [
- 'overflow-hidden border border-transparent',
- 'bg-[linear-gradient(98.077deg,var(--color-components-main-nav-glass-surface-first)_0%,var(--color-components-main-nav-glass-surface-middle-1)_17.98%,var(--color-components-main-nav-glass-surface-middle-2)_58.75%,var(--color-components-main-nav-glass-surface-end)_101.09%)]',
- 'system-md-semibold text-components-main-nav-text-active backdrop-blur-[5px]',
- 'shadow-[0px_4px_8px_0px_var(--color-components-main-nav-glass-shadow-reflection-glow),0px_12px_16px_-4px_var(--color-shadow-shadow-5),0px_4px_6px_-2px_var(--color-shadow-shadow-1),0px_10px_16px_-4px_var(--color-components-main-nav-glass-shadow-reflection)]',
- 'main-nav-active-edge',
-].join(' ')
-
-const inactiveNavItemClassName = 'system-md-medium bg-components-main-nav-nav-button-bg text-components-main-nav-text hover:bg-state-base-hover hover:text-components-main-nav-text'
-
-const getWorkspaceInitial = (name?: string) => name?.[0]?.toLocaleUpperCase() || '?'
-
-const getRemainingCredits = (total: number, used: number) => Math.max(total - used, 0)
-
-const formatCredits = (value: number) => new Intl.NumberFormat().format(value)
-
-const WorkspaceIcon = ({
- name,
- className,
-}: {
- name?: string
- className?: string
-}) => (
-
- {getWorkspaceInitial(name)}
-
-)
-
-const NavIcon = ({
- icon,
- className,
-}: {
- icon: string
- className?: string
-}) => (
-
-)
-
-const WorkspacePlanBadge = ({
- plan,
-}: {
- plan: Plan
-}) => {
- return (
-
- {plan === Plan.professional ? 'pro' : plan}
-
- )
-}
-
-const WorkspaceMenuItemContent = ({
- icon,
- label,
- trailing,
-}: {
- icon: ReactNode
- label: ReactNode
- trailing?: ReactNode
-}) => (
- <>
- {icon}
- {label}
- {trailing}
- >
-)
-
-const WorkspaceCard = () => {
- const { t } = useTranslation()
- const { currentWorkspace } = useAppContext()
- const { workspaces } = useWorkspacesContext()
- const { enableBilling, plan } = useProviderContext()
- const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
- const [open, setOpen] = useState(false)
- const credits = getRemainingCredits(currentWorkspace.trial_credits, currentWorkspace.trial_credits_used)
- const formattedCredits = formatCredits(credits)
- const workspacePlan = (workspaces.find(workspace => workspace.current)?.plan || currentWorkspace.plan || plan.type) as Plan
- const isFreePlan = plan.type === Plan.sandbox
-
- const handlePlanClick = () => {
- if (isFreePlan)
- setShowPricingModal()
- else
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
- }
-
- const handleSwitchWorkspace = async (tenant_id: string) => {
- try {
- if (currentWorkspace.id === tenant_id)
- return
-
- await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } })
- toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
- location.assign(`${location.origin}${basePath}`)
- }
- catch {
- toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
- }
- }
-
- return (
-
-
-
setOpen(value => !value)}
- >
-
-
-
- {currentWorkspace.name}
-
-
-
-
-
-
- {
- e.stopPropagation()
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
- }}
- >
-
- {formattedCredits}
- {t('mainNav.workspace.creditsUnit', { ns: 'common' })}
-
- {enableBilling && (
- {
- e.stopPropagation()
- handlePlanClick()
- }}
- >
- {t('upgradeBtn.encourageShort', { ns: 'billing' })}
-
- )}
-
-
- {open && (
-
-
-
setOpen(false)}
- >
-
-
{currentWorkspace.name}
-
-
-
-
-
{
- setOpen(false)
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
- }}
- >
- } label={t('mainNav.workspace.settings', { ns: 'common' })} />
-
-
{
- setOpen(false)
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })
- }}
- >
- } label={t('mainNav.workspace.inviteMembers', { ns: 'common' })} />
-
-
- {workspaces.length > 0 && (
-
-
- {t('mainNav.workspace.switchWorkspace', { ns: 'common' })}
-
- {workspaces.map(workspace => (
-
{
- setOpen(false)
- void handleSwitchWorkspace(workspace.id)
- }}
- >
- }
- label={workspace.name}
- trailing={workspace.current ? : undefined}
- />
-
- ))}
-
- )}
-
- )}
-
- )
-}
-
-const MainNavLink = ({
- item,
- pathname,
-}: {
- item: MainNavItem
- pathname: string
-}) => {
- const activated = item.active(pathname)
- return (
-
-
- {item.label}
-
- )
-}
-
-const MainNavSearchButton = () => {
- const { t } = useTranslation()
-
- return (
- window.dispatchEvent(new Event(GOTO_ANYTHING_OPEN_EVENT))}
- >
-
- ⌘K
-
- )
-}
-
-const WebAppItem = ({
- app,
- isSelected,
- onDelete,
- onTogglePin,
-}: {
- app: InstalledApp
- isSelected: boolean
- onDelete: (id: string) => void
- onTogglePin: () => void
-}) => {
- const router = useRouter()
- const url = `/explore/installed/${app.id}`
- const [isHovering, setIsHovering] = useState(false)
-
- return (
- router.push(url)}
- onMouseEnter={() => setIsHovering(true)}
- onMouseLeave={() => setIsHovering(false)}
- title={app.app.name}
- >
-
-
e.stopPropagation()}>
- onDelete(app.id)}
- />
-
-
- )
-}
-
-const WebAppsSection = () => {
- const { t } = useTranslation()
- const pathname = usePathname()
- const { data, isPending } = useGetInstalledApps()
- const installedApps = useMemo(() => data?.installed_apps ?? [], [data?.installed_apps])
- const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp()
- const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus()
- const [searchVisible, setSearchVisible] = useState(false)
- const [searchText, setSearchText] = useState('')
- const [showConfirm, setShowConfirm] = useState(false)
- const [currentId, setCurrentId] = useState('')
-
- const filteredApps = useMemo(() => {
- const normalizedSearch = searchText.trim().toLowerCase()
- if (!normalizedSearch)
- return installedApps
-
- return installedApps.filter(item => item.app.name.toLowerCase().includes(normalizedSearch))
- }, [installedApps, searchText])
-
- const handleDelete = async () => {
- await uninstallApp(currentId)
- setShowConfirm(false)
- toast.success(t('api.remove', { ns: 'common' }))
- }
-
- const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
- await updatePinStatus({ appId: id, isPinned })
- toast.success(t('api.success', { ns: 'common' }))
- }
-
- return (
-
-
-
setSearchVisible(value => !value)}
- >
- {t('sidebar.webApps', { ns: 'explore' })}
-
-
-
- setSearchVisible(value => !value)}
- >
-
-
-
-
-
-
- {searchVisible && (
-
- setSearchText(e.target.value)}
- placeholder={t('mainNav.webApps.searchPlaceholder', { ns: 'common' })}
- className="h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 system-sm-regular text-text-secondary outline-none placeholder:text-text-quaternary hover:border-components-input-border-hover focus:border-components-input-border-active"
- />
-
- )}
-
- {isPending && (
-
{t('loading', { ns: 'common' })}
- )}
- {!isPending && filteredApps.length === 0 && (
-
- {searchText ? t('mainNav.webApps.noResults', { ns: 'common' }) : t('sidebar.noApps.title', { ns: 'explore' })}
-
- )}
- {filteredApps.map(app => (
-
{
- setCurrentId(id)
- setShowConfirm(true)
- }}
- onTogglePin={() => {
- void handleUpdatePinStatus(app.id, !app.is_pinned)
- }}
- />
- ))}
-
-
-
-
-
- {t('sidebar.delete.title', { ns: 'explore' })}
-
-
- {t('sidebar.delete.content', { ns: 'explore' })}
-
-
-
-
- {t('operation.cancel', { ns: 'common' })}
-
-
- {t('operation.confirm', { ns: 'common' })}
-
-
-
-
-
- )
-}
-
-const HelpMenu = () => {
- const { t } = useTranslation()
- const docLink = useDocLink()
- const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
- const { langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
- const [aboutVisible, setAboutVisible] = useState(false)
- const [open, setOpen] = useState(false)
-
- return (
- <>
-
-
-
-
-
- {!systemFeatures.branding.enabled && (
- <>
-
-
- }
- />
-
- setOpen(false)} />
- {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && }
-
-
-
-
- }
- />
-
-
-
-
-
-
- )}
- />
-
- {env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
- {
- setAboutVisible(true)
- setOpen(false)
- }}
- >
-
- {t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}
-
-
- )}
- />
-
- )}
-
- >
- )}
-
-
- {aboutVisible && setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />}
- >
- )
-}
+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 MainNav = ({
className,
}: MainNavProps) => {
const { t } = useTranslation()
const pathname = usePathname()
- const { currentWorkspace, userProfile } = useAppContext()
+ const { currentWorkspace } = useAppContext()
const { plan } = useProviderContext()
const { workspaces } = useWorkspacesContext()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
@@ -670,20 +103,7 @@ const MainNav = ({
-
}
- variant="mainNav"
- trigger={({ isOpen, ariaLabel }) => (
-
-
- {userProfile.name}
-
- )}
- />
+
diff --git a/web/app/components/main-nav/types.ts b/web/app/components/main-nav/types.ts
new file mode 100644
index 0000000000..4ac9937d32
--- /dev/null
+++ b/web/app/components/main-nav/types.ts
@@ -0,0 +1,11 @@
+export type MainNavProps = {
+ className?: string
+}
+
+export type MainNavItem = {
+ href: string
+ label: string
+ active: (pathname: string) => boolean
+ icon: string
+ activeIcon: string
+}
diff --git a/web/app/components/main-nav/utils.ts b/web/app/components/main-nav/utils.ts
new file mode 100644
index 0000000000..619393fd30
--- /dev/null
+++ b/web/app/components/main-nav/utils.ts
@@ -0,0 +1,5 @@
+export const getWorkspaceInitial = (name?: string) => name?.[0]?.toLocaleUpperCase() || '?'
+
+export const getRemainingCredits = (total: number, used: number) => Math.max(total - used, 0)
+
+export const formatCredits = (value: number) => new Intl.NumberFormat().format(value)