@lobehub/chat 0.152.11 → 0.153.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/locales/ar/common.json +1 -0
  5. package/locales/bg-BG/common.json +1 -0
  6. package/locales/de-DE/common.json +1 -0
  7. package/locales/en-US/common.json +1 -0
  8. package/locales/es-ES/common.json +1 -0
  9. package/locales/fr-FR/common.json +1 -0
  10. package/locales/it-IT/common.json +1 -0
  11. package/locales/ja-JP/common.json +1 -0
  12. package/locales/ko-KR/common.json +1 -0
  13. package/locales/nl-NL/common.json +1 -0
  14. package/locales/pl-PL/common.json +1 -0
  15. package/locales/pt-BR/common.json +1 -0
  16. package/locales/ru-RU/common.json +1 -0
  17. package/locales/tr-TR/common.json +1 -0
  18. package/locales/vi-VN/common.json +1 -0
  19. package/locales/zh-CN/common.json +1 -0
  20. package/locales/zh-TW/common.json +1 -0
  21. package/package.json +1 -1
  22. package/src/app/(main)/chat/(desktop)/features/ChatHeader/Main.tsx +4 -9
  23. package/src/app/(main)/chat/(desktop)/features/SideBar/SystemRole/index.tsx +5 -6
  24. package/src/app/(main)/chat/features/SettingButton.tsx +3 -17
  25. package/src/app/(main)/chat/features/TopicListContent/Header.tsx +1 -1
  26. package/src/app/(main)/chat/settings/_layout/Desktop/Header.tsx +2 -1
  27. package/src/app/(main)/chat/settings/_layout/Mobile/Header.tsx +2 -1
  28. package/src/app/(main)/chat/settings/modal/page.tsx +23 -0
  29. package/src/app/(main)/settings/@category/features/CategoryContent.tsx +5 -1
  30. package/src/app/(main)/settings/modal/page.tsx +27 -0
  31. package/src/app/@modal/(.)settings/modal/index.tsx +40 -0
  32. package/src/app/@modal/(.)settings/modal/layout.tsx +32 -0
  33. package/src/app/@modal/(.)settings/modal/loading.tsx +5 -0
  34. package/src/app/@modal/(.)settings/modal/page.tsx +19 -0
  35. package/src/app/@modal/_layout/SettingModalLayout.tsx +59 -0
  36. package/src/app/@modal/chat/(.)settings/modal/features/CategoryContent.tsx +37 -0
  37. package/src/app/@modal/chat/(.)settings/modal/features/useCategory.tsx +54 -0
  38. package/src/app/@modal/chat/(.)settings/modal/layout.tsx +55 -0
  39. package/src/app/@modal/chat/(.)settings/modal/loading.tsx +5 -0
  40. package/src/app/@modal/chat/(.)settings/modal/page.tsx +55 -0
  41. package/src/app/@modal/default.tsx +3 -0
  42. package/src/app/@modal/error.tsx +5 -0
  43. package/src/app/@modal/layout.tsx +30 -0
  44. package/src/app/@modal/loading.tsx +5 -0
  45. package/src/app/layout.tsx +6 -2
  46. package/src/features/AgentSetting/AgentMeta/AutoGenerateInput.tsx +31 -26
  47. package/src/features/AgentSetting/AgentMeta/AutoGenerateSelect.tsx +3 -1
  48. package/src/features/AgentSetting/AgentMeta/index.tsx +2 -0
  49. package/src/features/Conversation/Messages/index.ts +9 -12
  50. package/src/features/MobileTabBar/index.tsx +3 -3
  51. package/src/features/User/UserPanel/useMenu.tsx +31 -16
  52. package/src/hooks/useInterceptingRoutes.test.ts +70 -0
  53. package/src/hooks/useInterceptingRoutes.ts +46 -0
  54. package/src/hooks/useQuery.test.ts +0 -1
  55. package/src/hooks/useQuery.ts +2 -1
  56. package/src/layout/GlobalProvider/StoreInitialization.tsx +12 -5
  57. package/src/locales/default/common.ts +2 -1
  58. package/src/store/global/initialState.ts +9 -0
  59. package/src/features/User/UserPanel/UserInfo.tsx +0 -35
  60. /package/src/{app/(main)/chat/components → components}/SidebarHeader/index.tsx +0 -0
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { useResponsive, useTheme, useThemeMode } from 'antd-style';
4
+ import { ReactNode, memo, useRef } from 'react';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ import Header from '@/app/(main)/settings/_layout/Desktop/Header';
8
+ import SideBar from '@/app/(main)/settings/_layout/Desktop/SideBar';
9
+
10
+ interface SettingLayoutProps {
11
+ category: ReactNode;
12
+ children: ReactNode;
13
+ desc?: string;
14
+ title?: string;
15
+ }
16
+
17
+ const SettingModalLayout = memo<SettingLayoutProps>(({ children, category, desc, title }) => {
18
+ const ref = useRef<any>(null);
19
+ const theme = useTheme();
20
+ const { isDarkMode } = useThemeMode();
21
+ const { md = true } = useResponsive();
22
+
23
+ return (
24
+ <>
25
+ {md ? (
26
+ <SideBar
27
+ desc={desc}
28
+ style={{
29
+ background: isDarkMode ? theme.colorBgContainer : theme.colorFillTertiary,
30
+ borderColor: theme.colorFillTertiary,
31
+ }}
32
+ title={title}
33
+ >
34
+ {category}
35
+ </SideBar>
36
+ ) : (
37
+ <Header getContainer={() => ref.current}>{category}</Header>
38
+ )}
39
+ <Flexbox
40
+ align={'center'}
41
+ gap={64}
42
+ style={{
43
+ background: isDarkMode ? theme.colorFillQuaternary : theme.colorBgElevated,
44
+ overflowX: 'hidden',
45
+ overflowY: 'auto',
46
+ paddingBlock: 40,
47
+ paddingInline: 56,
48
+ }}
49
+ width={'100%'}
50
+ >
51
+ {children}
52
+ </Flexbox>
53
+ </>
54
+ );
55
+ });
56
+
57
+ SettingModalLayout.displayName = 'SettingModalLayout';
58
+
59
+ export default SettingModalLayout;
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import HeaderContent from '@/app/(main)/chat/settings/features/HeaderContent';
7
+ import Menu from '@/components/Menu';
8
+ import { useQuery } from '@/hooks/useQuery';
9
+ import { useQueryRoute } from '@/hooks/useQueryRoute';
10
+ import { ChatSettingsTabs } from '@/store/global/initialState';
11
+
12
+ import { useCategory } from './useCategory';
13
+
14
+ const CategoryContent = memo(() => {
15
+ const cateItems = useCategory();
16
+ const router = useQueryRoute();
17
+ const { tab = ChatSettingsTabs.Meta } = useQuery();
18
+
19
+ return (
20
+ <>
21
+ <Menu
22
+ items={cateItems}
23
+ onClick={({ key }) => {
24
+ router.replace('/chat/settings/modal', { query: { tab: key } });
25
+ }}
26
+ selectable
27
+ selectedKeys={[tab as any]}
28
+ variant={'compact'}
29
+ />
30
+ <Flexbox align={'center'} gap={8} paddingInline={8} width={'100%'}>
31
+ <HeaderContent modal />
32
+ </Flexbox>
33
+ </>
34
+ );
35
+ });
36
+
37
+ export default CategoryContent;
@@ -0,0 +1,54 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Blocks, Bot, BrainCog, MessagesSquare, Mic2, UserCircle } from 'lucide-react';
3
+ import { useMemo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import type { MenuProps } from '@/components/Menu';
7
+ import { ChatSettingsTabs } from '@/store/global/initialState';
8
+
9
+ interface UseCategoryOptions {
10
+ mobile?: boolean;
11
+ }
12
+
13
+ export const useCategory = ({ mobile }: UseCategoryOptions = {}) => {
14
+ const { t } = useTranslation('setting');
15
+ const iconSize = mobile ? { fontSize: 20 } : undefined;
16
+
17
+ const cateItems: MenuProps['items'] = useMemo(
18
+ () => [
19
+ {
20
+ icon: <Icon icon={UserCircle} size={iconSize} />,
21
+ key: ChatSettingsTabs.Meta,
22
+ label: t('settingAgent.title'),
23
+ },
24
+ {
25
+ icon: <Icon icon={Bot} size={iconSize} />,
26
+ key: ChatSettingsTabs.Prompt,
27
+ label: t('settingAgent.prompt.title'),
28
+ },
29
+ {
30
+ icon: <Icon icon={MessagesSquare} size={iconSize} />,
31
+ key: ChatSettingsTabs.Chat,
32
+ label: t('settingChat.title'),
33
+ },
34
+ {
35
+ icon: <Icon icon={BrainCog} size={iconSize} />,
36
+ key: ChatSettingsTabs.Modal,
37
+ label: t('settingModel.title'),
38
+ },
39
+ {
40
+ icon: <Icon icon={Mic2} size={iconSize} />,
41
+ key: ChatSettingsTabs.TTS,
42
+ label: t('settingTTS.title'),
43
+ },
44
+ {
45
+ icon: <Icon icon={Blocks} size={iconSize} />,
46
+ key: ChatSettingsTabs.Plugin,
47
+ label: t('settingPlugin.title'),
48
+ },
49
+ ],
50
+ [t],
51
+ );
52
+
53
+ return cateItems;
54
+ };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { Skeleton } from 'antd';
4
+ import isEqual from 'fast-deep-equal';
5
+ import dynamic from 'next/dynamic';
6
+ import { PropsWithChildren, memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import StoreUpdater from '@/features/AgentSetting/StoreUpdater';
10
+ import { Provider, createStore } from '@/features/AgentSetting/store';
11
+ import { useAgentStore } from '@/store/agent';
12
+ import { agentSelectors } from '@/store/agent/slices/chat';
13
+ import { useSessionStore } from '@/store/session';
14
+ import { sessionMetaSelectors } from '@/store/session/selectors';
15
+
16
+ import SettingModalLayout from '../../../_layout/SettingModalLayout';
17
+
18
+ const CategoryContent = dynamic(() => import('./features/CategoryContent'), {
19
+ loading: () => <Skeleton paragraph={{ rows: 6 }} title={false} />,
20
+ ssr: false,
21
+ });
22
+
23
+ const Layout = memo<PropsWithChildren>(({ children }) => {
24
+ const { t } = useTranslation('setting');
25
+ const id = useSessionStore((s) => s.activeId);
26
+ const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
27
+ const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual);
28
+ const [updateAgentConfig] = useAgentStore((s) => [s.updateAgentConfig]);
29
+
30
+ const [updateAgentMeta] = useSessionStore((s) => [
31
+ s.updateSessionMeta,
32
+ sessionMetaSelectors.currentAgentTitle(s),
33
+ ]);
34
+
35
+ return (
36
+ <SettingModalLayout
37
+ category={<CategoryContent />}
38
+ desc={t('header.sessionDesc')}
39
+ title={t('header.session')}
40
+ >
41
+ <Provider createStore={createStore}>
42
+ <StoreUpdater
43
+ config={config}
44
+ id={id}
45
+ meta={meta}
46
+ onConfigChange={updateAgentConfig}
47
+ onMetaChange={updateAgentMeta}
48
+ />
49
+ {children}
50
+ </Provider>
51
+ </SettingModalLayout>
52
+ );
53
+ });
54
+
55
+ export default Layout;
@@ -0,0 +1,5 @@
1
+ import { Skeleton } from 'antd';
2
+
3
+ export default () => {
4
+ return <Skeleton paragraph={{ rows: 6 }} style={{ paddingBlock: 16 }} />;
5
+ };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+
5
+ import { useQuery } from '@/hooks/useQuery';
6
+ import { ChatSettingsTabs } from '@/store/global/initialState';
7
+
8
+ import Skeleton from './loading';
9
+
10
+ const loading = () => <Skeleton />;
11
+
12
+ const AgentMeta = dynamic(() => import('@/features/AgentSetting/AgentMeta'), {
13
+ loading,
14
+ ssr: false,
15
+ });
16
+ const AgentChat = dynamic(() => import('@/features/AgentSetting/AgentChat'), {
17
+ loading,
18
+ ssr: false,
19
+ });
20
+ const AgentPrompt = dynamic(() => import('@/features/AgentSetting/AgentPrompt'), {
21
+ loading,
22
+ ssr: false,
23
+ });
24
+ const AgentPlugin = dynamic(() => import('@/features/AgentSetting/AgentPlugin'), {
25
+ loading,
26
+ ssr: false,
27
+ });
28
+ const AgentModal = dynamic(() => import('@/features/AgentSetting/AgentModal'), {
29
+ loading,
30
+ ssr: false,
31
+ });
32
+ const AgentTTS = dynamic(() => import('@/features/AgentSetting/AgentTTS'), { loading, ssr: false });
33
+
34
+ /**
35
+ * @description: Agent Settings Modal (intercepting route: /chat/settings/modal )
36
+ * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942
37
+ */
38
+
39
+ const Page = () => {
40
+ const { tab = ChatSettingsTabs.Meta } = useQuery();
41
+ return (
42
+ <>
43
+ {tab === ChatSettingsTabs.Meta && <AgentMeta />}
44
+ {tab === ChatSettingsTabs.Prompt && <AgentPrompt modal />}
45
+ {tab === ChatSettingsTabs.Chat && <AgentChat />}
46
+ {tab === ChatSettingsTabs.Modal && <AgentModal />}
47
+ {tab === ChatSettingsTabs.TTS && <AgentTTS />}
48
+ {tab === ChatSettingsTabs.Plugin && <AgentPlugin />}
49
+ </>
50
+ );
51
+ };
52
+
53
+ Page.displayName = 'AgentSettingModal';
54
+
55
+ export default Page;
@@ -0,0 +1,3 @@
1
+ // This ensures that the modal is not rendered when it's not active.
2
+
3
+ export default () => null;
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+
5
+ export default dynamic(() => import('@/components/Error'));
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { Modal } from '@lobehub/ui';
4
+ import { useRouter } from 'next/navigation';
5
+ import { PropsWithChildren, memo, useState } from 'react';
6
+
7
+ const SessionSettingsModal = memo<PropsWithChildren>(({ children }) => {
8
+ const [open, setOpen] = useState(true);
9
+ const router = useRouter();
10
+
11
+ return (
12
+ <Modal
13
+ afterClose={() => {
14
+ router.back();
15
+ }}
16
+ footer={null}
17
+ onCancel={() => setOpen(false)}
18
+ open={open}
19
+ styles={{
20
+ body: { display: 'flex', minHeight: 'min(75vh, 750px)', overflow: 'hidden', padding: 0 },
21
+ }}
22
+ title={false}
23
+ width={1024}
24
+ >
25
+ {children}
26
+ </Modal>
27
+ );
28
+ });
29
+
30
+ export default SessionSettingsModal;
@@ -0,0 +1,5 @@
1
+ import { Skeleton } from 'antd';
2
+
3
+ export default () => {
4
+ return <Skeleton paragraph={{ rows: 6 }} style={{ padding: 56 }} />;
5
+ };
@@ -12,9 +12,10 @@ import { isMobileDevice } from '@/utils/responsive';
12
12
 
13
13
  type RootLayoutProps = {
14
14
  children: ReactNode;
15
+ modal: ReactNode;
15
16
  };
16
17
 
17
- const RootLayout = async ({ children }: RootLayoutProps) => {
18
+ const RootLayout = async ({ children, modal }: RootLayoutProps) => {
18
19
  const cookieStore = cookies();
19
20
 
20
21
  const lang = cookieStore.get(LOBE_LOCALE_COOKIE);
@@ -24,7 +25,10 @@ const RootLayout = async ({ children }: RootLayoutProps) => {
24
25
  <html dir={direction} lang={lang?.value || DEFAULT_LANG} suppressHydrationWarning>
25
26
  <body>
26
27
  <GlobalProvider>
27
- <AuthProvider>{children}</AuthProvider>
28
+ <AuthProvider>
29
+ {children}
30
+ {modal}
31
+ </AuthProvider>
28
32
  </GlobalProvider>
29
33
  <Analytics />
30
34
  <SpeedInsights />
@@ -5,37 +5,42 @@ import { Wand2 } from 'lucide-react';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
+
8
9
  export interface AutoGenerateInputProps extends InputProps {
10
+ canAutoGenerate?: boolean;
9
11
  loading?: boolean;
10
12
  onGenerate?: () => void;
11
13
  }
12
14
 
13
- const AutoGenerateInput = memo<AutoGenerateInputProps>(({ loading, onGenerate, ...props }) => {
14
- const { t } = useTranslation('common');
15
- const theme = useTheme();
15
+ const AutoGenerateInput = memo<AutoGenerateInputProps>(
16
+ ({ loading, onGenerate, canAutoGenerate, ...props }) => {
17
+ const { t } = useTranslation('common');
18
+ const theme = useTheme();
16
19
 
17
- return (
18
- <Input
19
- suffix={
20
- onGenerate && (
21
- <ActionIcon
22
- active
23
- icon={Wand2}
24
- loading={loading}
25
- onClick={onGenerate}
26
- size={'small'}
27
- style={{
28
- color: theme.colorInfo,
29
- marginRight: -4,
30
- }}
31
- title={t('autoGenerate')}
32
- />
33
- )
34
- }
35
- type={'block'}
36
- {...props}
37
- />
38
- );
39
- });
20
+ return (
21
+ <Input
22
+ suffix={
23
+ onGenerate && (
24
+ <ActionIcon
25
+ active
26
+ disable={!canAutoGenerate}
27
+ icon={Wand2}
28
+ loading={loading}
29
+ onClick={onGenerate}
30
+ size="small"
31
+ style={{
32
+ color: theme.colorInfo,
33
+ marginRight: -4,
34
+ }}
35
+ title={t('autoGenerate')}
36
+ />
37
+ )
38
+ }
39
+ type="block"
40
+ {...props}
41
+ />
42
+ );
43
+ },
44
+ );
40
45
 
41
46
  export default AutoGenerateInput;
@@ -7,12 +7,13 @@ import { memo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
9
  export interface AutoGenerateInputProps extends SelectProps {
10
+ canAutoGenerate?: boolean;
10
11
  loading?: boolean;
11
12
  onGenerate?: () => void;
12
13
  }
13
14
 
14
15
  const AutoGenerateSelect = memo<AutoGenerateInputProps>(
15
- ({ loading, onGenerate, value, ...props }) => {
16
+ ({ loading, onGenerate, value, canAutoGenerate, ...props }) => {
16
17
  const { t } = useTranslation('common');
17
18
  const theme = useTheme();
18
19
 
@@ -25,6 +26,7 @@ const AutoGenerateSelect = memo<AutoGenerateInputProps>(
25
26
  onGenerate && (
26
27
  <ActionIcon
27
28
  active
29
+ disable={!canAutoGenerate}
28
30
  icon={Wand2}
29
31
  loading={loading}
30
32
  onClick={onGenerate}
@@ -63,6 +63,8 @@ const AgentMeta = memo(() => {
63
63
  return {
64
64
  children: (
65
65
  <AutoGenerate
66
+ canAutoGenerate={hasSystemRole}
67
+ disabled={!hasSystemRole}
66
68
  loading={loading[item.key as keyof SessionLoadingState]}
67
69
  onChange={item.onChange}
68
70
  onGenerate={() => {
@@ -1,10 +1,7 @@
1
- import { useResponsive } from 'antd-style';
2
- import { useRouter } from 'next/navigation';
3
-
1
+ import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
4
2
  import { useGlobalStore } from '@/store/global';
5
3
  import { useSessionStore } from '@/store/session';
6
4
  import { sessionSelectors } from '@/store/session/selectors';
7
- import { pathString } from '@/utils/url';
8
5
 
9
6
  import { OnAvatarsClick, RenderMessage } from '../types';
10
7
  import { AssistantMessage } from './Assistant';
@@ -22,18 +19,18 @@ export const renderMessages: Record<string, RenderMessage> = {
22
19
  export const useAvatarsClick = (): OnAvatarsClick => {
23
20
  const [isInbox] = useSessionStore((s) => [sessionSelectors.isInboxSession(s)]);
24
21
  const [toggleSystemRole] = useGlobalStore((s) => [s.toggleSystemRole]);
25
- const { mobile } = useResponsive();
26
- const router = useRouter();
22
+ const openChatSettings = useOpenChatSettings();
27
23
 
28
24
  return (role) => {
29
25
  switch (role) {
30
26
  case 'assistant': {
31
- return () =>
32
- isInbox
33
- ? router.push('/settings/agent')
34
- : mobile
35
- ? router.push(pathString('/chat/settings', { search: location.search }))
36
- : toggleSystemRole(true);
27
+ return () => {
28
+ if (!isInbox) {
29
+ toggleSystemRole(true);
30
+ } else {
31
+ openChatSettings();
32
+ }
33
+ };
37
34
  }
38
35
  }
39
36
  };
@@ -6,6 +6,7 @@ import { rgba } from 'polished';
6
6
  import { memo, useMemo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
+ import { useOpenSettings } from '@/hooks/useInterceptingRoutes';
9
10
  import { SidebarTabKey } from '@/store/global/initialState';
10
11
 
11
12
  const useStyles = createStyles(({ css, token }) => ({
@@ -24,6 +25,7 @@ interface Props {
24
25
  export default memo<Props>(({ className, tabBarKey }) => {
25
26
  const { t } = useTranslation('common');
26
27
  const { styles } = useStyles();
28
+ const openSettings = useOpenSettings();
27
29
  const router = useRouter();
28
30
  const items: MobileTabBarProps['items'] = useMemo(
29
31
  () => [
@@ -48,9 +50,7 @@ export default memo<Props>(({ className, tabBarKey }) => {
48
50
  {
49
51
  icon: (active) => <Icon className={active ? styles.active : undefined} icon={User} />,
50
52
  key: SidebarTabKey.Setting,
51
- onClick: () => {
52
- router.push('/settings');
53
- },
53
+ onClick: openSettings,
54
54
  title: t('tab.setting'),
55
55
  },
56
56
  ],
@@ -1,4 +1,4 @@
1
- import { DiscordIcon, Icon } from '@lobehub/ui';
1
+ import { ActionIcon, DiscordIcon, Icon } from '@lobehub/ui';
2
2
  import { Badge } from 'antd';
3
3
  import {
4
4
  Book,
@@ -7,22 +7,29 @@ import {
7
7
  HardDriveUpload,
8
8
  LifeBuoy,
9
9
  Mail,
10
+ Maximize,
10
11
  Settings2,
11
12
  } from 'lucide-react';
12
13
  import Link from 'next/link';
13
14
  import { PropsWithChildren, useCallback } from 'react';
14
15
  import { useTranslation } from 'react-i18next';
15
16
  import { Flexbox } from 'react-layout-kit';
17
+ import urlJoin from 'url-join';
16
18
 
17
19
  import { type MenuProps } from '@/components/Menu';
18
20
  import { DISCORD, DOCUMENTS, EMAIL_SUPPORT, GITHUB_ISSUES } from '@/const/url';
19
21
  import DataImporter from '@/features/DataImporter';
22
+ import { useOpenSettings } from '@/hooks/useInterceptingRoutes';
23
+ import { useQueryRoute } from '@/hooks/useQueryRoute';
20
24
  import { configService } from '@/services/config';
25
+ import { SettingsTabs } from '@/store/global/initialState';
21
26
 
22
27
  import { useNewVersion } from './useNewVersion';
23
28
 
24
29
  export const useMenu = () => {
30
+ const router = useQueryRoute();
25
31
  const hasNewVersion = useNewVersion();
32
+ const openSettings = useOpenSettings();
26
33
  const { t } = useTranslation(['common', 'setting']);
27
34
 
28
35
  const NewVersionBadge = useCallback(
@@ -38,6 +45,29 @@ export const useMenu = () => {
38
45
  [t],
39
46
  );
40
47
 
48
+ const settings: MenuProps['items'] = [
49
+ {
50
+ icon: <Icon icon={Settings2} />,
51
+ key: 'setting',
52
+ label: (
53
+ <Flexbox align={'center'} horizontal>
54
+ <Flexbox flex={1} horizontal onClick={openSettings}>
55
+ <NewVersionBadge showBadge={hasNewVersion}>{t('userPanel.setting')}</NewVersionBadge>
56
+ </Flexbox>
57
+ <ActionIcon
58
+ icon={Maximize}
59
+ onClick={() => router.push(urlJoin('/settings', SettingsTabs.Common))}
60
+ size={'small'}
61
+ title={t('fullscreen')}
62
+ />
63
+ </Flexbox>
64
+ ),
65
+ },
66
+ {
67
+ type: 'divider',
68
+ },
69
+ ];
70
+
41
71
  const exports: MenuProps['items'] = [
42
72
  {
43
73
  icon: <Icon icon={HardDriveUpload} />,
@@ -79,21 +109,6 @@ export const useMenu = () => {
79
109
  },
80
110
  ];
81
111
 
82
- const settings: MenuProps['items'] = [
83
- {
84
- icon: <Icon icon={Settings2} />,
85
- key: 'setting',
86
- label: (
87
- <Link href={'/settings'}>
88
- <NewVersionBadge showBadge={hasNewVersion}>{t('userPanel.setting')}</NewVersionBadge>
89
- </Link>
90
- ),
91
- },
92
- {
93
- type: 'divider',
94
- },
95
- ];
96
-
97
112
  const helps: MenuProps['items'] = [
98
113
  {
99
114
  icon: <Icon icon={DiscordIcon} />,
@@ -0,0 +1,70 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import urlJoin from 'url-join';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { useIsMobile } from '@/hooks/useIsMobile';
7
+ import { useGlobalStore } from '@/store/global';
8
+ import { ChatSettingsTabs, SettingsTabs, SidebarTabKey } from '@/store/global/initialState';
9
+ import { useSessionStore } from '@/store/session';
10
+
11
+ import { useOpenChatSettings, useOpenSettings } from './useInterceptingRoutes';
12
+
13
+ // Mocks
14
+ vi.mock('next/navigation', () => ({
15
+ useRouter: vi.fn(() => ({
16
+ push: vi.fn((href) => href),
17
+ replace: vi.fn((href) => href),
18
+ })),
19
+ }));
20
+ vi.mock('@/hooks/useQuery', () => ({
21
+ useQuery: vi.fn(() => ({})),
22
+ }));
23
+ vi.mock('@/hooks/useIsMobile', () => ({
24
+ useIsMobile: vi.fn(),
25
+ }));
26
+ vi.mock('@/store/session', () => ({
27
+ useSessionStore: vi.fn(),
28
+ }));
29
+ vi.mock('@/store/global', () => ({
30
+ useGlobalStore: {
31
+ setState: vi.fn(),
32
+ },
33
+ }));
34
+
35
+ describe('useOpenSettings', () => {
36
+ it('should handle mobile route correctly', () => {
37
+ vi.mocked(useIsMobile).mockReturnValue(true);
38
+ const { result } = renderHook(() => useOpenSettings(SettingsTabs.Common));
39
+ expect(result.current()).toBe('/settings/common');
40
+ });
41
+
42
+ it('should handle desktop route correctly', () => {
43
+ vi.mocked(useIsMobile).mockReturnValue(false);
44
+ const { result } = renderHook(() => useOpenSettings(SettingsTabs.Agent));
45
+ expect(result.current()).toBe('/settings/modal?tab=agent');
46
+ });
47
+ });
48
+
49
+ describe('useOpenChatSettings', () => {
50
+ it('should handle inbox session id correctly', () => {
51
+ vi.mocked(useSessionStore).mockReturnValue(INBOX_SESSION_ID);
52
+ const { result } = renderHook(() => useOpenChatSettings());
53
+
54
+ expect(result.current()).toBe('/settings/modal?session=inbox&tab=agent'); // Assuming openSettings returns a function
55
+ });
56
+
57
+ it('should handle mobile route for chat settings', () => {
58
+ vi.mocked(useSessionStore).mockReturnValue('123');
59
+ vi.mocked(useIsMobile).mockReturnValue(true);
60
+ const { result } = renderHook(() => useOpenChatSettings(ChatSettingsTabs.Meta));
61
+ expect(result.current()).toBe('/chat/settings');
62
+ });
63
+
64
+ it('should handle desktop route for chat settings with session and tab', () => {
65
+ vi.mocked(useSessionStore).mockReturnValue('456');
66
+ vi.mocked(useIsMobile).mockReturnValue(false);
67
+ const { result } = renderHook(() => useOpenChatSettings(ChatSettingsTabs.Meta));
68
+ expect(result.current()).toBe('/chat/settings/modal?session=456&tab=meta');
69
+ });
70
+ });