@lobehub/chat 0.149.4 → 0.149.5

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 (80) hide show
  1. package/.github/FUNDING.yml +1 -1
  2. package/CHANGELOG.md +33 -0
  3. package/package.json +1 -1
  4. package/src/app/chat/(desktop)/features/ChatHeader/Main.tsx +5 -5
  5. package/src/app/chat/(desktop)/features/ChatHeader/Tags.tsx +3 -3
  6. package/src/app/chat/(desktop)/features/ChatInput/Footer/DragUpload.tsx +9 -9
  7. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +3 -3
  8. package/src/app/chat/(desktop)/features/SideBar/SystemRole/index.tsx +8 -3
  9. package/src/app/chat/(mobile)/mobile/ChatHeader/ChatHeaderTitle.tsx +2 -2
  10. package/src/app/chat/(mobile)/mobile/page.tsx +0 -6
  11. package/src/app/chat/_layout/Desktop/SessionList.tsx +2 -0
  12. package/src/app/chat/features/PageTitle/index.tsx +3 -3
  13. package/src/app/chat/features/PluginTag/PluginStatus.tsx +2 -2
  14. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +4 -2
  15. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +10 -17
  16. package/src/app/chat/features/SessionListContent/index.tsx +2 -0
  17. package/src/app/chat/features/ShareButton/Preview.tsx +15 -11
  18. package/src/app/chat/features/ShareButton/useScreenshot.ts +2 -2
  19. package/src/app/chat/settings/features/EditPage.tsx +10 -7
  20. package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +5 -3
  21. package/src/app/metadata.ts +3 -3
  22. package/src/app/settings/(mobile)/features/AvatarBanner.tsx +1 -0
  23. package/src/config/modelProviders/ollama.ts +11 -12
  24. package/src/const/session.ts +1 -0
  25. package/src/database/client/models/session.ts +1 -0
  26. package/src/database/client/models/user.ts +6 -0
  27. package/src/features/ChatInput/ActionBar/FileUpload.tsx +11 -5
  28. package/src/features/ChatInput/ActionBar/History.tsx +3 -3
  29. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -0
  30. package/src/features/ChatInput/ActionBar/Temperature.tsx +3 -3
  31. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +4 -4
  32. package/src/features/ChatInput/ActionBar/Token/index.tsx +3 -3
  33. package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +3 -3
  34. package/src/features/ChatInput/ActionBar/Tools/index.tsx +4 -4
  35. package/src/features/ChatInput/STT/browser.tsx +3 -3
  36. package/src/features/ChatInput/STT/openai.tsx +3 -3
  37. package/src/features/ChatInput/useChatInput.ts +3 -3
  38. package/src/features/Conversation/Extras/Assistant.test.tsx +7 -7
  39. package/src/features/Conversation/Extras/Assistant.tsx +3 -3
  40. package/src/features/Conversation/Extras/TTS/index.tsx +3 -3
  41. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +2 -2
  42. package/src/features/Conversation/components/ChatItem/index.tsx +6 -4
  43. package/src/features/Conversation/hooks/useInitConversation.ts +10 -7
  44. package/src/features/Conversation/index.tsx +6 -3
  45. package/src/features/ModelSwitchPanel/index.tsx +6 -4
  46. package/src/hooks/useTTS.ts +4 -4
  47. package/src/services/chat.ts +3 -3
  48. package/src/services/session/client.ts +19 -0
  49. package/src/services/session/type.ts +2 -0
  50. package/src/store/agent/index.ts +2 -0
  51. package/src/store/agent/initialState.ts +7 -0
  52. package/src/store/agent/selectors.ts +1 -0
  53. package/src/store/{session/slices/agent → agent/slices/chat}/action.test.ts +26 -63
  54. package/src/store/agent/slices/chat/action.ts +107 -0
  55. package/src/store/agent/slices/chat/initialState.ts +14 -0
  56. package/src/store/agent/slices/chat/selectors.test.ts +82 -0
  57. package/src/store/agent/slices/chat/selectors.ts +81 -0
  58. package/src/store/agent/store.ts +27 -0
  59. package/src/store/chat/slices/message/action.test.ts +3 -2
  60. package/src/store/chat/slices/message/action.ts +3 -3
  61. package/src/store/chat/slices/message/selectors.test.ts +9 -2
  62. package/src/store/chat/slices/message/selectors.ts +6 -4
  63. package/src/store/chat/slices/share/action.ts +5 -3
  64. package/src/store/global/slices/preference/selectors.ts +3 -1
  65. package/src/store/session/selectors.ts +1 -2
  66. package/src/store/session/slices/session/action.test.ts +43 -0
  67. package/src/store/session/slices/session/action.ts +28 -18
  68. package/src/store/session/slices/session/helpers.ts +2 -3
  69. package/src/store/session/slices/session/initialState.ts +1 -17
  70. package/src/store/session/slices/session/selectors/index.ts +1 -0
  71. package/src/store/session/slices/session/selectors/list.test.ts +5 -3
  72. package/src/store/session/slices/session/selectors/list.ts +2 -3
  73. package/src/store/session/slices/session/selectors/meta.test.ts +108 -0
  74. package/src/store/session/slices/session/selectors/meta.ts +45 -0
  75. package/src/store/session/store.ts +1 -7
  76. package/src/types/session.ts +1 -0
  77. package/src/store/session/slices/agent/action.ts +0 -84
  78. package/src/store/session/slices/agent/selectors.test.ts +0 -180
  79. package/src/store/session/slices/agent/selectors.ts +0 -129
  80. /package/src/store/{session/slices/agent → agent/slices/chat}/index.ts +0 -0
