From 92a0959448751d858c42f749b46be6a69820998b Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 24 May 2026 22:09:05 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20refactor(web/default):=20adopt=20dr?= =?UTF-8?q?ill-in=20sidebar=20pattern=20for=20System=20Settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ad-hoc "workspace" abstraction with a focused, URL-driven "sidebar view" registry that implements the modern Vercel / Cloudflare drill-in pattern: clicking a top-level entry (e.g. System Settings) swaps the sidebar to a contextual workspace, with a `← Back to Dashboard` affordance, instead of stacking sub-navigation in the root. Architecture ------------ - types.ts + SidebarView — declarative nested view config (id, pathPattern, parent, getNavGroups) + SidebarViewParent — back-navigation descriptor + ResolvedSidebarView — { key, view, navGroups } returned by hook + SidebarData — slimmed to { navGroups } only - Workspace — removed (logo/plan never rendered) - lib/sidebar-view-registry.ts (new, replaces workspace-registry.ts) + SIDEBAR_VIEWS array — single source of truth for nested views + resolveSidebarView(pathname) + getNavGroupsForPath(pathname, t) — back-compat helper for the command palette - config/system-settings.config.ts Refactored to export a single SYSTEM_SETTINGS_VIEW (SidebarView) with parent `/dashboard/overview` + label `Back to Dashboard`. - components/sidebar-view-header.tsx (new) Renders only the back affordance (chevron + label). Uses the default SidebarMenuButton size so its typography matches the nav items below; collapses gracefully into icon mode via the existing tooltip behavior. The redundant "title + icon" row was removed — workspace context is already carried by the nav groups. - hooks/use-sidebar-view.ts (new) Encapsulates view resolution and root-nav filtering: · matched view → returns its nav groups verbatim (route-level beforeLoad guards already enforce access); · no match → returns root nav groups, narrowed by user role (admin gate) and useSidebarConfig (admin × user sidebar_modules overlay). - components/app-sidebar.tsx Now a thin presentation layer: reads { key, view, navGroups } from useSidebarView() and orchestrates the view transition via AnimatePresence + MOTION_VARIANTS.sidebarSlide (respects prefers-reduced-motion). No logic, no role checks, no path matching — those live in the hook. - components/command-menu.tsx Switched to the new getNavGroupsForPath() API; behavior preserved. Cleanup ------- - Deleted layout/context/workspace-context.tsx (zero consumers). - Deleted layout/lib/workspace-registry.ts and its workspace-registry.example.ts companion (over-abstracted: name/id metadata, isInWorkspace / getAllWorkspaces / WORKSPACE_IDS were registered but never read). - Removed `workspaces` field from useSidebarData (never consumed after the top-switcher was dropped). - Dropped WorkspaceProvider from authenticated-layout.tsx. - Trimmed dead `Manage and configure` translation key from all six locale files and from static-keys.ts. i18n ---- Added the `Back to Dashboard` key to en, zh, fr, ja, ru, vi, and registered it in static-keys.ts under "Sidebar views". Verification ------------ - bun run typecheck: passes - Lint: no new warnings/errors on the touched files - Adding a new drill-in workspace now only requires registering a SidebarView in SIDEBAR_VIEWS — no changes to AppSidebar required. --- web/default/src/components/command-menu.tsx | 7 +- .../components/data-table/faceted-filter.tsx | 5 +- .../layout/components/app-sidebar.tsx | 78 +++++------ .../components/authenticated-layout.tsx | 35 +++-- .../components/layout/components/footer.tsx | 4 +- .../layout/components/sidebar-view-header.tsx | 70 ++++++++++ .../layout/config/system-settings.config.ts | 30 +++- .../layout/context/workspace-context.tsx | 62 --------- web/default/src/components/layout/index.ts | 37 ++--- .../layout/lib/sidebar-view-registry.ts | 58 ++++++++ .../layout/lib/workspace-registry.example.ts | 119 ---------------- .../layout/lib/workspace-registry.ts | 128 ------------------ web/default/src/components/layout/types.ts | 60 ++++++-- .../src/features/auth/sign-in/index.tsx | 25 ++-- .../auth/sign-up/components/sign-up-form.tsx | 1 - .../channels/components/channels-columns.tsx | 2 +- .../dialogs/fetch-models-dialog.tsx | 18 +-- .../drawers/channel-mutate-drawer.tsx | 4 +- .../overview/performance-health-panel.tsx | 82 ++++++----- .../components/overview/summary-cards.tsx | 23 +--- .../dashboard/components/ui/stat-card.tsx | 3 +- web/default/src/features/dashboard/index.tsx | 2 +- .../home/components/sections/hero.tsx | 4 +- .../components/api-keys-mutate-drawer.tsx | 9 +- .../src/features/keys/lib/api-key-form.ts | 4 +- web/default/src/features/models/index.tsx | 2 +- .../components/profile-settings-card.tsx | 2 +- .../subscriptions-mutate-drawer.tsx | 8 +- .../waffo-pancake-settings-section.tsx | 16 +-- .../columns/common-logs-columns.tsx | 10 +- .../components/common-logs-filter-bar.tsx | 4 +- .../components/dialogs/details-dialog.tsx | 56 ++++---- web/default/src/features/usage-logs/index.tsx | 2 +- web/default/src/features/wallet/lib/ui.tsx | 2 +- web/default/src/hooks/use-sidebar-data.ts | 38 +++--- web/default/src/hooks/use-sidebar-view.ts | 74 ++++++++++ web/default/src/hooks/use-top-nav-links.ts | 2 +- web/default/src/i18n/locales/en.json | 2 +- web/default/src/i18n/locales/fr.json | 2 +- web/default/src/i18n/locales/ja.json | 2 +- web/default/src/i18n/locales/ru.json | 2 +- web/default/src/i18n/locales/vi.json | 2 +- web/default/src/i18n/locales/zh.json | 2 +- web/default/src/i18n/static-keys.ts | 4 +- web/default/src/routes/__root.tsx | 2 +- .../_authenticated/playground/index.tsx | 2 +- 46 files changed, 515 insertions(+), 591 deletions(-) create mode 100644 web/default/src/components/layout/components/sidebar-view-header.tsx delete mode 100644 web/default/src/components/layout/context/workspace-context.tsx create mode 100644 web/default/src/components/layout/lib/sidebar-view-registry.ts delete mode 100644 web/default/src/components/layout/lib/workspace-registry.example.ts delete mode 100644 web/default/src/components/layout/lib/workspace-registry.ts create mode 100644 web/default/src/hooks/use-sidebar-view.ts diff --git a/web/default/src/components/command-menu.tsx b/web/default/src/components/command-menu.tsx index 66af025f..692cf863 100644 --- a/web/default/src/components/command-menu.tsx +++ b/web/default/src/components/command-menu.tsx @@ -33,7 +33,7 @@ import { CommandList, CommandSeparator, } from '@/components/ui/command' -import { getNavGroupsForPath } from './layout/lib/workspace-registry' +import { getNavGroupsForPath } from './layout/lib/sidebar-view-registry' import { ScrollArea } from './ui/scroll-area' export function CommandMenu() { @@ -44,8 +44,9 @@ export function CommandMenu() { const { pathname } = useLocation() const sidebarData = useSidebarData() - // 根据当前路径从工作区注册表获取对应的侧边栏配置 - const navGroups = getNavGroupsForPath(pathname, t) || sidebarData.navGroups + // Use the active nested sidebar view's nav groups when one matches + // the current URL; otherwise fall back to the root navigation. + const navGroups = getNavGroupsForPath(pathname, t) ?? sidebarData.navGroups const runCommand = React.useCallback( (command: () => unknown) => { diff --git a/web/default/src/components/data-table/faceted-filter.tsx b/web/default/src/components/data-table/faceted-filter.tsx index bd81a153..9198e7e0 100644 --- a/web/default/src/components/data-table/faceted-filter.tsx +++ b/web/default/src/components/data-table/faceted-filter.tsx @@ -107,10 +107,7 @@ export function DataTableFacetedFilter({ )} - + diff --git a/web/default/src/components/layout/components/app-sidebar.tsx b/web/default/src/components/layout/components/app-sidebar.tsx index ba387a9a..f96926b3 100644 --- a/web/default/src/components/layout/components/app-sidebar.tsx +++ b/web/default/src/components/layout/components/app-sidebar.tsx @@ -16,59 +16,59 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useMemo } from 'react' -import { useLocation } from '@tanstack/react-router' -import { useTranslation } from 'react-i18next' -import { useAuthStore } from '@/stores/auth-store' -import { ROLE } from '@/lib/roles' +import { AnimatePresence, motion, useReducedMotion } from 'motion/react' +import { MOTION_TRANSITION, MOTION_VARIANTS } from '@/lib/motion' import { useLayout } from '@/context/layout-provider' -import { useSidebarConfig } from '@/hooks/use-sidebar-config' -import { useSidebarData } from '@/hooks/use-sidebar-data' +import { useSidebarView } from '@/hooks/use-sidebar-view' import { Sidebar, SidebarContent, SidebarRail } from '@/components/ui/sidebar' -import { getNavGroupsForPath } from '../lib/workspace-registry' import { NavGroup } from './nav-group' +import { SidebarViewHeader } from './sidebar-view-header' /** - * Application sidebar component - * Fetches corresponding navigation menu from workspace registry based on current path - * Dynamically filters navigation items based on backend SidebarModulesAdmin configuration + * Application sidebar. * - * Automatically matches workspace configuration for current path through workspace registry system - * Adding new workspaces only requires registration in workspace-registry.ts + * Adopts the Vercel / Cloudflare "drill-in" pattern: the URL drives + * which sidebar *view* is rendered. Clicking a top-level entry like + * `System Settings` swaps the sidebar to a contextual workspace — + * with a `← Back to Dashboard` affordance — instead of stacking the + * sub-navigation inside the root tree. + * + * Architecture: + * - View resolution + filtering: {@link useSidebarView} + * - View registry: `layout/lib/sidebar-view-registry.ts` + * - Per-view header: {@link SidebarViewHeader} + * + * Adding a new nested view only requires registering a {@link SidebarView} + * in the registry; this component requires no changes. */ export function AppSidebar() { - const { t } = useTranslation() const { collapsible, variant } = useLayout() - const { pathname } = useLocation() - const userRole = useAuthStore((state) => state.auth.user?.role) - const sidebarData = useSidebarData() - - // Get navigation group configuration corresponding to current path from workspace registry - const allNavGroups = getNavGroupsForPath(pathname, t) || sidebarData.navGroups - - // Filter sidebar navigation items based on backend configuration - const configFilteredNavGroups = useSidebarConfig(allNavGroups) - - // Filter navigation groups based on user role - // Non-Admin users cannot see Admin navigation group - const currentNavGroups = useMemo(() => { - const isAdmin = userRole && userRole >= ROLE.ADMIN - return configFilteredNavGroups.filter((group) => { - if (group.id === 'admin') { - return isAdmin - } - return true - }) - }, [configFilteredNavGroups, userRole]) + const { key, view, navGroups } = useSidebarView() + const shouldReduce = useReducedMotion() return ( + {view && } + - {currentNavGroups.map((props) => { - const key = props.id || props.title - return - })} + + + {navGroups.map((props) => ( + + ))} + + + ) diff --git a/web/default/src/components/layout/components/authenticated-layout.tsx b/web/default/src/components/layout/components/authenticated-layout.tsx index 49a1ff89..a39b4e19 100644 --- a/web/default/src/components/layout/components/authenticated-layout.tsx +++ b/web/default/src/components/layout/components/authenticated-layout.tsx @@ -23,7 +23,6 @@ import { SearchProvider } from '@/context/search-provider' import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar' import { AnimatedOutlet } from '@/components/page-transition' import { SkipToMain } from '@/components/skip-to-main' -import { WorkspaceProvider } from '../context/workspace-context' import { AppHeader } from './app-header' import { AppSidebar } from './app-sidebar' @@ -37,24 +36,22 @@ export function AuthenticatedLayout(props: AuthenticatedLayoutProps) { return ( - - - - -
- - - {props.children ?? } - -
-
-
+ + + +
+ + + {props.children ?? } + +
+
) diff --git a/web/default/src/components/layout/components/footer.tsx b/web/default/src/components/layout/components/footer.tsx index 4e18e672..be4e612f 100644 --- a/web/default/src/components/layout/components/footer.tsx +++ b/web/default/src/components/layout/components/footer.tsx @@ -20,8 +20,8 @@ import { Fragment, useMemo } from 'react' import { Link } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' -import { useSystemConfig } from '@/hooks/use-system-config' import { useStatus } from '@/hooks/use-status' +import { useSystemConfig } from '@/hooks/use-system-config' interface FooterLink { text: string @@ -235,7 +235,7 @@ export function Footer(props: FooterProps) { className='custom-footer text-muted-foreground min-w-0 text-center text-sm sm:text-left' dangerouslySetInnerHTML={{ __html: footerHtml }} /> -
+
diff --git a/web/default/src/components/layout/components/sidebar-view-header.tsx b/web/default/src/components/layout/components/sidebar-view-header.tsx new file mode 100644 index 00000000..97d001bc --- /dev/null +++ b/web/default/src/components/layout/components/sidebar-view-header.tsx @@ -0,0 +1,70 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { Link } from '@tanstack/react-router' +import { ChevronLeft } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' +import { + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/ui/sidebar' +import type { SidebarView } from '../types' + +type SidebarViewHeaderProps = { + view: SidebarView +} + +/** + * Header for a nested sidebar view (Vercel / Cloudflare drill-in pattern). + * + * Renders only the back affordance — workspace context is conveyed by + * the nav groups below, not a redundant title row. + */ +export function SidebarViewHeader(props: SidebarViewHeaderProps) { + const { t } = useTranslation() + const { setOpenMobile } = useSidebar() + + return ( + + + + setOpenMobile(false)} + /> + } + > + + {t(props.view.parent.label)} + + + + + ) +} diff --git a/web/default/src/components/layout/config/system-settings.config.ts b/web/default/src/components/layout/config/system-settings.config.ts index 281223a8..c0b7b260 100644 --- a/web/default/src/components/layout/config/system-settings.config.ts +++ b/web/default/src/components/layout/config/system-settings.config.ts @@ -33,15 +33,16 @@ import { getModelsSectionNavItems } from '@/features/system-settings/models/sect import { getOperationsSectionNavItems } from '@/features/system-settings/operations/section-registry.tsx' import { getSecuritySectionNavItems } from '@/features/system-settings/security/section-registry.tsx' import { getSiteSectionNavItems } from '@/features/system-settings/site/section-registry.tsx' -import { type NavGroup } from '../types' +import type { NavGroup, SidebarView } from '../types' /** - * System settings sidebar configuration - * Displayed when switching to "System Settings" workspace + * Sidebar nav groups for the System Settings nested view. + * + * Kept as a single group because the workspace title in the sidebar + * header already provides top-level context — the inner group label + * scopes the items as "administration" actions. */ -export const WORKSPACE_SYSTEM_SETTINGS_ID = 'system-settings' - -export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { +function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { return [ { id: 'system-administration', @@ -86,3 +87,20 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { }, ] } + +/** + * Nested sidebar view for `/system-settings/*`. + * + * Activates the Vercel / Cloudflare-style drill-in sidebar: + * the root navigation is replaced by the system administration + * groups, with a "Back to Dashboard" affordance in the header. + */ +export const SYSTEM_SETTINGS_VIEW: SidebarView = { + id: 'system-settings', + pathPattern: /^\/system-settings(\/|$)/, + parent: { + to: '/dashboard/overview', + label: 'Back to Dashboard', + }, + getNavGroups: getSystemSettingsNavGroups, +} diff --git a/web/default/src/components/layout/context/workspace-context.tsx b/web/default/src/components/layout/context/workspace-context.tsx deleted file mode 100644 index c5b2a96b..00000000 --- a/web/default/src/components/layout/context/workspace-context.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright (C) 2023-2026 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ -/* eslint-disable react-refresh/only-export-components */ -import * as React from 'react' -import { type Workspace } from '../types' - -type WorkspaceContextType = { - activeWorkspace: Workspace | null - setActiveWorkspace: (workspace: Workspace) => void -} - -const WorkspaceContext = React.createContext( - undefined -) - -/** - * 工作区上下文 Provider - * 管理当前激活的工作区状态,用于切换不同的侧边栏视图 - */ -export function WorkspaceProvider({ children }: { children: React.ReactNode }) { - const [activeWorkspace, setActiveWorkspace] = - React.useState(null) - - const value = React.useMemo( - () => ({ activeWorkspace, setActiveWorkspace }), - [activeWorkspace] - ) - - return ( - - {children} - - ) -} - -/** - * 使用工作区上下文的 Hook - * @throws 如果在 WorkspaceProvider 外部使用会抛出错误 - */ -export function useWorkspace() { - const context = React.useContext(WorkspaceContext) - if (!context) { - throw new Error('useWorkspace must be used within WorkspaceProvider') - } - return context -} diff --git a/web/default/src/components/layout/index.ts b/web/default/src/components/layout/index.ts index 7a3ac23e..fe5ae178 100644 --- a/web/default/src/components/layout/index.ts +++ b/web/default/src/components/layout/index.ts @@ -17,10 +17,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ /** - * Layout 组件统一导出 + * Public surface of the Layout module. */ -// 核心组件 +// Core components export { AppHeader } from './components/app-header' export { AppSidebar } from './components/app-sidebar' export { AuthenticatedLayout } from './components/authenticated-layout' @@ -34,41 +34,34 @@ export { Main } from './components/main' export { PageFooterPortal } from './components/page-footer' export { NavGroup } from './components/nav-group' export { SectionPageLayout } from './components/section-page-layout' +export { SidebarViewHeader } from './components/sidebar-view-header' export { SystemBrand } from './components/system-brand' export { TopNav } from './components/top-nav' export { MobileDrawer } from './components/mobile-drawer' -// 上下文 -export { WorkspaceProvider, useWorkspace } from './context/workspace-context' - -// 配置 -export { - getSystemSettingsNavGroups, - WORKSPACE_SYSTEM_SETTINGS_ID, -} from './config/system-settings.config' +// Configuration +export { SYSTEM_SETTINGS_VIEW } from './config/system-settings.config' export { defaultTopNavLinks } from './config/top-nav.config' -// 常量 +// Constants export { MOBILE_DRAWER_ANIMATION, MOBILE_DRAWER_CONFIG } from './constants' -// 工具函数 - 工作区注册表 +// Sidebar view registry export { - getWorkspaceByPath, getNavGroupsForPath, - isInWorkspace, - getAllWorkspaces, - WORKSPACE_IDS, -} from './lib/workspace-registry' + resolveSidebarView, +} from './lib/sidebar-view-registry' -// 类型导出(使用 type-only 导出避免与组件冲突) +// Type exports (type-only to avoid conflicts with components above) export type { - Workspace, - NavLink, NavCollapsible, - NavItem, NavGroup as NavGroupType, + NavItem, + NavLink, + ResolvedSidebarView, SidebarData, + SidebarView, + SidebarViewParent, TopNavLink, } from './types' -export type { WorkspaceConfig, WorkspaceId } from './lib/workspace-registry' export type { SectionPageLayoutProps } from './components/section-page-layout' diff --git a/web/default/src/components/layout/lib/sidebar-view-registry.ts b/web/default/src/components/layout/lib/sidebar-view-registry.ts new file mode 100644 index 00000000..c22f8808 --- /dev/null +++ b/web/default/src/components/layout/lib/sidebar-view-registry.ts @@ -0,0 +1,58 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { type TFunction } from 'i18next' +import { SYSTEM_SETTINGS_VIEW } from '../config/system-settings.config' +import type { NavGroup, SidebarView } from '../types' + +/** + * Registered nested sidebar views. + * + * Each entry describes a contextual sidebar that replaces the root + * navigation when the user enters that workspace (Vercel-style + * "drill-in" pattern). Add new entries here to register a new view. + * + * Match priority is array order; the first matching `pathPattern` wins. + */ +const SIDEBAR_VIEWS: readonly SidebarView[] = [SYSTEM_SETTINGS_VIEW] + +/** + * Resolve the active nested view for the given path. + * + * @returns Matching {@link SidebarView}, or `null` when the root + * navigation should be displayed. + */ +export function resolveSidebarView(pathname: string): SidebarView | null { + return SIDEBAR_VIEWS.find((view) => view.pathPattern.test(pathname)) ?? null +} + +/** + * Backwards-compatible helper for consumers (e.g. command palette) that + * just need the navigation groups for the current path, without caring + * about the view metadata. + * + * @returns Nav groups for the matched view, or `null` if no nested view + * matches (callers should then fall back to root nav groups). + */ +export function getNavGroupsForPath( + pathname: string, + t: TFunction +): NavGroup[] | null { + const view = resolveSidebarView(pathname) + return view ? view.getNavGroups(t) : null +} diff --git a/web/default/src/components/layout/lib/workspace-registry.example.ts b/web/default/src/components/layout/lib/workspace-registry.example.ts deleted file mode 100644 index 8e330725..00000000 --- a/web/default/src/components/layout/lib/workspace-registry.example.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright (C) 2023-2026 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ -/** - * 工作区注册表使用示例 - * - * 本文件展示如何添加新工作区,仅作示例参考,不会被编译 - */ - -/** - * 步骤1: 创建工作区的侧边栏配置文件 - * 例如:web/src/components/layout/config/user-management.config.ts - */ -/* -import { Users, UserPlus, Shield } from 'lucide-react' -import { type NavGroup } from '../types' - -export const userManagementConfig: NavGroup[] = [ - { - title: 'User Management', - items: [ - { - title: 'All Users', - url: '/user-management/list', - icon: Users, - }, - { - title: 'Create User', - url: '/user-management/create', - icon: UserPlus, - }, - { - title: 'Permissions', - url: '/user-management/permissions', - icon: Shield, - }, - ], - }, -] -*/ - -/** - * 步骤2: 在 workspace-registry.ts 中注册新工作区 - * 在 workspaceRegistry 数组中添加配置(在默认工作区之前) - */ -/* -import { userManagementConfig } from '../config/user-management.config' - -const workspaceRegistry: WorkspaceConfig[] = [ - // System Settings 工作区 - { - name: 'System Settings', - pathPattern: /^\/system-settings/, - navGroups: systemSettingsConfig, - }, - // 新增的 User Management 工作区 - { - name: 'User Management', - pathPattern: /^\/user-management/, // 或使用字符串: '/user-management' - navGroups: userManagementConfig, - }, - // 默认工作区(必须放在最后) - { - name: 'Default', - pathPattern: /.* /, - navGroups: sidebarConfig.navGroups, - }, -] -*/ - -/** - * 步骤3: (可选)在 sidebar.config.ts 中添加工作区到切换器 - */ -/* -export const sidebarConfig: SidebarData = { - workspaces: [ - { - name: '', - logo: Command, - plan: '', - }, - { - name: 'User Management', - logo: Users, - plan: 'Manage users', - }, - { - name: 'System Settings', - logo: Settings, - plan: 'Manage and configure', - }, - ], - navGroups: [...], -} -*/ - -/** - * 同语注:这里就完成了,现在: - * - 侧边栏会根据当前路径自动切换显示对应的工作区菜单 - * - 搜索功能会自动显示当前工作区的菜单项 - * - 工作区切换器会显示新的工作区选项 - * - * 无需修改任何其他文件! - */ diff --git a/web/default/src/components/layout/lib/workspace-registry.ts b/web/default/src/components/layout/lib/workspace-registry.ts deleted file mode 100644 index f50668f1..00000000 --- a/web/default/src/components/layout/lib/workspace-registry.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* -Copyright (C) 2023-2026 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ -import { type TFunction } from 'i18next' -import { - getSystemSettingsNavGroups, - WORKSPACE_SYSTEM_SETTINGS_ID, -} from '../config/system-settings.config' -import type { NavGroup } from '../types' - -export const WORKSPACE_IDS = { - SYSTEM_SETTINGS: WORKSPACE_SYSTEM_SETTINGS_ID, - DEFAULT: 'default', -} as const - -export type WorkspaceId = (typeof WORKSPACE_IDS)[keyof typeof WORKSPACE_IDS] - -/** - * Workspace configuration type - * Each workspace contains name, path matching rules, and corresponding navigation group configuration - */ -export type WorkspaceConfig = { - /** Workspace identifier (for logic) */ - id: WorkspaceId - /** Workspace name */ - name: string - /** Path matching rule, supports string (contains match) or regular expression */ - pathPattern: string | RegExp - /** Sidebar navigation group configuration for this workspace */ - getNavGroups?: (t: TFunction) => NavGroup[] -} - -/** - * Workspace registry - * - * Sorted by priority, first matched workspace will be used - * Last one should be default workspace (matches all paths) - * - * @example - * // Add new workspace - * { - * name: 'User Management', - * pathPattern: /^\/user-management/, - * navGroups: userManagementConfig - * } - */ -const workspaceRegistry: WorkspaceConfig[] = [ - // System Settings workspace - { - id: WORKSPACE_IDS.SYSTEM_SETTINGS, - name: 'System Settings', - pathPattern: /^\/system-settings/, - getNavGroups: getSystemSettingsNavGroups, - }, - // Default workspace (must be last) - { - id: WORKSPACE_IDS.DEFAULT, - name: 'Default', - pathPattern: /.*/, - // getNavGroups is undefined, will be handled by consumers (e.g. useSidebarData) - }, -] - -/** - * Get matched workspace configuration based on path - * @param pathname - Current route path - * @returns Matched workspace configuration - */ -export function getWorkspaceByPath(pathname: string): WorkspaceConfig { - const workspace = workspaceRegistry.find((ws) => { - if (typeof ws.pathPattern === 'string') { - return pathname.includes(ws.pathPattern) - } - return ws.pathPattern.test(pathname) - }) - - // If no match, return default workspace (last one) - return workspace || workspaceRegistry[workspaceRegistry.length - 1] -} - -/** - * Get corresponding sidebar navigation group configuration based on path - * @param pathname - Current route path - * @returns Navigation group configuration for corresponding workspace - */ -export function getNavGroupsForPath( - pathname: string, - t: TFunction -): NavGroup[] | undefined { - const workspace = getWorkspaceByPath(pathname) - return workspace.getNavGroups?.(t) -} - -/** - * Determine if in specified workspace - * @param pathname - Current route path - * @param workspaceId - Workspace identifier - * @returns Whether in specified workspace - */ -export function isInWorkspace( - pathname: string, - workspaceId: WorkspaceId -): boolean { - return getWorkspaceByPath(pathname).id === workspaceId -} - -/** - * Get all registered workspace configurations - * @returns Array of workspace configurations - */ -export function getAllWorkspaces(): WorkspaceConfig[] { - return workspaceRegistry -} diff --git a/web/default/src/components/layout/types.ts b/web/default/src/components/layout/types.ts index 8e0e843f..087ff2e5 100644 --- a/web/default/src/components/layout/types.ts +++ b/web/default/src/components/layout/types.ts @@ -17,17 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { type LinkProps } from '@tanstack/react-router' - -/** - * Workspace type - * Used for top switcher to display different workspaces - */ -export type Workspace = { - id: string - name: string - logo: React.ElementType - plan: string -} +import { type TFunction } from 'i18next' /** * Base navigation item type @@ -82,10 +72,12 @@ export type NavGroup = { } /** - * Sidebar data type + * Root sidebar data type + * + * Used by the default (top-level) sidebar view that lists primary + * application navigation (chat, dashboard, admin, etc). */ export type SidebarData = { - workspaces: Workspace[] navGroups: NavGroup[] } @@ -100,3 +92,45 @@ export type TopNavLink = { requiresAuth?: boolean external?: boolean } + +/** + * Back-navigation descriptor for a nested sidebar view + */ +export type SidebarViewParent = { + /** Destination URL for the back button */ + to: LinkProps['to'] | (string & {}) + /** Visible label, e.g. "Back to Dashboard" — already localized */ + label: string +} + +/** + * Nested sidebar view configuration + * + * A nested view replaces the root navigation when the user enters a + * dedicated workspace (e.g. System Settings). It models the modern + * Vercel / Cloudflare "drill-in" sidebar UX: clicking a top-level entry + * swaps the sidebar to a contextual view with a "Back" affordance. + */ +export type SidebarView = { + /** Stable identifier (also drives transition animation keys) */ + id: string + /** Path matcher that activates this view */ + pathPattern: RegExp + /** Back-navigation descriptor; required for nested views */ + parent: SidebarViewParent + /** Nav group builder, called per render with the active translator */ + getNavGroups: (t: TFunction) => NavGroup[] +} + +/** + * Resolved sidebar view returned by `useSidebarView()` + * + * - `view === null`: root navigation (default sidebar) + * - `view !== null`: nested workspace view (renders header + back button) + */ +export type ResolvedSidebarView = { + /** Animation/identity key — falls back to a sentinel for the root view */ + key: string + view: SidebarView | null + navGroups: NavGroup[] +} diff --git a/web/default/src/features/auth/sign-in/index.tsx b/web/default/src/features/auth/sign-in/index.tsx index d9675c23..1cf73730 100644 --- a/web/default/src/features/auth/sign-in/index.tsx +++ b/web/default/src/features/auth/sign-in/index.tsx @@ -35,18 +35,19 @@ export function SignIn() {

