@lobehub/chat 0.127.0 → 0.127.1

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 (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/app/chat/features/ChatHeader/SettingButton.tsx +1 -2
  4. package/src/app/settings/(desktop)/features/Header.tsx +3 -4
  5. package/src/app/settings/(desktop)/features/SideBar.tsx +42 -0
  6. package/src/app/settings/(desktop)/index.tsx +4 -3
  7. package/src/app/settings/(desktop)/layout.desktop.tsx +14 -6
  8. package/src/app/settings/(desktop)/layout.responsive.tsx +4 -4
  9. package/src/app/settings/(mobile)/features/ExtraList.tsx +7 -5
  10. package/src/app/settings/(mobile)/features/Header/index.tsx +8 -5
  11. package/src/app/settings/(mobile)/index.tsx +1 -3
  12. package/src/app/settings/(mobile)/layout.mobile.tsx +8 -4
  13. package/src/app/settings/(mobile)/mobile/index.tsx +3 -5
  14. package/src/app/settings/agent/Agent.tsx +0 -4
  15. package/src/app/settings/agent/layout.tsx +9 -1
  16. package/src/app/settings/agent/page.tsx +17 -2
  17. package/src/app/settings/common/index.tsx +0 -3
  18. package/src/app/settings/common/layout.tsx +9 -1
  19. package/src/app/settings/features/SettingList/Item.tsx +50 -0
  20. package/src/app/settings/features/{SideBar/List.tsx → SettingList/index.tsx} +14 -15
  21. package/src/app/settings/layout.server.tsx +10 -3
  22. package/src/app/settings/llm/Azure/index.tsx +78 -106
  23. package/src/app/settings/llm/Bedrock/index.tsx +60 -89
  24. package/src/app/settings/llm/Google/index.tsx +26 -55
  25. package/src/app/settings/llm/Moonshot/index.tsx +29 -54
  26. package/src/app/settings/llm/Ollama/index.tsx +35 -61
  27. package/src/app/settings/llm/OpenAI/index.tsx +91 -107
  28. package/src/app/settings/llm/Zhipu/index.tsx +29 -54
  29. package/src/app/settings/llm/components/ProviderConfig/index.tsx +58 -0
  30. package/src/app/settings/llm/index.tsx +42 -0
  31. package/src/app/settings/llm/layout.tsx +9 -1
  32. package/src/app/settings/llm/page.tsx +6 -46
  33. package/src/app/settings/tts/layout.tsx +9 -1
  34. package/src/app/settings/tts/page.tsx +17 -2
  35. package/src/const/settings.ts +1 -0
  36. package/src/features/SideBar/BottomActions.tsx +1 -2
  37. package/src/layout/ResponsiveLayout.client.tsx +7 -7
  38. package/src/layout/ResponsiveLayout.server.tsx +6 -6
  39. package/src/services/_auth.test.ts +132 -0
  40. package/src/services/_auth.ts +1 -1
  41. package/src/store/global/slices/common/initialState.ts +0 -2
  42. package/src/store/global/slices/settings/action.test.ts +0 -13
  43. package/src/store/global/slices/settings/action.ts +0 -5
  44. package/src/store/global/slices/settings/selectors/modelProvider.ts +7 -0
  45. package/src/types/settings/modelProvider.ts +1 -0
  46. package/src/app/settings/agent/index.tsx +0 -21
  47. package/src/app/settings/features/SideBar/Item.tsx +0 -44
  48. package/src/app/settings/features/SideBar/index.tsx +0 -42
  49. package/src/app/settings/tts/index.tsx +0 -21
  50. package/src/store/global/hooks/useSwitchSettingsOnInit.ts +0 -12
  51. /package/src/app/settings/llm/{Checker.tsx → components/Checker.tsx} +0 -0
  52. /package/src/app/settings/llm/{useSyncSettings.ts → components/ProviderConfig/useSyncSettings.ts} +0 -0
@@ -1,26 +1,26 @@
1
1
  'use client';
2
2
 
3
- import { PropsWithChildren, ReactNode, Suspense } from 'react';
3
+ import { ReactNode, Suspense } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import FullscreenLoading from '@/components/FullscreenLoading';
7
7
  import { useIsMobile } from '@/hooks/useIsMobile';
8
8
 
9
- interface ServerResponsiveLayoutProps {
10
- Desktop: (props: PropsWithChildren) => ReactNode;
11
- Mobile: (props: PropsWithChildren) => ReactNode;
9
+ interface ServerResponsiveLayoutProps extends Record<string, any> {
10
+ Desktop: (props: any) => ReactNode;
11
+ Mobile: (props: any) => ReactNode;
12
12
  children?: ReactNode;
13
13
  }
14
- const ResponsiveLayout = ({ children, Desktop, Mobile }: ServerResponsiveLayoutProps) => {
14
+ const ResponsiveLayout = ({ children, Desktop, Mobile, ...res }: ServerResponsiveLayoutProps) => {
15
15
  const { t } = useTranslation();
16
16
  const mobile = useIsMobile();
17
17
 
18
18
  return mobile ? (
19
19
  <Suspense fallback={<FullscreenLoading title={t('layoutInitializing', { ns: 'common' })} />}>
20
- <Mobile>{children}</Mobile>
20
+ <Mobile {...res}>{children}</Mobile>
21
21
  </Suspense>
22
22
  ) : (
23
- <Desktop>{children}</Desktop>
23
+ <Desktop {...res}>{children}</Desktop>
24
24
  );
25
25
  };
26
26
 
@@ -1,16 +1,16 @@
1
- import { PropsWithChildren, ReactNode } from 'react';
1
+ import { ReactNode } from 'react';
2
2
 
3
3
  import { isMobileDevice } from '@/utils/responsive';
4
4
 
5
- interface ServerResponsiveLayoutProps {
6
- Desktop: (props: PropsWithChildren) => ReactNode;
7
- Mobile: (props: PropsWithChildren) => ReactNode;
5
+ interface ServerResponsiveLayoutProps extends Record<string, any> {
6
+ Desktop: (props: any) => ReactNode;
7
+ Mobile: (props: any) => ReactNode;
8
8
  children?: ReactNode;
9
9
  }
10
- const ResponsiveLayout = ({ children, Desktop, Mobile }: ServerResponsiveLayoutProps) => {
10
+ const ResponsiveLayout = ({ children, Desktop, Mobile, ...res }: ServerResponsiveLayoutProps) => {
11
11
  const mobile = isMobileDevice();
12
12
 
13
- return mobile ? <Mobile>{children}</Mobile> : <Desktop>{children}</Desktop>;
13
+ return mobile ? <Mobile {...res}>{children}</Mobile> : <Desktop {...res}>{children}</Desktop>;
14
14
  };
15
15
 
16
16
  export default ResponsiveLayout;
@@ -0,0 +1,132 @@
1
+ import { act } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { ModelProvider } from '@/libs/agent-runtime';
5
+ import { useGlobalStore } from '@/store/global';
6
+ import { GlobalLLMConfig, GlobalLLMProviderKey } from '@/types/settings';
7
+
8
+ import { getProviderAuthPayload } from './_auth';
9
+
10
+ // Mock data for different providers
11
+ const mockZhiPuAPIKey = 'zhipu-api-key';
12
+ const mockMoonshotAPIKey = 'moonshot-api-key';
13
+ const mockGoogleAPIKey = 'google-api-key';
14
+
15
+ // mock the traditional zustand
16
+ vi.mock('zustand/traditional');
17
+
18
+ const setModelProviderConfig = <T extends GlobalLLMProviderKey>(
19
+ provider: T,
20
+ config: Partial<GlobalLLMConfig[T]>,
21
+ ) => {
22
+ useGlobalStore.setState({
23
+ settings: { languageModel: { [provider]: config } },
24
+ });
25
+ };
26
+
27
+ describe('getProviderAuthPayload', () => {
28
+ it('should return correct payload for ZhiPu provider', () => {
29
+ act(() => {
30
+ setModelProviderConfig('zhipu', { apiKey: mockZhiPuAPIKey });
31
+ });
32
+
33
+ const payload = getProviderAuthPayload(ModelProvider.ZhiPu);
34
+ expect(payload).toEqual({ apiKey: mockZhiPuAPIKey });
35
+ });
36
+
37
+ it('should return correct payload for Moonshot provider', () => {
38
+ act(() => {
39
+ setModelProviderConfig('moonshot', { apiKey: mockMoonshotAPIKey });
40
+ });
41
+
42
+ const payload = getProviderAuthPayload(ModelProvider.Moonshot);
43
+ expect(payload).toEqual({ apiKey: mockMoonshotAPIKey });
44
+ });
45
+
46
+ it('should return correct payload for Google provider', () => {
47
+ act(() => {
48
+ setModelProviderConfig('google', { apiKey: mockGoogleAPIKey });
49
+ });
50
+
51
+ const payload = getProviderAuthPayload(ModelProvider.Google);
52
+ expect(payload).toEqual({ apiKey: mockGoogleAPIKey });
53
+ });
54
+
55
+ it('should return correct payload for Bedrock provider', () => {
56
+ // 假设的 Bedrock 配置
57
+ const mockBedrockConfig = {
58
+ accessKeyId: 'bedrock-access-key-id',
59
+ region: 'bedrock-region',
60
+ secretAccessKey: 'bedrock-secret-access-key',
61
+ };
62
+ act(() => {
63
+ setModelProviderConfig('bedrock', mockBedrockConfig);
64
+ });
65
+
66
+ const payload = getProviderAuthPayload(ModelProvider.Bedrock);
67
+ expect(payload).toEqual({
68
+ apiKey: mockBedrockConfig.secretAccessKey + mockBedrockConfig.accessKeyId,
69
+ awsAccessKeyId: mockBedrockConfig.accessKeyId,
70
+ awsRegion: mockBedrockConfig.region,
71
+ awsSecretAccessKey: mockBedrockConfig.secretAccessKey,
72
+ });
73
+ });
74
+
75
+ it('should return correct payload for Azure provider', () => {
76
+ // 假设的 Azure 配置
77
+ const mockAzureConfig = {
78
+ apiKey: 'azure-api-key',
79
+ apiVersion: 'azure-api-version',
80
+ endpoint: 'azure-endpoint',
81
+ };
82
+ act(() => {
83
+ setModelProviderConfig('azure', mockAzureConfig);
84
+ });
85
+
86
+ const payload = getProviderAuthPayload(ModelProvider.Azure);
87
+ expect(payload).toEqual({
88
+ apiKey: mockAzureConfig.apiKey,
89
+ azureApiVersion: mockAzureConfig.apiVersion,
90
+ endpoint: mockAzureConfig.endpoint,
91
+ });
92
+ });
93
+
94
+ it('should return correct payload for Ollama provider', () => {
95
+ // 假设的 Ollama 配置
96
+ const mockOllamaProxyUrl = 'ollama-proxy-url';
97
+ act(() => {
98
+ setModelProviderConfig('ollama', { endpoint: mockOllamaProxyUrl });
99
+ });
100
+
101
+ const payload = getProviderAuthPayload(ModelProvider.Ollama);
102
+ expect(payload).toEqual({
103
+ endpoint: mockOllamaProxyUrl,
104
+ });
105
+ });
106
+
107
+ it('should return correct payload for OpenAI provider', () => {
108
+ // 假设的 OpenAI 配置
109
+ const mockOpenAIConfig = {
110
+ OPENAI_API_KEY: 'openai-api-key',
111
+ endpoint: 'openai-endpoint',
112
+ useAzure: true,
113
+ azureApiVersion: 'openai-azure-api-version',
114
+ };
115
+ act(() => {
116
+ setModelProviderConfig('openAI', mockOpenAIConfig);
117
+ });
118
+
119
+ const payload = getProviderAuthPayload(ModelProvider.OpenAI);
120
+ expect(payload).toEqual({
121
+ apiKey: mockOpenAIConfig.OPENAI_API_KEY,
122
+ azureApiVersion: mockOpenAIConfig.azureApiVersion,
123
+ endpoint: mockOpenAIConfig.endpoint,
124
+ useAzure: mockOpenAIConfig.useAzure,
125
+ });
126
+ });
127
+
128
+ it('should return an empty object or throw an error for an unknown provider', () => {
129
+ const payload = getProviderAuthPayload('UnknownProvider');
130
+ expect(payload).toEqual({ apiKey: '', endpoint: '' });
131
+ });
132
+ });
@@ -4,7 +4,7 @@ import { useGlobalStore } from '@/store/global';
4
4
  import { modelProviderSelectors, settingsSelectors } from '@/store/global/selectors';
5
5
  import { createJWT } from '@/utils/jwt';
6
6
 
7
- const getProviderAuthPayload = (provider: string) => {
7
+ export const getProviderAuthPayload = (provider: string) => {
8
8
  switch (provider) {
9
9
  case ModelProvider.ZhiPu: {
10
10
  return { apiKey: modelProviderSelectors.zhipuAPIKey(useGlobalStore.getState()) };
@@ -23,12 +23,10 @@ export interface GlobalCommonState {
23
23
  isMobile?: boolean;
24
24
  latestVersion?: string;
25
25
  router?: AppRouterInstance;
26
- settingsTab: SettingsTabs;
27
26
  sidebarKey: SidebarTabKey;
28
27
  }
29
28
 
30
29
  export const initialCommonState: GlobalCommonState = {
31
30
  isMobile: false,
32
- settingsTab: SettingsTabs.Common,
33
31
  sidebarKey: SidebarTabKey.Chat,
34
32
  };
@@ -6,7 +6,6 @@ import { withSWR } from '~test-utils';
6
6
  import { DEFAULT_AGENT, DEFAULT_SETTINGS } from '@/const/settings';
7
7
  import { userService } from '@/services/user';
8
8
  import { useGlobalStore } from '@/store/global';
9
- import { SettingsTabs } from '@/store/global/initialState';
10
9
  import { LobeAgentSettings } from '@/types/session';
11
10
  import { GlobalSettings, OpenAIConfig } from '@/types/settings';
12
11
 
@@ -100,18 +99,6 @@ describe('SettingsAction', () => {
100
99
  });
101
100
  });
102
101
 
103
- describe('switchSettingTabs', () => {
104
- it('should switch settings tabs', () => {
105
- const { result } = renderHook(() => useGlobalStore());
106
-
107
- act(() => {
108
- result.current.switchSettingTabs(SettingsTabs.Agent);
109
- });
110
-
111
- expect(result.current.settingsTab).toEqual(SettingsTabs.Agent);
112
- });
113
- });
114
-
115
102
  describe('switchThemeMode', () => {
116
103
  it('should switch theme mode', async () => {
117
104
  const { result } = renderHook(() => useGlobalStore());
@@ -5,7 +5,6 @@ import type { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { userService } from '@/services/user';
7
7
  import type { GlobalStore } from '@/store/global';
8
- import { SettingsTabs } from '@/store/global/initialState';
9
8
  import { LobeAgentSettings } from '@/types/session';
10
9
  import { GlobalLLMConfig, GlobalLLMProviderKey, GlobalSettings } from '@/types/settings';
11
10
  import { difference } from '@/utils/difference';
@@ -22,7 +21,6 @@ export interface SettingsAction {
22
21
  config: Partial<GlobalLLMConfig[T]>,
23
22
  ) => Promise<void>;
24
23
  setSettings: (settings: DeepPartial<GlobalSettings>) => Promise<void>;
25
- switchSettingTabs: (tab: SettingsTabs) => void;
26
24
  switchThemeMode: (themeMode: ThemeMode) => Promise<void>;
27
25
  toggleProviderEnabled: (provider: GlobalLLMProviderKey, enabled: boolean) => Promise<void>;
28
26
  updateDefaultAgent: (agent: DeepPartial<LobeAgentSettings>) => Promise<void>;
@@ -61,9 +59,6 @@ export const createSettingsSlice: StateCreator<
61
59
  await get().refreshUserConfig();
62
60
  },
63
61
 
64
- switchSettingTabs: (tab) => {
65
- set({ settingsTab: tab });
66
- },
67
62
  switchThemeMode: async (themeMode) => {
68
63
  await get().setSettings({ themeMode });
69
64
  },
@@ -10,12 +10,16 @@ import {
10
10
  ZhiPuProvider,
11
11
  } from '@/config/modelProviders';
12
12
  import { ChatModelCard, ModelProviderCard } from '@/types/llm';
13
+ import { GlobalLLMProviderKey } from '@/types/settings';
13
14
  import { parseModelString } from '@/utils/parseModels';
14
15
 
15
16
  import { GlobalStore } from '../../../store';
16
17
  import { currentSettings } from './settings';
17
18
 
18
19
  const modelProvider = (s: GlobalStore) => currentSettings(s).languageModel;
20
+ const providerEnabled = (provider: GlobalLLMProviderKey) => (s: GlobalStore) =>
21
+ currentSettings(s).languageModel[provider]?.enabled || false;
22
+
19
23
  const openAIConfig = (s: GlobalStore) => modelProvider(s).openAI;
20
24
 
21
25
  const openAIAPIKey = (s: GlobalStore) => openAIConfig(s).OPENAI_API_KEY;
@@ -169,6 +173,9 @@ export const modelProviderSelectors = {
169
173
  modelEnabledFiles,
170
174
  modelEnabledUpload,
171
175
 
176
+ modelProviderConfig: modelProvider,
177
+ providerEnabled,
178
+
172
179
  // OpenAI
173
180
  openAIConfig,
174
181
  openAIAPIKey,
@@ -7,6 +7,7 @@ export interface OpenAIConfig {
7
7
  * custom mode name for fine-tuning or openai like model
8
8
  */
9
9
  customModelName?: string;
10
+ enabled: boolean;
10
11
  endpoint?: string;
11
12
  /**
12
13
  * @deprecated
@@ -1,21 +0,0 @@
1
- 'use client';
2
-
3
- import { memo } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
-
6
- import PageTitle from '@/components/PageTitle';
7
- import { useSwitchSideBarOnInit } from '@/store/global/hooks/useSwitchSettingsOnInit';
8
- import { SettingsTabs } from '@/store/global/initialState';
9
-
10
- import Agent from './Agent';
11
-
12
- export default memo(() => {
13
- useSwitchSideBarOnInit(SettingsTabs.Agent);
14
- const { t } = useTranslation('setting');
15
- return (
16
- <>
17
- <PageTitle title={t('tab.agent')} />
18
- <Agent />
19
- </>
20
- );
21
- });
@@ -1,44 +0,0 @@
1
- import { Icon, List } from '@lobehub/ui';
2
- import { createStyles, useResponsive } from 'antd-style';
3
- import { ChevronRight, type LucideIcon } from 'lucide-react';
4
- import { CSSProperties, ReactNode, memo } from 'react';
5
-
6
- const { Item } = List;
7
-
8
- const useStyles = createStyles(({ css, token, responsive }) => ({
9
- container: css`
10
- position: relative;
11
- padding-top: 20px;
12
- padding-bottom: 20px;
13
- border-radius: ${token.borderRadius}px;
14
- ${responsive.mobile} {
15
- border-radius: 0;
16
- }
17
- `,
18
- }));
19
-
20
- export interface ItemProps {
21
- active?: boolean;
22
- className?: string;
23
- icon: LucideIcon;
24
- label: ReactNode;
25
- style?: CSSProperties;
26
- }
27
-
28
- const SettingItem = memo<ItemProps>(({ label, icon, active = false, style, className }) => {
29
- const { cx, styles } = useStyles();
30
- const { mobile } = useResponsive();
31
- return (
32
- <Item
33
- active={active}
34
- avatar={<Icon icon={icon} size={{ fontSize: 16 }} />}
35
- className={cx(styles.container, className)}
36
- style={style}
37
- title={label as string}
38
- >
39
- {mobile && <Icon icon={ChevronRight} size={{ fontSize: 16 }} />}
40
- </Item>
41
- );
42
- });
43
-
44
- export default SettingItem;
@@ -1,42 +0,0 @@
1
- import { DraggablePanelBody, Logo } from '@lobehub/ui';
2
- import { createStyles } from 'antd-style';
3
- import { memo } from 'react';
4
- import { Flexbox } from 'react-layout-kit';
5
-
6
- import FolderPanel from '@/features/FolderPanel';
7
-
8
- import UpgradeAlert from '../UpgradeAlert';
9
- import List from './List';
10
-
11
- const useStyles = createStyles(({ stylish, token, css }) => ({
12
- body: stylish.noScrollbar,
13
- logo: css`
14
- fill: ${token.colorText};
15
- `,
16
- top: css`
17
- position: sticky;
18
- top: 0;
19
- `,
20
- }));
21
-
22
- const SideBar = memo(() => {
23
- const { styles } = useStyles();
24
-
25
- return (
26
- <FolderPanel>
27
- <DraggablePanelBody className={styles.body} style={{ padding: 0 }}>
28
- <Flexbox className={styles.top} padding={16}>
29
- <div>
30
- <Logo className={styles.logo} extra={'Settings'} size={36} type={'text'} />
31
- </div>
32
- </Flexbox>
33
- <Flexbox gap={2} style={{ paddingInline: 8 }}>
34
- <UpgradeAlert />
35
- <List />
36
- </Flexbox>
37
- </DraggablePanelBody>
38
- </FolderPanel>
39
- );
40
- });
41
-
42
- export default SideBar;
@@ -1,21 +0,0 @@
1
- 'use client';
2
-
3
- import { memo } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
-
6
- import PageTitle from '@/components/PageTitle';
7
- import { useSwitchSideBarOnInit } from '@/store/global/hooks/useSwitchSettingsOnInit';
8
- import { SettingsTabs } from '@/store/global/initialState';
9
-
10
- import TTS from './TTS';
11
-
12
- export default memo(() => {
13
- useSwitchSideBarOnInit(SettingsTabs.TTS);
14
- const { t } = useTranslation('setting');
15
- return (
16
- <>
17
- <PageTitle title={t('tab.llm')} />
18
- <TTS />
19
- </>
20
- );
21
- });
@@ -1,12 +0,0 @@
1
- import { useOnFinishHydrationGlobal } from '@/store/global';
2
-
3
- import { SettingsTabs } from '../initialState';
4
-
5
- /**
6
- * 切换设置侧边栏选项
7
- */
8
- export const useSwitchSideBarOnInit = (key: SettingsTabs) => {
9
- useOnFinishHydrationGlobal((store) => {
10
- store.switchSettingTabs(key);
11
- });
12
- };