@@ -3,14 +3,14 @@ import * as immer from 'immer';
3
3
  import { describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { sessionService } from '@/services/session';
6
+ import { useAgentStore } from '@/store/agent';
7
+ import { agentSelectors } from '@/store/agent/selectors';
6
8
  import { useGlobalStore } from '@/store/global';
7
- import { useSessionStore } from '@/store/session';
8
- import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
9
9
 
10
10
  describe('AgentSlice', () => {
11
11
  describe('removePlugin', () => {
12
12
  it('should call togglePlugin with the provided id and false', async () => {
13
- const { result } = renderHook(() => useSessionStore());
13
+ const { result } = renderHook(() => useAgentStore());
14
14
  const pluginId = 'plugin-id';
15
15
  const togglePluginMock = vi.spyOn(result.current, 'togglePlugin');
16
16
 
@@ -25,7 +25,7 @@ describe('AgentSlice', () => {
25
25
 
26
26
  describe('togglePlugin', () => {
27
27
  it('should add plugin id to plugins array if not present and open is true or undefined', async () => {
28
- const { result } = renderHook(() => useSessionStore());
28
+ const { result } = renderHook(() => useAgentStore());
29
29
  const pluginId = 'plugin-id';
30
30
  const updateAgentConfigMock = vi.spyOn(result.current, 'updateAgentConfig');
31
31
 
@@ -43,7 +43,7 @@ describe('AgentSlice', () => {
43
43
  });
44
44
 
45
45
  it('should remove plugin id from plugins array if present and open is false', async () => {
46
- const { result } = renderHook(() => useSessionStore());
46
+ const { result } = renderHook(() => useAgentStore());
47
47
  const pluginId = 'plugin-id';
48
48
  const updateAgentConfigMock = vi.spyOn(result.current, 'updateAgentConfig');
49
49
 
@@ -61,7 +61,7 @@ describe('AgentSlice', () => {
61
61
  });
62
62
 
63
63
  it('should not modify plugins array if plugin id is not present and open is false', async () => {
64
- const { result } = renderHook(() => useSessionStore());
64
+ const { result } = renderHook(() => useAgentStore());
65
65
  const pluginId = 'plugin-id';
66
66
  const updateAgentConfigMock = vi.spyOn(result.current, 'updateAgentConfig');
67
67
 
@@ -79,49 +79,53 @@ describe('AgentSlice', () => {
79
79
 
80
80
  describe('updateAgentConfig', () => {
81
81
  it('should update global config if current session is inbox session', async () => {
82
- const { result } = renderHook(() => useSessionStore());
82
+ const { result } = renderHook(() => useAgentStore());
83
83
  const config = { model: 'gpt-3.5-turbo' };
84
- const updateDefaultAgentMock = vi.spyOn(useGlobalStore.getState(), 'updateDefaultAgent');
85
-
86
- // 模拟当前会话是收件箱会话
87
- vi.spyOn(sessionSelectors, 'isInboxSession').mockReturnValue(true);
84
+ const updateSessionConfigMock = vi.spyOn(sessionService, 'updateSessionConfig');
85
+ const refreshMock = vi.spyOn(result.current, 'internal_refreshAgentConfig');
88
86
 
89
87
  await act(async () => {
90
88
  await result.current.updateAgentConfig(config);
91
89
  });
92
90
 
93
- expect(updateDefaultAgentMock).toHaveBeenCalledWith({ config });
94
- updateDefaultAgentMock.mockRestore();
91
+ expect(updateSessionConfigMock).toHaveBeenCalledWith('inbox', config);
92
+ expect(refreshMock).toHaveBeenCalled();
93
+ updateSessionConfigMock.mockRestore();
94
+ refreshMock.mockRestore();
95
95
  });
96
96
 
97
97
  it('should update session config if current session is not inbox session', async () => {
98
- const { result } = renderHook(() => useSessionStore());
98
+ const { result } = renderHook(() => useAgentStore());
99
99
  const config = { model: 'gpt-3.5-turbo' };
100
100
  const updateSessionConfigMock = vi.spyOn(sessionService, 'updateSessionConfig');
101
- const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
101
+ const refreshMock = vi.spyOn(result.current, 'internal_refreshAgentConfig');
102
102
 
103
103
  // 模拟当前会话不是收件箱会话
104
- vi.spyOn(sessionSelectors, 'isInboxSession').mockReturnValue(false);
105
- vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue({ id: 'session-id' } as any);
106
- vi.spyOn(result.current, 'activeId', 'get').mockReturnValue('session-id');
104
+ act(() => {
105
+ useAgentStore.setState({
106
+ activeId: 'session-id',
107
+ });
108
+ });
107
109
 
108
110
  await act(async () => {
109
111
  await result.current.updateAgentConfig(config);
110
112
  });
111
113
 
112
114
  expect(updateSessionConfigMock).toHaveBeenCalledWith('session-id', config);
113
- expect(refreshSessionsMock).toHaveBeenCalled();
115
+ expect(refreshMock).toHaveBeenCalled();
114
116
  updateSessionConfigMock.mockRestore();
115
- refreshSessionsMock.mockRestore();
117
+ refreshMock.mockRestore();
116
118
  });
117
119
 
118
120
  it('should not update config if there is no current session', async () => {
119
- const { result } = renderHook(() => useSessionStore());
121
+ const { result } = renderHook(() => useAgentStore());
120
122
  const config = { model: 'gpt-3.5-turbo' };
121
123
  const updateSessionConfigMock = vi.spyOn(sessionService, 'updateSessionConfig');
122
124
 
123
125
  // 模拟没有当前会话
124
- vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue(null as any);
126
+ act(() => {
127
+ useAgentStore.setState({ activeId: null as any });
128
+ });
125
129
 
126
130
  await act(async () => {
127
131
  await result.current.updateAgentConfig(config);
@@ -131,45 +135,4 @@ describe('AgentSlice', () => {
131
135
  updateSessionConfigMock.mockRestore();
132
136
  });
133
137
  });
134
-
135
- describe('updateAgentMeta', () => {
136
- it('should not update meta if there is no current session', async () => {
137
- const { result } = renderHook(() => useSessionStore());
138
- const meta = { title: 'Test Agent' };
139
- const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
140
- const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
141
-
142
- // 模拟没有当前会话
143
- vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue(null as any);
144
-
145
- await act(async () => {
146
- await result.current.updateAgentMeta(meta as any);
147
- });
148
-
149
- expect(updateSessionMock).not.toHaveBeenCalled();
150
- expect(refreshSessionsMock).not.toHaveBeenCalled();
151
- updateSessionMock.mockRestore();
152
- refreshSessionsMock.mockRestore();
153
- });
154
-
155
- it('should update session meta and refresh sessions', async () => {
156
- const { result } = renderHook(() => useSessionStore());
157
- const meta = { title: 'Test Agent' };
158
- const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
159
- const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
160
-
161
- // 模拟有当前会话
162
- vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue({ id: 'session-id' } as any);
163
- vi.spyOn(result.current, 'activeId', 'get').mockReturnValue('session-id');
164
-
165
- await act(async () => {
166
- await result.current.updateAgentMeta(meta);
167
- });
168
-
169
- expect(updateSessionMock).toHaveBeenCalledWith('session-id', { meta });
170
- expect(refreshSessionsMock).toHaveBeenCalled();
171
- updateSessionMock.mockRestore();
172
- refreshSessionsMock.mockRestore();
173
- });
174
- });
175
138
  });
@@ -0,0 +1,107 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import { produce } from 'immer';
3
+ import { SWRResponse, mutate } from 'swr';
4
+ import { DeepPartial } from 'utility-types';
5
+ import { StateCreator } from 'zustand/vanilla';
6
+
7
+ import { useClientDataSWR } from '@/libs/swr';
8
+ import { sessionService } from '@/services/session';
9
+ import { useSessionStore } from '@/store/session';
10
+ import { LobeAgentConfig } from '@/types/agent';
11
+ import { merge } from '@/utils/merge';
12
+
13
+ import { AgentStore } from '../../store';
14
+ import { agentSelectors } from './selectors';
15
+
16
+ /**
17
+ * 助手接口
18
+ */
19
+ export interface AgentChatAction {
20
+ removePlugin: (id: string) => void;
21
+ togglePlugin: (id: string, open?: boolean) => Promise<void>;
22
+ updateAgentConfig: (config: Partial<LobeAgentConfig>) => Promise<void>;
23
+
24
+ useFetchAgentConfig: (id: string) => SWRResponse<LobeAgentConfig>;
25
+
26
+ /* eslint-disable typescript-sort-keys/interface */
27
+
28
+ internal_updateAgentConfig: (id: string, data: DeepPartial<LobeAgentConfig>) => Promise<void>;
29
+ internal_refreshAgentConfig: (id: string) => Promise<void>;
30
+ /* eslint-enable */
31
+ }
32
+
33
+ const FETCH_AGENT_CONFIG_KEY = 'FETCH_AGENT_CONFIG';
34
+
35
+ export const createChatSlice: StateCreator<
36
+ AgentStore,
37
+ [['zustand/devtools', never]],
38
+ [],
39
+ AgentChatAction
40
+ > = (set, get) => ({
41
+ removePlugin: async (id) => {
42
+ await get().togglePlugin(id, false);
43
+ },
44
+
45
+ togglePlugin: async (id, open) => {
46
+ const originConfig = agentSelectors.currentAgentConfig(get());
47
+
48
+ const config = produce(originConfig, (draft) => {
49
+ draft.plugins = produce(draft.plugins || [], (plugins) => {
50
+ const index = plugins.indexOf(id);
51
+ const shouldOpen = open !== undefined ? open : index === -1;
52
+
53
+ if (shouldOpen) {
54
+ // 如果 open 为 true 或者 id 不存在于 plugins 中,则添加它
55
+ if (index === -1) {
56
+ plugins.push(id);
57
+ }
58
+ } else {
59
+ // 如果 open 为 false 或者 id 存在于 plugins 中,则移除它
60
+ if (index !== -1) {
61
+ plugins.splice(index, 1);
62
+ }
63
+ }
64
+ });
65
+ });
66
+
67
+ await get().updateAgentConfig(config);
68
+ },
69
+ updateAgentConfig: async (config) => {
70
+ const { activeId } = get();
71
+
72
+ if (!activeId) return;
73
+
74
+ await get().internal_updateAgentConfig(activeId, config);
75
+ },
76
+
77
+ useFetchAgentConfig: (sessionId) =>
78
+ useClientDataSWR<LobeAgentConfig>(
79
+ [FETCH_AGENT_CONFIG_KEY, sessionId],
80
+ ([, id]: string[]) => sessionService.getSessionConfig(id),
81
+ {
82
+ onSuccess: (data) => {
83
+ if (get().isAgentConfigInit && isEqual(get().agentConfig, data)) return;
84
+
85
+ set({ agentConfig: data, isAgentConfigInit: true }, false, 'fetchAgentConfig');
86
+ },
87
+ },
88
+ ),
89
+
90
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
91
+
92
+ internal_updateAgentConfig: async (id, data) => {
93
+ const prevModel = agentSelectors.currentAgentModel(get());
94
+ // optimistic update at frontend
95
+ set({ agentConfig: merge(get().agentConfig, data) }, false, 'optimistic_updateAgentConfig');
96
+
97
+ await sessionService.updateSessionConfig(id, data);
98
+ await get().internal_refreshAgentConfig(id);
99
+
100
+ // refresh sessions to update the agent config if the model has changed
101
+ if (prevModel !== data.model) await useSessionStore.getState().refreshSessions();
102
+ },
103
+
104
+ internal_refreshAgentConfig: async (id) => {
105
+ await mutate([FETCH_AGENT_CONFIG_KEY, id]);
106
+ },
107
+ });
@@ -0,0 +1,14 @@
1
+ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
2
+ import { LobeAgentConfig } from '@/types/agent';
3
+
4
+ export interface AgentState {
5
+ activeId: string;
6
+ agentConfig: LobeAgentConfig;
7
+ isAgentConfigInit: boolean;
8
+ }
9
+
10
+ export const initialSessionState: AgentState = {
11
+ activeId: 'inbox',
12
+ agentConfig: DEFAULT_AGENT_CONFIG,
13
+ isAgentConfigInit: false,
14
+ };
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { DEFAULT_AGENT_CONFIG, DEFAUTT_AGENT_TTS_CONFIG } from '@/const/settings';
4
+ import { AgentStore } from '@/store/agent';
5
+
6
+ import { agentSelectors } from './selectors';
7
+
8
+ vi.mock('i18next', () => ({
9
+ t: vi.fn((key) => key), // Simplified mock return value
10
+ }));
11
+
12
+ const mockSessionStore = {
13
+ activeId: '1',
14
+ agentConfig: DEFAULT_AGENT_CONFIG,
15
+ } as AgentStore;
16
+
17
+ describe('agentSelectors', () => {
18
+ describe('currentAgentConfig', () => {
19
+ it('should return the merged default and session-specific agent config', () => {
20
+ const config = agentSelectors.currentAgentConfig(mockSessionStore);
21
+ expect(config).toEqual(expect.objectContaining(mockSessionStore.agentConfig));
22
+ });
23
+ });
24
+
25
+ describe('currentAgentModel', () => {
26
+ it('should return the model from the agent config', () => {
27
+ const model = agentSelectors.currentAgentModel(mockSessionStore);
28
+ expect(model).toBe(mockSessionStore.agentConfig.model);
29
+ });
30
+ });
31
+
32
+ describe('hasSystemRole', () => {
33
+ it('should return true if the system role is defined in the agent config', () => {
34
+ const hasRole = agentSelectors.hasSystemRole(mockSessionStore);
35
+ expect(hasRole).toBe(false);
36
+ });
37
+
38
+ it('should return false if the system role is not defined in the agent config', () => {
39
+ const modifiedSessionStore = {
40
+ ...mockSessionStore,
41
+ agentConfig: {
42
+ ...mockSessionStore.agentConfig,
43
+ systemRole: 'test',
44
+ },
45
+ };
46
+ const hasRole = agentSelectors.hasSystemRole(modifiedSessionStore);
47
+ expect(hasRole).toBe(true);
48
+ });
49
+ });
50
+
51
+ describe('currentAgentTTS', () => {
52
+ it('should return the TTS config from the agent config', () => {
53
+ const ttsConfig = agentSelectors.currentAgentTTS(mockSessionStore);
54
+ expect(ttsConfig).toEqual(mockSessionStore.agentConfig.tts);
55
+ });
56
+
57
+ it('should return the default TTS config if none is defined in the agent config', () => {
58
+ const modifiedSessionStore = {
59
+ ...mockSessionStore,
60
+ sessions: [
61
+ {
62
+ ...mockSessionStore.agentConfig,
63
+ config: {
64
+ ...mockSessionStore.agentConfig,
65
+ tts: DEFAUTT_AGENT_TTS_CONFIG,
66
+ },
67
+ },
68
+ ],
69
+ };
70
+ const ttsConfig = agentSelectors.currentAgentTTS(modifiedSessionStore);
71
+ expect(ttsConfig).toEqual(DEFAUTT_AGENT_TTS_CONFIG);
72
+ });
73
+ });
74
+
75
+ describe('currentAgentTTSVoice', () => {
76
+ it('should return the appropriate TTS voice based on the service and language', () => {
77
+ const lang = 'en';
78
+ const ttsVoice = agentSelectors.currentAgentTTSVoice(lang)(mockSessionStore);
79
+ expect(ttsVoice).toBe(mockSessionStore.agentConfig.tts.voice.openai);
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,81 @@
1
+ import { VoiceList } from '@lobehub/tts';
2
+
3
+ import { INBOX_SESSION_ID } from '@/const/session';
4
+ import { DEFAULT_AGENT_CONFIG, DEFAUTT_AGENT_TTS_CONFIG } from '@/const/settings';
5
+ import { AgentStore } from '@/store/agent';
6
+ import { LobeAgentTTSConfig } from '@/types/agent';
7
+ import { merge } from '@/utils/merge';
8
+
9
+ const isInboxSession = (s: AgentStore) => s.activeId === INBOX_SESSION_ID;
10
+
11
+ // ========== Config ============== //
12
+ const currentAgentConfig = (s: AgentStore) => merge(DEFAULT_AGENT_CONFIG, s.agentConfig);
13
+
14
+ const currentAgentSystemRole = (s: AgentStore) => {
15
+ return currentAgentConfig(s).systemRole;
16
+ };
17
+
18
+ const currentAgentModel = (s: AgentStore): string => {
19
+ const config = currentAgentConfig(s);
20
+
21
+ return config?.model || 'gpt-3.5-turbo';
22
+ };
23
+
24
+ const currentAgentModelProvider = (s: AgentStore) => {
25
+ const config = currentAgentConfig(s);
26
+
27
+ return config?.provider;
28
+ };
29
+
30
+ const currentAgentPlugins = (s: AgentStore) => {
31
+ const config = currentAgentConfig(s);
32
+
33
+ return config?.plugins || [];
34
+ };
35
+
36
+ const currentAgentTTS = (s: AgentStore): LobeAgentTTSConfig => {
37
+ const config = currentAgentConfig(s);
38
+
39
+ return config?.tts || DEFAUTT_AGENT_TTS_CONFIG;
40
+ };
41
+
42
+ const currentAgentTTSVoice =
43
+ (lang: string) =>
44
+ (s: AgentStore): string => {
45
+ const { voice, ttsService } = currentAgentTTS(s);
46
+ const voiceList = new VoiceList(lang);
47
+ let currentVoice;
48
+ switch (ttsService) {
49
+ case 'openai': {
50
+ currentVoice = voice.openai || (VoiceList.openaiVoiceOptions?.[0].value as string);
51
+ break;
52
+ }
53
+ case 'edge': {
54
+ currentVoice = voice.edge || (voiceList.edgeVoiceOptions?.[0].value as string);
55
+ break;
56
+ }
57
+ case 'microsoft': {
58
+ currentVoice = voice.microsoft || (voiceList.microsoftVoiceOptions?.[0].value as string);
59
+ break;
60
+ }
61
+ }
62
+ return currentVoice || 'alloy';
63
+ };
64
+
65
+ const hasSystemRole = (s: AgentStore) => {
66
+ const config = currentAgentConfig(s);
67
+
68
+ return !!config.systemRole;
69
+ };
70
+
71
+ export const agentSelectors = {
72
+ currentAgentConfig,
73
+ currentAgentModel,
74
+ currentAgentModelProvider,
75
+ currentAgentPlugins,
76
+ currentAgentSystemRole,
77
+ currentAgentTTS,
78
+ currentAgentTTSVoice,
79
+ hasSystemRole,
80
+ isInboxSession,
81
+ };
@@ -0,0 +1,27 @@
1
+ import { devtools } from 'zustand/middleware';
2
+ import { shallow } from 'zustand/shallow';
3
+ import { createWithEqualityFn } from 'zustand/traditional';
4
+ import { StateCreator } from 'zustand/vanilla';
5
+
6
+ import { isDev } from '@/utils/env';
7
+
8
+ import { SessionStoreState, initialState } from './initialState';
9
+ import { AgentChatAction, createChatSlice } from './slices/chat/action';
10
+
11
+ // =============== aggregate createStoreFn ============ //
12
+
13
+ export interface AgentStore extends AgentChatAction, SessionStoreState {}
14
+
15
+ const createStore: StateCreator<AgentStore, [['zustand/devtools', never]]> = (...parameters) => ({
16
+ ...initialState,
17
+ ...createChatSlice(...parameters),
18
+ });
19
+
20
+ // =============== implement useStore ============ //
21
+
22
+ export const useAgentStore = createWithEqualityFn<AgentStore>()(
23
+ devtools(createStore, {
24
+ name: 'LobeChat_Agent' + (isDev ? '_DEV' : ''),
25
+ }),
26
+ shallow,
27
+ );
@@ -7,8 +7,9 @@ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
7
7
  import { chatService } from '@/services/chat';
8
8
  import { messageService } from '@/services/message';
9
9
  import { topicService } from '@/services/topic';
10
+ import { agentSelectors } from '@/store/agent/selectors';
10
11
  import { chatSelectors } from '@/store/chat/selectors';
11
- import { agentSelectors } from '@/store/session/selectors';
12
+ import { sessionMetaSelectors } from '@/store/session/selectors';
12
13
  import { ChatMessage } from '@/types/message';
13
14
 
14
15
  import { useChatStore } from '../../store';
@@ -69,7 +70,7 @@ beforeEach(() => {
69
70
  vi.clearAllMocks();
70
71
  useChatStore.setState(mockState, false);
71
72
  vi.spyOn(agentSelectors, 'currentAgentConfig').mockImplementation(() => DEFAULT_AGENT_CONFIG);
72
- vi.spyOn(agentSelectors, 'currentAgentMeta').mockImplementation(() => ({ tags: [] }));
73
+ vi.spyOn(sessionMetaSelectors, 'currentAgentMeta').mockImplementation(() => ({ tags: [] }));
73
74
  });
74
75
 
75
76
  afterEach(() => {
@@ -13,10 +13,10 @@ import { chatService } from '@/services/chat';
13
13
  import { CreateMessageParams, messageService } from '@/services/message';
14
14
  import { topicService } from '@/services/topic';
15
15
  import { traceService } from '@/services/trace';
16
+ import { useAgentStore } from '@/store/agent';
17
+ import { agentSelectors } from '@/store/agent/selectors';
16
18
  import { chatHelpers } from '@/store/chat/helpers';
17
19
  import { ChatStore } from '@/store/chat/store';
18
- import { useSessionStore } from '@/store/session';
19
- import { agentSelectors } from '@/store/session/selectors';
20
20
  import { ChatMessage } from '@/types/message';
21
21
  import { TraceEventPayloads } from '@/types/trace';
22
22
  import { setNamespace } from '@/utils/storeDebug';
@@ -120,7 +120,7 @@ export interface ChatMessageAction {
120
120
  internalTraceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
121
121
  }
122
122
 
123
- const getAgentConfig = () => agentSelectors.currentAgentConfig(useSessionStore.getState());
123
+ const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
124
124
 
125
125
  const preventLeavingFn = (e: BeforeUnloadEvent) => {
126
126
  // set returnValue to trigger alert modal
@@ -3,10 +3,12 @@ import { describe, expect, it } from 'vitest';
3
3
 
4
4
  import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
5
5
  import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { useAgentStore } from '@/store/agent';
6
7
  import { ChatStore } from '@/store/chat';
7
8
  import { initialState } from '@/store/chat/initialState';
8
9
  import { useGlobalStore } from '@/store/global';
9
10
  import { useSessionStore } from '@/store/session';
11
+ import { LobeAgentConfig } from '@/types/agent';
10
12
  import { ChatMessage } from '@/types/message';
11
13
  import { MetaData } from '@/types/meta';
12
14
  import { merge } from '@/utils/merge';
@@ -158,8 +160,13 @@ describe('chatSelectors', () => {
158
160
  it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
159
161
  const state = merge(initialStore, { messages: mockMessages });
160
162
  act(() => {
161
- useGlobalStore.setState({
162
- settings: { defaultAgent: { config: { historyCount: 2, enableHistoryCount: true } } },
163
+ useAgentStore.setState({
164
+ activeId: 'inbox',
165
+ agentConfig: {
166
+ historyCount: 2,
167
+ enableHistoryCount: true,
168
+ model: 'abc',
169
+ } as LobeAgentConfig,
163
170
  });
164
171
  });
165
172
 
@@ -3,10 +3,12 @@ import { t } from 'i18next';
3
3
 
4
4
  import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta';
5
5
  import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { useAgentStore } from '@/store/agent';
7
+ import { agentSelectors } from '@/store/agent/selectors';
6
8
  import { useGlobalStore } from '@/store/global';
7
9
  import { commonSelectors } from '@/store/global/selectors';
8
10
  import { useSessionStore } from '@/store/session';
9
- import { agentSelectors } from '@/store/session/selectors';
11
+ import { sessionMetaSelectors } from '@/store/session/selectors';
10
12
  import { ChatMessage } from '@/types/message';
11
13
  import { MetaData } from '@/types/meta';
12
14
  import { merge } from '@/utils/merge';
@@ -27,7 +29,7 @@ const getMeta = (message: ChatMessage) => {
27
29
  }
28
30
 
29
31
  case 'assistant': {
30
- return agentSelectors.currentAgentMeta(useSessionStore.getState());
32
+ return sessionMetaSelectors.currentAgentMeta(useSessionStore.getState());
31
33
  }
32
34
 
33
35
  case 'function': {
@@ -88,14 +90,14 @@ const currentChatsWithGuideMessage =
88
90
  };
89
91
 
90
92
  const currentChatIDsWithGuideMessage = (s: ChatStore) => {
91
- const meta = agentSelectors.currentAgentMeta(useSessionStore.getState());
93
+ const meta = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState());
92
94
 
93
95
  return currentChatsWithGuideMessage(meta)(s).map((s) => s.id);
94
96
  };
95
97
 
96
98
  const currentChatsWithHistoryConfig = (s: ChatStore): ChatMessage[] => {
97
99
  const chats = currentChats(s);
98
- const config = agentSelectors.currentAgentConfig(useSessionStore.getState());
100
+ const config = agentSelectors.currentAgentConfig(useAgentStore.getState());
99
101
 
100
102
  return chatHelpers.getSlicedMessagesWithConfig(chats, config);
101
103
  };
@@ -4,8 +4,10 @@ import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
6
6
  import { shareGPTService } from '@/services/share';
7
+ import { useAgentStore } from '@/store/agent';
8
+ import { agentSelectors } from '@/store/agent/selectors';
7
9
  import { useSessionStore } from '@/store/session';
8
- import { agentSelectors } from '@/store/session/selectors';
10
+ import { sessionMetaSelectors } from '@/store/session/selectors';
9
11
  import { ShareGPTConversation } from '@/types/share';
10
12
 
11
13
  import { chatSelectors } from '../../selectors';
@@ -56,8 +58,8 @@ export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [
56
58
  ) => ({
57
59
  shareToShareGPT: async ({ withSystemRole, withPluginInfo, avatar }) => {
58
60
  const messages = chatSelectors.currentChats(get());
59
- const config = agentSelectors.currentAgentConfig(useSessionStore.getState());
60
- const meta = agentSelectors.currentAgentMeta(useSessionStore.getState());
61
+ const config = agentSelectors.currentAgentConfig(useAgentStore.getState());
62
+ const meta = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState());
61
63
 
62
64
  const defaultMsg: ShareGPTConversation['items'] = [];
63
65
  const showSystemRole = withSystemRole && !!config.systemRole;
@@ -1,6 +1,8 @@
1
1
  import { GlobalStore } from '@/store/global';
2
+ import { SessionDefaultGroup } from '@/types/session';
2
3
 
3
- const sessionGroupKeys = (s: GlobalStore): string[] => s.preference.expandSessionGroupKeys || [];
4
+ const sessionGroupKeys = (s: GlobalStore): string[] =>
5
+ s.preference.expandSessionGroupKeys || [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default];
4
6
 
5
7
  const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterToSend || false;
6
8
 
@@ -1,3 +1,2 @@
1
- export { agentSelectors } from './slices/agent/selectors';
2
- export { sessionSelectors } from './slices/session/selectors';
1
+ export { sessionMetaSelectors, sessionSelectors } from './slices/session/selectors';
3
2
  export { sessionGroupSelectors } from './slices/sessionGroup/selectors';