@lobehub/chat 0.149.3 → 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.
- package/.github/FUNDING.yml +1 -1
- package/CHANGELOG.md +58 -0
- package/package.json +1 -1
- package/src/app/chat/(desktop)/features/ChatHeader/Main.tsx +5 -5
- package/src/app/chat/(desktop)/features/ChatHeader/Tags.tsx +3 -3
- package/src/app/chat/(desktop)/features/ChatInput/Footer/DragUpload.tsx +9 -9
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +3 -3
- package/src/app/chat/(desktop)/features/SideBar/SystemRole/index.tsx +8 -3
- package/src/app/chat/(mobile)/mobile/ChatHeader/ChatHeaderTitle.tsx +2 -2
- package/src/app/chat/(mobile)/mobile/page.tsx +0 -6
- package/src/app/chat/_layout/Desktop/SessionList.tsx +2 -0
- package/src/app/chat/features/PageTitle/index.tsx +3 -3
- package/src/app/chat/features/PluginTag/PluginStatus.tsx +2 -2
- package/src/app/chat/features/SessionListContent/DefaultMode.tsx +4 -2
- package/src/app/chat/features/SessionListContent/List/Item/index.tsx +10 -17
- package/src/app/chat/features/SessionListContent/index.tsx +2 -0
- package/src/app/chat/features/ShareButton/Preview.tsx +15 -11
- package/src/app/chat/features/ShareButton/useScreenshot.ts +2 -2
- package/src/app/chat/settings/features/EditPage.tsx +10 -7
- package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +5 -3
- package/src/app/metadata.ts +3 -3
- package/src/app/settings/(mobile)/features/AvatarBanner.tsx +1 -0
- package/src/config/modelProviders/ollama.ts +11 -12
- package/src/const/session.ts +1 -0
- package/src/database/client/models/session.ts +1 -0
- package/src/database/client/models/user.ts +6 -0
- package/src/features/ChatInput/ActionBar/FileUpload.tsx +11 -5
- package/src/features/ChatInput/ActionBar/History.tsx +3 -3
- package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -0
- package/src/features/ChatInput/ActionBar/Temperature.tsx +3 -3
- package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +4 -4
- package/src/features/ChatInput/ActionBar/Token/index.tsx +3 -3
- package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +3 -3
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +4 -4
- package/src/features/ChatInput/STT/browser.tsx +3 -3
- package/src/features/ChatInput/STT/openai.tsx +3 -3
- package/src/features/ChatInput/useChatInput.ts +3 -3
- package/src/features/Conversation/Extras/Assistant.test.tsx +7 -7
- package/src/features/Conversation/Extras/Assistant.tsx +3 -3
- package/src/features/Conversation/Extras/TTS/index.tsx +3 -3
- package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +2 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +6 -4
- package/src/features/Conversation/hooks/useInitConversation.ts +10 -7
- package/src/features/Conversation/index.tsx +6 -3
- package/src/features/ModelSwitchPanel/index.tsx +6 -4
- package/src/hooks/useTTS.ts +4 -4
- package/src/libs/agent-runtime/anthropic/index.test.ts +44 -32
- package/src/libs/agent-runtime/anthropic/index.ts +12 -9
- package/src/libs/agent-runtime/azureOpenai/index.ts +3 -4
- package/src/libs/agent-runtime/bedrock/index.ts +1 -1
- package/src/libs/agent-runtime/ollama/index.ts +7 -0
- package/src/libs/agent-runtime/perplexity/index.ts +1 -0
- package/src/libs/agent-runtime/types/chat.ts +2 -1
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -0
- package/src/services/chat.ts +18 -15
- package/src/services/session/client.ts +19 -0
- package/src/services/session/type.ts +2 -0
- package/src/store/agent/index.ts +2 -0
- package/src/store/agent/initialState.ts +7 -0
- package/src/store/agent/selectors.ts +1 -0
- package/src/store/{session/slices/agent → agent/slices/chat}/action.test.ts +26 -63
- package/src/store/agent/slices/chat/action.ts +107 -0
- package/src/store/agent/slices/chat/initialState.ts +14 -0
- package/src/store/agent/slices/chat/selectors.test.ts +82 -0
- package/src/store/agent/slices/chat/selectors.ts +81 -0
- package/src/store/agent/store.ts +27 -0
- package/src/store/chat/slices/message/action.test.ts +3 -2
- package/src/store/chat/slices/message/action.ts +3 -3
- package/src/store/chat/slices/message/selectors.test.ts +9 -2
- package/src/store/chat/slices/message/selectors.ts +6 -4
- package/src/store/chat/slices/share/action.ts +5 -3
- package/src/store/global/slices/preference/selectors.ts +3 -1
- package/src/store/session/selectors.ts +1 -2
- package/src/store/session/slices/session/action.test.ts +43 -0
- package/src/store/session/slices/session/action.ts +28 -18
- package/src/store/session/slices/session/helpers.ts +2 -3
- package/src/store/session/slices/session/initialState.ts +1 -17
- package/src/store/session/slices/session/selectors/index.ts +1 -0
- package/src/store/session/slices/session/selectors/list.test.ts +5 -3
- package/src/store/session/slices/session/selectors/list.ts +2 -3
- package/src/store/session/slices/session/selectors/meta.test.ts +108 -0
- package/src/store/session/slices/session/selectors/meta.ts +45 -0
- package/src/store/session/store.ts +1 -7
- package/src/types/session.ts +1 -0
- package/src/store/session/slices/agent/action.ts +0 -84
- package/src/store/session/slices/agent/selectors.test.ts +0 -180
- package/src/store/session/slices/agent/selectors.ts +0 -129
- /package/src/store/{session/slices/agent → agent/slices/chat}/index.ts +0 -0
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
162
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
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(
|
|
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 {
|
|
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(
|
|
60
|
-
const meta =
|
|
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[] =>
|
|
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 {
|
|
2
|
-
export { sessionSelectors } from './slices/session/selectors';
|
|
1
|
+
export { sessionMetaSelectors, sessionSelectors } from './slices/session/selectors';
|
|
3
2
|
export { sessionGroupSelectors } from './slices/sessionGroup/selectors';
|
|
@@ -7,6 +7,8 @@ import { sessionService } from '@/services/session';
|
|
|
7
7
|
import { useSessionStore } from '@/store/session';
|
|
8
8
|
import { LobeSessionType } from '@/types/session';
|
|
9
9
|
|
|
10
|
+
import { sessionSelectors } from './selectors';
|
|
11
|
+
|
|
10
12
|
// Mock sessionService 和其他依赖项
|
|
11
13
|
vi.mock('@/services/session', () => ({
|
|
12
14
|
sessionService: {
|
|
@@ -189,4 +191,45 @@ describe('SessionAction', () => {
|
|
|
189
191
|
expect(mockRefresh).toHaveBeenCalled();
|
|
190
192
|
});
|
|
191
193
|
});
|
|
194
|
+
|
|
195
|
+
describe('updateAgentMeta', () => {
|
|
196
|
+
it('should not update meta if there is no current session', async () => {
|
|
197
|
+
const { result } = renderHook(() => useSessionStore());
|
|
198
|
+
const meta = { title: 'Test Agent' };
|
|
199
|
+
const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
|
|
200
|
+
const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
|
|
201
|
+
|
|
202
|
+
// 模拟没有当前会话
|
|
203
|
+
vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue(null as any);
|
|
204
|
+
|
|
205
|
+
await act(async () => {
|
|
206
|
+
await result.current.updateSessionMeta(meta as any);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(updateSessionMock).not.toHaveBeenCalled();
|
|
210
|
+
expect(refreshSessionsMock).not.toHaveBeenCalled();
|
|
211
|
+
updateSessionMock.mockRestore();
|
|
212
|
+
refreshSessionsMock.mockRestore();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should update session meta and refresh sessions', async () => {
|
|
216
|
+
const { result } = renderHook(() => useSessionStore());
|
|
217
|
+
const meta = { title: 'Test Agent' };
|
|
218
|
+
const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
|
|
219
|
+
const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
|
|
220
|
+
|
|
221
|
+
// 模拟有当前会话
|
|
222
|
+
vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue({ id: 'session-id' } as any);
|
|
223
|
+
vi.spyOn(result.current, 'activeId', 'get').mockReturnValue('session-id');
|
|
224
|
+
|
|
225
|
+
await act(async () => {
|
|
226
|
+
await result.current.updateSessionMeta(meta);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(updateSessionMock).toHaveBeenCalledWith('session-id', { meta });
|
|
230
|
+
expect(refreshSessionsMock).toHaveBeenCalled();
|
|
231
|
+
updateSessionMock.mockRestore();
|
|
232
|
+
refreshSessionsMock.mockRestore();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
192
235
|
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { t } from 'i18next';
|
|
2
|
+
import { isEqual } from 'lodash-es';
|
|
2
3
|
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
3
4
|
import { DeepPartial } from 'utility-types';
|
|
4
5
|
import { StateCreator } from 'zustand/vanilla';
|
|
5
6
|
|
|
6
7
|
import { message } from '@/components/AntdStaticMethods';
|
|
7
|
-
import { INBOX_SESSION_ID } from '@/const/session';
|
|
8
|
-
import {
|
|
8
|
+
import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
|
|
9
|
+
import { useClientDataSWR } from '@/libs/swr';
|
|
9
10
|
import { sessionService } from '@/services/session';
|
|
10
11
|
import { useGlobalStore } from '@/store/global';
|
|
11
12
|
import { settingsSelectors } from '@/store/global/selectors';
|
|
12
13
|
import { SessionStore } from '@/store/session';
|
|
14
|
+
import { MetaData } from '@/types/meta';
|
|
13
15
|
import {
|
|
14
16
|
ChatSessionList,
|
|
15
17
|
LobeAgentSession,
|
|
@@ -21,10 +23,9 @@ import {
|
|
|
21
23
|
import { merge } from '@/utils/merge';
|
|
22
24
|
import { setNamespace } from '@/utils/storeDebug';
|
|
23
25
|
|
|
24
|
-
import { agentSelectors } from '../agent/selectors';
|
|
25
|
-
import { initLobeSession } from './initialState';
|
|
26
26
|
import { SessionDispatch, sessionsReducer } from './reducers';
|
|
27
27
|
import { sessionSelectors } from './selectors';
|
|
28
|
+
import { sessionMetaSelectors } from './selectors/meta';
|
|
28
29
|
|
|
29
30
|
const n = setNamespace('session');
|
|
30
31
|
|
|
@@ -53,6 +54,7 @@ export interface SessionAction {
|
|
|
53
54
|
) => Promise<string>;
|
|
54
55
|
duplicateSession: (id: string) => Promise<void>;
|
|
55
56
|
updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
|
|
57
|
+
updateSessionMeta: (meta: Partial<MetaData>) => void;
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* Pins or unpins a session.
|
|
@@ -61,7 +63,7 @@ export interface SessionAction {
|
|
|
61
63
|
/**
|
|
62
64
|
* re-fetch the data
|
|
63
65
|
*/
|
|
64
|
-
refreshSessions: (
|
|
66
|
+
refreshSessions: () => Promise<void>;
|
|
65
67
|
/**
|
|
66
68
|
* remove session
|
|
67
69
|
* @param id - sessionId
|
|
@@ -106,7 +108,7 @@ export const createSessionSlice: StateCreator<
|
|
|
106
108
|
|
|
107
109
|
// merge the defaultAgent in settings
|
|
108
110
|
const defaultAgent = merge(
|
|
109
|
-
|
|
111
|
+
DEFAULT_AGENT_LOBE_SESSION,
|
|
110
112
|
settingsSelectors.defaultAgent(useGlobalStore.getState()),
|
|
111
113
|
);
|
|
112
114
|
|
|
@@ -125,7 +127,7 @@ export const createSessionSlice: StateCreator<
|
|
|
125
127
|
const session = sessionSelectors.getSessionById(id)(get());
|
|
126
128
|
|
|
127
129
|
if (!session) return;
|
|
128
|
-
const title =
|
|
130
|
+
const title = sessionMetaSelectors.getTitle(session.meta);
|
|
129
131
|
|
|
130
132
|
const newTitle = t('duplicateSession.title', { ns: 'chat', title: title });
|
|
131
133
|
|
|
@@ -157,10 +159,6 @@ export const createSessionSlice: StateCreator<
|
|
|
157
159
|
await get().internal_updateSession(id, { pinned });
|
|
158
160
|
},
|
|
159
161
|
|
|
160
|
-
refreshSessions: async () => {
|
|
161
|
-
await mutate(FETCH_SESSIONS_KEY);
|
|
162
|
-
},
|
|
163
|
-
|
|
164
162
|
removeSession: async (sessionId) => {
|
|
165
163
|
await sessionService.removeSession(sessionId);
|
|
166
164
|
await get().refreshSessions();
|
|
@@ -175,16 +173,25 @@ export const createSessionSlice: StateCreator<
|
|
|
175
173
|
await get().internal_updateSession(sessionId, { group });
|
|
176
174
|
},
|
|
177
175
|
|
|
176
|
+
updateSessionMeta: async (meta) => {
|
|
177
|
+
const session = sessionSelectors.currentSession(get());
|
|
178
|
+
if (!session) return;
|
|
179
|
+
|
|
180
|
+
const { activeId, refreshSessions } = get();
|
|
181
|
+
|
|
182
|
+
await sessionService.updateSession(activeId, { meta });
|
|
183
|
+
await refreshSessions();
|
|
184
|
+
},
|
|
185
|
+
|
|
178
186
|
useFetchSessions: () =>
|
|
179
187
|
useClientDataSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getGroupedSessions, {
|
|
180
188
|
onSuccess: (data) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// 避免互相依赖的情况出现
|
|
189
|
+
if (
|
|
190
|
+
get().isSessionsFirstFetchFinished &&
|
|
191
|
+
isEqual(get().sessions, data.sessions) &&
|
|
192
|
+
isEqual(get().sessionGroups, data.sessionGroups)
|
|
193
|
+
)
|
|
194
|
+
return;
|
|
188
195
|
|
|
189
196
|
get().internal_processSessions(
|
|
190
197
|
data.sessions,
|
|
@@ -239,4 +246,7 @@ export const createSessionSlice: StateCreator<
|
|
|
239
246
|
n('processSessions'),
|
|
240
247
|
);
|
|
241
248
|
},
|
|
249
|
+
refreshSessions: async () => {
|
|
250
|
+
await mutate(FETCH_SESSIONS_KEY);
|
|
251
|
+
},
|
|
242
252
|
});
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
|
|
1
2
|
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
2
3
|
|
|
3
|
-
import { initLobeSession } from './initialState';
|
|
4
|
-
|
|
5
4
|
export const getSessionPinned = (session: LobeAgentSession) => session.pinned;
|
|
6
5
|
|
|
7
6
|
const getSessionById = (id: string, sessions: LobeSessions): LobeAgentSession => {
|
|
8
7
|
const session = sessions.find((s) => s.id === id);
|
|
9
8
|
|
|
10
|
-
if (!session) return
|
|
9
|
+
if (!session) return DEFAULT_AGENT_LOBE_SESSION;
|
|
11
10
|
|
|
12
11
|
return session;
|
|
13
12
|
};
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
|
3
|
-
import {
|
|
4
|
-
CustomSessionGroup,
|
|
5
|
-
LobeAgentSession,
|
|
6
|
-
LobeSessionGroups,
|
|
7
|
-
LobeSessionType,
|
|
8
|
-
} from '@/types/session';
|
|
9
|
-
|
|
10
|
-
export const initLobeSession: LobeAgentSession = {
|
|
11
|
-
config: DEFAULT_AGENT_CONFIG,
|
|
12
|
-
createdAt: Date.now(),
|
|
13
|
-
id: '',
|
|
14
|
-
meta: DEFAULT_AGENT_META,
|
|
15
|
-
type: LobeSessionType.Agent,
|
|
16
|
-
updatedAt: Date.now(),
|
|
17
|
-
};
|
|
1
|
+
import { CustomSessionGroup, LobeAgentSession, LobeSessionGroups } from '@/types/session';
|
|
18
2
|
|
|
19
3
|
export interface SessionState {
|
|
20
4
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
|
|
1
2
|
import type { SessionStore } from '@/store/session';
|
|
2
3
|
import { LobeAgentSession, LobeSessionType } from '@/types/session';
|
|
3
4
|
|
|
4
|
-
import { initLobeSession } from '../initialState';
|
|
5
5
|
import { sessionSelectors } from './list';
|
|
6
6
|
|
|
7
7
|
describe('currentSession', () => {
|
|
@@ -64,7 +64,9 @@ describe('currentSessionSafe', () => {
|
|
|
64
64
|
} as unknown as SessionStore;
|
|
65
65
|
|
|
66
66
|
it('should return initLobeSession when currentSession(s) returns undefined', () => {
|
|
67
|
-
expect(sessionSelectors.currentSessionSafe({ sessions: {} } as any)).toEqual(
|
|
67
|
+
expect(sessionSelectors.currentSessionSafe({ sessions: {} } as any)).toEqual(
|
|
68
|
+
DEFAULT_AGENT_LOBE_SESSION,
|
|
69
|
+
);
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
it('should return the result of currentSession(s) when it returns a non-undefined value', () => {
|
|
@@ -102,7 +104,7 @@ describe('getSessionById', () => {
|
|
|
102
104
|
});
|
|
103
105
|
|
|
104
106
|
it('should return initLobeSession when the session with the specified id does not exist', () => {
|
|
105
|
-
expect(sessionSelectors.getSessionById('3')(s)).toEqual(
|
|
107
|
+
expect(sessionSelectors.getSessionById('3')(s)).toEqual(DEFAULT_AGENT_LOBE_SESSION);
|
|
106
108
|
});
|
|
107
109
|
});
|
|
108
110
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { INBOX_SESSION_ID } from '@/const/session';
|
|
1
|
+
import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
|
|
2
2
|
import { sessionHelpers } from '@/store/session/slices/session/helpers';
|
|
3
3
|
import { MetaData } from '@/types/meta';
|
|
4
4
|
import { CustomSessionGroup, LobeAgentSession, LobeSessions } from '@/types/session';
|
|
5
5
|
|
|
6
6
|
import { SessionStore } from '../../../store';
|
|
7
|
-
import { initLobeSession } from '../initialState';
|
|
8
7
|
|
|
9
8
|
const defaultSessions = (s: SessionStore): LobeSessions => s.defaultSessions;
|
|
10
9
|
const pinnedSessions = (s: SessionStore): LobeSessions => s.pinnedSessions;
|
|
@@ -33,7 +32,7 @@ const currentSession = (s: SessionStore): LobeAgentSession | undefined => {
|
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
const currentSessionSafe = (s: SessionStore): LobeAgentSession => {
|
|
36
|
-
return currentSession(s) ||
|
|
35
|
+
return currentSession(s) || DEFAULT_AGENT_LOBE_SESSION;
|
|
37
36
|
};
|
|
38
37
|
|
|
39
38
|
const hasCustomAgents = (s: SessionStore) => defaultSessions(s).length > 0;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_AVATAR } from '@/const/meta';
|
|
4
|
+
import { DEFAULT_AGENT_CONFIG, DEFAUTT_AGENT_TTS_CONFIG } from '@/const/settings';
|
|
5
|
+
import { SessionStore } from '@/store/session';
|
|
6
|
+
import { MetaData } from '@/types/meta';
|
|
7
|
+
import { LobeAgentSession, LobeSessionType } from '@/types/session';
|
|
8
|
+
|
|
9
|
+
import { sessionMetaSelectors } from './meta';
|
|
10
|
+
|
|
11
|
+
vi.mock('i18next', () => ({
|
|
12
|
+
t: vi.fn((key) => key), // Simplified mock return value
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockSessionStore = {
|
|
16
|
+
activeId: '1',
|
|
17
|
+
sessions: [
|
|
18
|
+
{
|
|
19
|
+
id: '1',
|
|
20
|
+
config: DEFAULT_AGENT_CONFIG,
|
|
21
|
+
meta: {
|
|
22
|
+
title: 'title1',
|
|
23
|
+
description: 'description1',
|
|
24
|
+
},
|
|
25
|
+
type: LobeSessionType.Agent,
|
|
26
|
+
} as LobeAgentSession,
|
|
27
|
+
{
|
|
28
|
+
id: '2',
|
|
29
|
+
meta: {
|
|
30
|
+
title: 'title2',
|
|
31
|
+
description: 'description2',
|
|
32
|
+
},
|
|
33
|
+
config: DEFAULT_AGENT_CONFIG,
|
|
34
|
+
type: LobeSessionType.Agent,
|
|
35
|
+
} as LobeAgentSession,
|
|
36
|
+
],
|
|
37
|
+
} as unknown as SessionStore;
|
|
38
|
+
|
|
39
|
+
describe('sessionMetaSelectors', () => {
|
|
40
|
+
describe('currentAgentMeta', () => {
|
|
41
|
+
it('should return the merged default and session-specific meta data', () => {
|
|
42
|
+
const meta = sessionMetaSelectors.currentAgentMeta(mockSessionStore);
|
|
43
|
+
expect(meta).toEqual(expect.objectContaining(mockSessionStore.sessions[0].meta));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return inbox defaults if it is an inbox session', () => {
|
|
47
|
+
// Assume sessionSelectors.isInboxSession() is mocked to return true for this test
|
|
48
|
+
const meta = sessionMetaSelectors.currentAgentMeta(mockSessionStore);
|
|
49
|
+
expect(meta.avatar).toBe(DEFAULT_AVATAR);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('currentAgentTitle', () => {
|
|
54
|
+
it('should return the title from the session meta data', () => {
|
|
55
|
+
const title = sessionMetaSelectors.currentAgentTitle(mockSessionStore);
|
|
56
|
+
expect(title).toBe(mockSessionStore.sessions[0].meta.title);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('currentAgentDescription', () => {
|
|
61
|
+
it('should return the description from the session meta data', () => {
|
|
62
|
+
const description = sessionMetaSelectors.currentAgentDescription(mockSessionStore);
|
|
63
|
+
expect(description).toBe(mockSessionStore.sessions[0].meta.description);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getAvatar', () => {
|
|
68
|
+
it('should return the avatar from the meta data', () => {
|
|
69
|
+
const meta: MetaData = { avatar: 'custom-avatar.png' };
|
|
70
|
+
const avatar = sessionMetaSelectors.getAvatar(meta);
|
|
71
|
+
expect(avatar).toBe(meta.avatar);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return the default avatar if none is defined in the meta data', () => {
|
|
75
|
+
const meta: MetaData = {};
|
|
76
|
+
const avatar = sessionMetaSelectors.getAvatar(meta);
|
|
77
|
+
expect(avatar).toBe(DEFAULT_AVATAR);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getTitle', () => {
|
|
82
|
+
it('should return the title from the meta data', () => {
|
|
83
|
+
const meta: MetaData = { title: 'Custom Title' };
|
|
84
|
+
const title = sessionMetaSelectors.getTitle(meta);
|
|
85
|
+
expect(title).toBe(meta.title);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return the default title if none is defined in the meta data', () => {
|
|
89
|
+
const meta: MetaData = {};
|
|
90
|
+
const title = sessionMetaSelectors.getTitle(meta);
|
|
91
|
+
expect(title).toBe('defaultSession'); // Assuming translation returns this key
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('getDescription', () => {
|
|
96
|
+
it('should return the description from the meta data', () => {
|
|
97
|
+
const meta: MetaData = { description: 'Custom Description' };
|
|
98
|
+
const description = sessionMetaSelectors.getDescription(meta);
|
|
99
|
+
expect(description).toBe(meta.description);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return the default description if none is defined in the meta data', () => {
|
|
103
|
+
const meta: MetaData = {};
|
|
104
|
+
const description = sessionMetaSelectors.getDescription(meta);
|
|
105
|
+
expect(description).toBe('noDescription'); // Assuming translation returns this key
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|