{t('Sign in')}

- {!status?.self_use_mode_enabled && status?.register_enabled !== false && ( -

- {t("Don't have an account?")}{' '} - - {t('Sign up')} - - . -

- )} + {!status?.self_use_mode_enabled && + status?.register_enabled !== false && ( +

+ {t("Don't have an account?")}{' '} + + {t('Sign up')} + + . +

+ )}
diff --git a/web/default/src/features/auth/sign-up/components/sign-up-form.tsx b/web/default/src/features/auth/sign-up/components/sign-up-form.tsx index 6db51347..1e7cb5a8 100644 --- a/web/default/src/features/auth/sign-up/components/sign-up-form.tsx +++ b/web/default/src/features/auth/sign-up/components/sign-up-form.tsx @@ -319,7 +319,6 @@ export function SignUpForm({ )} - )} diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 8747c862..6ad8b646 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -36,7 +36,6 @@ import { } from '@/lib/format' import { getLobeIcon } from '@/lib/lobe-icon' import { cn, truncateText } from '@/lib/utils' -import { TruncatedText } from '@/components/truncated-text' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { @@ -53,6 +52,7 @@ import { dotColorMap, textColorMap, } from '@/components/status-badge' +import { TruncatedText } from '@/components/truncated-text' import { getCodexUsage } from '../api' import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants' import { diff --git a/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx b/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx index 180fdb9a..108d49a9 100644 --- a/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx @@ -89,9 +89,7 @@ export function FetchModelsDialog({ // Parse existing models const existingModels = useMemo( - () => - existingModelsOverride ?? - parseModelsString(currentRow?.models || ''), + () => existingModelsOverride ?? parseModelsString(currentRow?.models || ''), [existingModelsOverride, currentRow?.models] ) @@ -369,12 +367,14 @@ export function FetchModelsDialog({ {t('Fetch Models')} - {currentRow - ? <> - {t('Fetch available models for:')}{' '} - {currentRow.name} - - : t('Fetch available models from upstream')} + {currentRow ? ( + <> + {t('Fetch available models for:')}{' '} + {currentRow.name} + + ) : ( + t('Fetch available models from upstream') + )} diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 39a6e152..a3195a53 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -3381,7 +3381,9 @@ export function ChannelMutateDrawer({ redirectSourceModels={redirectModelKeyList} customFetcher={!isEditing ? createModeFetcher : undefined} existingModelsOverride={ - !isEditing ? parseModelsString(form.getValues('models') || '') : undefined + !isEditing + ? parseModelsString(form.getValues('models') || '') + : undefined } /> diff --git a/web/default/src/features/dashboard/components/overview/performance-health-panel.tsx b/web/default/src/features/dashboard/components/overview/performance-health-panel.tsx index bcbe3e1b..cbde734d 100644 --- a/web/default/src/features/dashboard/components/overview/performance-health-panel.tsx +++ b/web/default/src/features/dashboard/components/overview/performance-health-panel.tsx @@ -82,9 +82,17 @@ export function PerformanceHealthPanel() { const summary = useMemo(() => { return { avgLatencyMs: Math.round( - simpleAverage(models, 'avg_latency_ms', (v) => Number.isFinite(v) && v > 0) + simpleAverage( + models, + 'avg_latency_ms', + (v) => Number.isFinite(v) && v > 0 + ) + ), + avgTps: simpleAverage( + models, + 'avg_tps', + (v) => Number.isFinite(v) && v > 0 ), - avgTps: simpleAverage(models, 'avg_tps', (v) => Number.isFinite(v) && v > 0), successRate: simpleAverage(models, 'success_rate', Number.isFinite), } }, [models]) @@ -96,7 +104,10 @@ export function PerformanceHealthPanel() { return (
-
- ) : hasData && ( -
- - {t('Top models by traffic')} - -
- {topModels.map((model) => ( -
- - {model.model_name} - - -
-
+ ) )}
diff --git a/web/default/src/features/dashboard/components/overview/summary-cards.tsx b/web/default/src/features/dashboard/components/overview/summary-cards.tsx index 21839692..37ef4003 100644 --- a/web/default/src/features/dashboard/components/overview/summary-cards.tsx +++ b/web/default/src/features/dashboard/components/overview/summary-cards.tsx @@ -19,12 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' -import { - ArrowRight, - Flame, - ShieldCheck, - TrendingDown, -} from 'lucide-react' +import { ArrowRight, Flame, ShieldCheck, TrendingDown } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/stores/auth-store' import { getCurrencyLabel, isCurrencyDisplayEnabled } from '@/lib/currency' @@ -102,7 +97,10 @@ function getSummarySparkline( return undefined } -function getRunwayDays(remainQuota: number, recentUsage: number): number | null { +function getRunwayDays( + remainQuota: number, + recentUsage: number +): number | null { if (remainQuota <= 0 || recentUsage <= 0) return null const days = remainQuota / recentUsage if (!Number.isFinite(days)) return null @@ -111,10 +109,7 @@ function getRunwayDays(remainQuota: number, recentUsage: number): number | null type HealthLevel = 'healthy' | 'caution' | 'critical' -function getHealthLevel( - remainQuota: number, - recentUsage: number -): HealthLevel { +function getHealthLevel(remainQuota: number, recentUsage: number): HealthLevel { if (remainQuota <= 0) return 'critical' const days = getRunwayDays(remainQuota, recentUsage) if (days !== null && days < 3) return 'caution' @@ -139,7 +134,6 @@ const HEALTH_CONFIG: Record< }, } - export function SummaryCards() { const { t } = useTranslation() const user = useAuthStore((state) => state.auth.user) @@ -341,10 +335,7 @@ export function SummaryCards() { - diff --git a/web/default/src/features/dashboard/components/ui/stat-card.tsx b/web/default/src/features/dashboard/components/ui/stat-card.tsx index 0cc541f2..b2c9d129 100644 --- a/web/default/src/features/dashboard/components/ui/stat-card.tsx +++ b/web/default/src/features/dashboard/components/ui/stat-card.tsx @@ -96,8 +96,7 @@ function buildLineSparkline(values?: number[]) { sanitized.length === 1 ? width / 2 : (index / (sanitized.length - 1)) * width - const normalized = - range > 0 ? (value - min) / range : max > 0 ? 0.5 : 0 + const normalized = range > 0 ? (value - min) / range : max > 0 ? 0.5 : 0 const y = height - padding - normalized * (height - padding * 2) return { x, y } diff --git a/web/default/src/features/dashboard/index.tsx b/web/default/src/features/dashboard/index.tsx index 4e300be2..6f57e7a9 100644 --- a/web/default/src/features/dashboard/index.tsx +++ b/web/default/src/features/dashboard/index.tsx @@ -236,7 +236,7 @@ export function Dashboard() {
{showSectionTabs ? ( - + {visibleSections.map((section) => ( {t(SECTION_META[section].titleKey)} diff --git a/web/default/src/features/home/components/sections/hero.tsx b/web/default/src/features/home/components/sections/hero.tsx index 34f6d915..83ec3bec 100644 --- a/web/default/src/features/home/components/sections/hero.tsx +++ b/web/default/src/features/home/components/sections/hero.tsx @@ -65,7 +65,9 @@ export function Hero(props: HeroProps) { className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-lg text-base leading-relaxed opacity-0 md:text-lg' style={{ animationDelay: '80ms' }} > - {t('Power AI applications, manage digital assets, connect the Future')} + {t( + 'Power AI applications, manage digital assets, connect the Future' + )}

g.value === currentGroup)) { - const fallback = groups.find((g) => g.value === 'default')?.value ?? groups[0]?.value ?? '' + const fallback = + groups.find((g) => g.value === 'default')?.value ?? + groups[0]?.value ?? + '' form.setValue('group', fallback) if (currentGroup === 'auto') { form.setValue('cross_group_retry', false) diff --git a/web/default/src/features/keys/lib/api-key-form.ts b/web/default/src/features/keys/lib/api-key-form.ts index 692e359f..a2635db2 100644 --- a/web/default/src/features/keys/lib/api-key-form.ts +++ b/web/default/src/features/keys/lib/api-key-form.ts @@ -57,9 +57,7 @@ export function getApiKeyFormSchema(t: TFunction) { }) } -export type ApiKeyFormValues = z.infer< - ReturnType -> +export type ApiKeyFormValues = z.infer> // ============================================================================ // Form Defaults diff --git a/web/default/src/features/models/index.tsx b/web/default/src/features/models/index.tsx index 202005bb..71d96024 100644 --- a/web/default/src/features/models/index.tsx +++ b/web/default/src/features/models/index.tsx @@ -142,7 +142,7 @@ function ModelsContent() {
- + {MODELS_SECTION_IDS.map((section) => ( {t(SECTION_META[section].titleKey)} diff --git a/web/default/src/features/profile/components/profile-settings-card.tsx b/web/default/src/features/profile/components/profile-settings-card.tsx index 160386b1..9703104f 100644 --- a/web/default/src/features/profile/components/profile-settings-card.tsx +++ b/web/default/src/features/profile/components/profile-settings-card.tsx @@ -69,7 +69,7 @@ export function ProfileSettingsCard({ icon={} > - + - + {items.map((item) => ( @@ -689,7 +687,9 @@ export function SubscriptionsMutateDrawer({ type='button' variant='outline' onClick={handleCreatePancakeProduct} - disabled={creatingPancakeProduct || !pancakeCreateReady} + disabled={ + creatingPancakeProduct || !pancakeCreateReady + } className='shrink-0' > {creatingPancakeProduct diff --git a/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx b/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx index 8b0fa16a..94a4aafb 100644 --- a/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx +++ b/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx @@ -75,9 +75,7 @@ const DEFAULT_NEW_PAIR_NAME = `${DEFAULT_NEW_STORE_NAME} + ${DEFAULT_NEW_PRODUCT export function WaffoPancakeSettingsSection(props: Props) { const { t } = useTranslation() - const [storeID, setStoreID] = React.useState( - props.provisionedStoreID ?? '' - ) + const [storeID, setStoreID] = React.useState(props.provisionedStoreID ?? '') const [productID, setProductID] = React.useState( props.provisionedProductID ?? '' ) @@ -283,9 +281,7 @@ export function WaffoPancakeSettingsSection(props: Props) { // returning admins (saved merchant ID but empty key field) would send // a mixed-state body that the backend rejects. const readCreds = () => { - const formMerchant = ( - form.getValues('WaffoPancakeMerchantID') || '' - ).trim() + const formMerchant = (form.getValues('WaffoPancakeMerchantID') || '').trim() const formKey = (form.getValues('WaffoPancakePrivateKey') || '').trim() const saved = (props.defaultValues.WaffoPancakeMerchantID || '').trim() const edited = formMerchant !== saved || formKey.length > 0 @@ -370,12 +366,8 @@ export function WaffoPancakeSettingsSection(props: Props) { // Sends raw form values (not readCreds): SaveWaffoPancakeConfig already // treats a blank PrivateKey as "keep existing", and MerchantID stays // populated from props for returning admins. - const merchantID = ( - form.getValues('WaffoPancakeMerchantID') || '' - ).trim() - const privateKey = ( - form.getValues('WaffoPancakePrivateKey') || '' - ).trim() + const merchantID = (form.getValues('WaffoPancakeMerchantID') || '').trim() + const privateKey = (form.getValues('WaffoPancakePrivateKey') || '').trim() if (!merchantID) { toast.error(t('Merchant ID is required')) return diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index d512f79f..4753d44e 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -441,9 +441,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { {sensitiveVisible ? log.username : '••••'} {sensitiveVisible && log.username.length > 12 && ( - - {log.username} - + {log.username} )} @@ -484,11 +482,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] {
- - } - > + }> ( - handleChange('upstreamRequestId', e.target.value) - } + onChange={(e) => handleChange('upstreamRequestId', e.target.value)} onKeyDown={handleKeyDown} className={inputClass} /> diff --git a/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx b/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx index 929695a9..6f4adfb3 100644 --- a/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx +++ b/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx @@ -986,35 +986,33 @@ export function DetailsDialog(props: DetailsDialogProps) { )} {/* Param override */} - {other?.po && - Array.isArray(other.po) && - other.po.length > 0 && ( - - )} + {other?.po && Array.isArray(other.po) && other.po.length > 0 && ( + + )} {/* Content */} {details && ( diff --git a/web/default/src/features/usage-logs/index.tsx b/web/default/src/features/usage-logs/index.tsx index a975c78d..0e25253b 100644 --- a/web/default/src/features/usage-logs/index.tsx +++ b/web/default/src/features/usage-logs/index.tsx @@ -127,7 +127,7 @@ function UsageLogsContent() {
{showTaskSwitcher && ( - + {visibleSections.map((section) => ( {t(SECTION_META[section].titleKey)} diff --git a/web/default/src/features/wallet/lib/ui.tsx b/web/default/src/features/wallet/lib/ui.tsx index b9129302..5806b53f 100644 --- a/web/default/src/features/wallet/lib/ui.tsx +++ b/web/default/src/features/wallet/lib/ui.tsx @@ -17,9 +17,9 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { type ReactNode } from 'react' +import i18next from 'i18next' import { CreditCard, Landmark } from 'lucide-react' import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si' -import i18next from 'i18next' import { PAYMENT_TYPES, PAYMENT_ICON_COLORS } from '../constants' // ============================================================================ diff --git a/web/default/src/hooks/use-sidebar-data.ts b/web/default/src/hooks/use-sidebar-data.ts index f5c9a978..e1b99d03 100644 --- a/web/default/src/hooks/use-sidebar-data.ts +++ b/web/default/src/hooks/use-sidebar-data.ts @@ -17,39 +17,35 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { - LayoutDashboard, Activity, - Key, - FileText, - Wallet, Box, - Users, + CreditCard, + FileText, + FlaskConical, + Key, + LayoutDashboard, + ListTodo, + MessageSquare, + Radio, + Settings, Ticket, User, - Command, - Radio, - FlaskConical, - MessageSquare, - CreditCard, - ListTodo, - Settings, + Users, + Wallet, } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { WORKSPACE_IDS } from '@/components/layout/lib/workspace-registry' import { type SidebarData } from '@/components/layout/types' +/** + * Root navigation groups for the application sidebar. + * + * These are shown when the URL does not match any nested sidebar view + * registered in `layout/lib/sidebar-view-registry.ts`. + */ export function useSidebarData(): SidebarData { const { t } = useTranslation() return { - workspaces: [ - { - id: WORKSPACE_IDS.DEFAULT, - name: '', // Dynamically fetches system name - logo: Command, - plan: '', // Dynamically fetches system version - }, - ], navGroups: [ { id: 'chat', diff --git a/web/default/src/hooks/use-sidebar-view.ts b/web/default/src/hooks/use-sidebar-view.ts new file mode 100644 index 00000000..1b430db5 --- /dev/null +++ b/web/default/src/hooks/use-sidebar-view.ts @@ -0,0 +1,74 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { useMemo } from 'react' +import { useLocation } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { useAuthStore } from '@/stores/auth-store' +import { ROLE } from '@/lib/roles' +import { resolveSidebarView } from '@/components/layout/lib/sidebar-view-registry' +import type { NavGroup, ResolvedSidebarView } from '@/components/layout/types' +import { useSidebarConfig } from './use-sidebar-config' +import { useSidebarData } from './use-sidebar-data' + +/** Sentinel key used for the root navigation in animation `key=` props */ +const ROOT_VIEW_KEY = '__root' + +/** + * Resolve the active sidebar view for the current location. + * + * - Returns the matching nested {@link SidebarView} (with its nav + * groups) when the URL belongs to a registered drill-in workspace. + * - Otherwise returns the root navigation, narrowed by: + * · admin-only group visibility (role-based); + * · `useSidebarConfig` (admin × user `sidebar_modules` overlay). + * + * Nested views are intentionally NOT passed through `useSidebarConfig` + * — those filters target known dashboard URLs only, and gating is + * already enforced at the route level (`beforeLoad` redirects). + */ +export function useSidebarView(): ResolvedSidebarView { + const { t } = useTranslation() + const pathname = useLocation({ select: (l) => l.pathname }) + const userRole = useAuthStore((s) => s.auth.user?.role) + const rootSidebarData = useSidebarData() + const configFilteredRoot = useSidebarConfig(rootSidebarData.navGroups) + + const rootNavGroups = useMemo(() => { + const isAdmin = userRole !== undefined && userRole >= ROLE.ADMIN + return configFilteredRoot.filter((group) => + group.id === 'admin' ? isAdmin : true + ) + }, [configFilteredRoot, userRole]) + + const view = resolveSidebarView(pathname) + + if (view) { + return { + key: view.id, + view, + navGroups: view.getNavGroups(t), + } + } + + return { + key: ROOT_VIEW_KEY, + view: null, + navGroups: rootNavGroups, + } +} diff --git a/web/default/src/hooks/use-top-nav-links.ts b/web/default/src/hooks/use-top-nav-links.ts index 0897284f..a7996101 100644 --- a/web/default/src/hooks/use-top-nav-links.ts +++ b/web/default/src/hooks/use-top-nav-links.ts @@ -19,8 +19,8 @@ For commercial licensing, please contact support@quantumnous.com import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/stores/auth-store' -import { useStatus } from '@/hooks/use-status' import { parseHeaderNavModulesFromStatus } from '@/lib/nav-modules' +import { useStatus } from '@/hooks/use-status' export type TopNavLink = { title: string diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 41c34e44..8f710982 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -469,6 +469,7 @@ "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Back", + "Back to Dashboard": "Back to Dashboard", "Back to Home": "Back to Home", "Back to login": "Back to login", "Back to Models": "Back to Models", @@ -2241,7 +2242,6 @@ "Make it easier for teammates to pick the right group.": "Make it easier for teammates to pick the right group.", "Manage": "Manage", "Manage account bindings for this user": "Manage account bindings for this user", - "Manage and configure": "Manage and configure", "Manage API channels and provider configurations": "Manage API channels and provider configurations", "Manage Bindings": "Manage Bindings", "Manage catalog visibility and pricing.": "Manage catalog visibility and pricing.", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 7f1aae94..4b29cf81 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -469,6 +469,7 @@ "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Retour", + "Back to Dashboard": "Retour au tableau de bord", "Back to Home": "Retour à l'accueil", "Back to login": "Retour à la connexion", "Back to Models": "Retour aux modèles", @@ -2232,7 +2233,6 @@ "Make it easier for teammates to pick the right group.": "Faciliter le choix du bon groupe pour les coéquipiers.", "Manage": "Gestion", "Manage account bindings for this user": "Gérer les liaisons de compte pour cet utilisateur", - "Manage and configure": "Gérer et configurer", "Manage API channels and provider configurations": "Gérer les canaux d'API et les configurations des fournisseurs", "Manage Bindings": "Gérer les liaisons", "Manage catalog visibility and pricing.": "Gérer la visibilité du catalogue et les prix.", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index c317c612..0b9259b5 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -469,6 +469,7 @@ "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "戻る", + "Back to Dashboard": "ダッシュボードに戻る", "Back to Home": "ホームに戻る", "Back to login": "ログインに戻る", "Back to Models": "モデルに戻る", @@ -2232,7 +2233,6 @@ "Make it easier for teammates to pick the right group.": "チームメイトが適切なグループを選択しやすくする。", "Manage": "管理", "Manage account bindings for this user": "このユーザーのアカウントバインドを管理", - "Manage and configure": "管理と設定", "Manage API channels and provider configurations": "APIチャネルとプロバイダー構成を管理する", "Manage Bindings": "バインド管理", "Manage catalog visibility and pricing.": "カタログの表示と価格設定を管理。", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index baa9f290..db4f32da 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -469,6 +469,7 @@ "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Назад", + "Back to Dashboard": "Вернуться к панели управления", "Back to Home": "Вернуться на главную", "Back to login": "Вернуться к входу", "Back to Models": "Вернуться к моделям", @@ -2232,7 +2233,6 @@ "Make it easier for teammates to pick the right group.": "Упростите выбор правильной группы для товарищей по команде.", "Manage": "Управление", "Manage account bindings for this user": "Управление привязками аккаунта пользователя", - "Manage and configure": "Управление и настройка", "Manage API channels and provider configurations": "Управление каналами API и конфигурациями провайдеров", "Manage Bindings": "Управление привязками", "Manage catalog visibility and pricing.": "Управление видимостью каталога и ценообразованием.", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 2bc63aae..4901a0b0 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -469,6 +469,7 @@ "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Quay lại", + "Back to Dashboard": "Quay lại Bảng điều khiển", "Back to Home": "Trở về Trang chủ", "Back to login": "Quay lại đăng nhập", "Back to Models": "Quay lại Mô hình", @@ -2232,7 +2233,6 @@ "Make it easier for teammates to pick the right group.": "Giúp đồng đội dễ dàng chọn đúng nhóm hơn.", "Manage": "Quản lý", "Manage account bindings for this user": "Quản lý liên kết tài khoản cho người dùng này", - "Manage and configure": "Quản lý và cấu hình", "Manage API channels and provider configurations": "Quản lý các kênh API và cấu hình nhà cung cấp", "Manage Bindings": "Quản lý liên kết", "Manage catalog visibility and pricing.": "Quản lý hiển thị danh mục và giá cả.", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 0b1d3ad4..2525c348 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -469,6 +469,7 @@ "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "返回", + "Back to Dashboard": "返回控制台", "Back to Home": "返回主页", "Back to login": "返回登录", "Back to Models": "返回模型", @@ -2241,7 +2242,6 @@ "Make it easier for teammates to pick the right group.": "让队友更容易选择正确的分组。", "Manage": "管理", "Manage account bindings for this user": "管理此用户的账户绑定", - "Manage and configure": "管理和配置", "Manage API channels and provider configurations": "管理 API 渠道和提供商配置", "Manage Bindings": "管理绑定", "Manage catalog visibility and pricing.": "管理目录可见性和定价。", diff --git a/web/default/src/i18n/static-keys.ts b/web/default/src/i18n/static-keys.ts index 3576e8c1..0942b198 100644 --- a/web/default/src/i18n/static-keys.ts +++ b/web/default/src/i18n/static-keys.ts @@ -27,9 +27,9 @@ export const STATIC_I18N_KEYS = [ 'Docs', 'About', - // Workspace + // Sidebar views (drill-in workspaces) 'System Settings', - 'Manage and configure', + 'Back to Dashboard', // System settings sidebar 'System Administration', diff --git a/web/default/src/routes/__root.tsx b/web/default/src/routes/__root.tsx index 1af569a1..1a1fd826 100644 --- a/web/default/src/routes/__root.tsx +++ b/web/default/src/routes/__root.tsx @@ -29,10 +29,10 @@ import { ThemeCustomizationProvider } from '@/context/theme-customization-provid import { useSystemConfig } from '@/hooks/use-system-config' import { Toaster } from '@/components/ui/sonner' import { NavigationProgress } from '@/components/navigation-progress' +import { saveAffiliateCode } from '@/features/auth/lib/storage' import { GeneralError } from '@/features/errors/general-error' import { NotFoundError } from '@/features/errors/not-found-error' import { getSetupStatus } from '@/features/setup/api' -import { saveAffiliateCode } from '@/features/auth/lib/storage' function RootComponent() { // Load system configuration (logo, system name, etc.) from backend diff --git a/web/default/src/routes/_authenticated/playground/index.tsx b/web/default/src/routes/_authenticated/playground/index.tsx index a755accc..6a6aa4d4 100644 --- a/web/default/src/routes/_authenticated/playground/index.tsx +++ b/web/default/src/routes/_authenticated/playground/index.tsx @@ -17,9 +17,9 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { createFileRoute, redirect } from '@tanstack/react-router' +import { isSidebarModuleEnabled } from '@/lib/nav-modules' import { Main } from '@/components/layout' import { Playground } from '@/features/playground' -import { isSidebarModuleEnabled } from '@/lib/nav-modules' export const Route = createFileRoute('/_authenticated/playground/')({ beforeLoad: () => {