@lobehub/chat 0.106.0 → 0.107.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.
- package/CHANGELOG.md +50 -0
- package/locales/en_US/common.json +19 -0
- package/locales/en_US/migration.json +45 -0
- package/locales/en_US/setting.json +7 -7
- package/locales/ja_JP/common.json +19 -0
- package/locales/ja_JP/migration.json +45 -0
- package/locales/ja_JP/setting.json +2 -2
- package/locales/ko_KR/common.json +19 -0
- package/locales/ko_KR/migration.json +45 -0
- package/locales/ko_KR/setting.json +2 -2
- package/locales/ru_RU/common.json +19 -0
- package/locales/ru_RU/migration.json +45 -0
- package/locales/ru_RU/setting.json +2 -2
- package/locales/zh_CN/common.json +19 -0
- package/locales/zh_CN/migration.json +45 -0
- package/locales/zh_CN/setting.json +1 -1
- package/locales/zh_TW/common.json +19 -0
- package/locales/zh_TW/migration.json +45 -0
- package/locales/zh_TW/setting.json +2 -2
- package/package.json +2 -1
- package/src/app/api/openai/chat/createChatCompletion.test.ts +4 -4
- package/src/app/api/openai/chat/createChatCompletion.ts +4 -10
- package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
- package/src/app/api/openai/errorResponse.test.ts +1 -1
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +2 -1
- package/src/app/chat/(mobile)/features/ChatInput/SendButton.tsx +2 -2
- package/src/app/chat/features/ChatHeader/ShareButton/Inner.tsx +2 -5
- package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +2 -2
- package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +2 -2
- package/src/app/chat/features/ChatInput/ActionBar/Token/TokenTag.tsx +8 -2
- package/src/app/chat/features/ChatInput/InputAreaInner/index.tsx +2 -2
- package/src/app/chat/features/ChatInput/STT/index.tsx +2 -2
- package/src/app/chat/features/ChatInput/Topic/index.tsx +2 -2
- package/src/app/chat/features/ChatInput/useSend.ts +3 -3
- package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +2 -2
- package/src/app/chat/features/Conversation/ChatList/Error/ApiKeyForm.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Error/InvalidAccess.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.test.tsx +4 -4
- package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +30 -31
- package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Extras/Translate.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +8 -5
- package/src/app/chat/features/Conversation/ChatList/Extras/index.ts +3 -3
- package/src/app/chat/features/Conversation/ChatList/Extras/type.ts +5 -0
- package/src/app/chat/features/Conversation/ChatList/Messages/Assistant.tsx +10 -6
- package/src/app/chat/features/Conversation/ChatList/Messages/Default.tsx +7 -3
- package/src/app/chat/features/Conversation/ChatList/Messages/Function.tsx +7 -6
- package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +7 -7
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +6 -6
- package/src/app/chat/features/Conversation/ChatList/index.tsx +16 -24
- package/src/app/chat/features/Conversation/ScrollAnchor/index.tsx +2 -2
- package/src/app/chat/features/Conversation/index.tsx +3 -3
- package/src/app/chat/features/Conversation/useInitConversation.ts +43 -0
- package/src/app/chat/features/Migration/ExportConfigButton.tsx +33 -0
- package/src/app/chat/features/Migration/Failed.tsx +115 -0
- package/src/app/chat/features/Migration/Modal.tsx +80 -0
- package/src/app/chat/features/Migration/Start.tsx +106 -0
- package/src/app/chat/features/Migration/UpgradeButton.tsx +74 -0
- package/src/app/chat/features/Migration/const.ts +15 -0
- package/src/app/chat/features/Migration/index.tsx +44 -0
- package/src/app/chat/features/SessionListContent/DefaultMode.tsx +54 -0
- package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +5 -4
- package/src/app/chat/features/SessionListContent/List/Item/index.tsx +22 -28
- package/src/app/chat/features/SessionListContent/SearchMode.tsx +15 -0
- package/src/app/chat/features/SessionListContent/index.tsx +4 -44
- package/src/app/chat/features/SessionSearchBar/index.tsx +11 -3
- package/src/app/chat/features/TopicListContent/Header.tsx +4 -4
- package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +11 -9
- package/src/app/chat/features/TopicListContent/Topic/TopicItem.tsx +15 -13
- package/src/app/chat/features/TopicListContent/Topic/index.tsx +9 -16
- package/src/app/chat/features/TopicListContent/TopicSearchBar/index.tsx +13 -4
- package/src/app/chat/features/TopicListContent/index.tsx +1 -2
- package/src/app/chat/page.tsx +6 -1
- package/src/app/chat/settings/features/HeaderContent.tsx +3 -3
- package/src/app/home/Redirect.tsx +13 -7
- package/src/app/settings/(mobile)/features/ExtraList.tsx +12 -4
- package/src/app/settings/common/Common.tsx +16 -4
- package/src/app/welcome/features/Banner/index.tsx +9 -10
- package/src/chains/summaryTitle.ts +5 -2
- package/src/components/DataStyleModal/index.tsx +64 -0
- package/src/config/client.ts +5 -0
- package/src/const/session.ts +21 -0
- package/src/const/settings.ts +1 -1
- package/src/database/core/__tests__/db.test.ts +8 -5
- package/src/database/core/__tests__/model.test.ts +3 -3
- package/src/database/core/db.ts +38 -10
- package/src/database/core/index.ts +1 -0
- package/src/database/core/model.ts +121 -10
- package/src/database/core/schemas.ts +22 -0
- package/src/database/core/types/db.ts +13 -0
- package/src/database/models/__DEBUG.ts +119 -0
- package/src/database/models/{file.test.ts → __tests__/file.test.ts} +19 -6
- package/src/database/models/__tests__/message.test.ts +314 -0
- package/src/database/models/__tests__/session.test.ts +117 -0
- package/src/database/models/__tests__/topic.test.ts +250 -0
- package/src/database/models/file.ts +9 -5
- package/src/database/models/message.ts +186 -0
- package/src/database/models/session.ts +160 -0
- package/src/database/models/topic.ts +196 -0
- package/src/{types/database → database/schemas}/files.ts +2 -2
- package/src/database/schemas/message.ts +37 -0
- package/src/database/schemas/session.ts +56 -0
- package/src/database/schemas/topic.ts +12 -0
- package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +1 -1
- package/src/features/AgentSetting/store/action.ts +1 -1
- package/src/features/AgentSetting/store/initialState.ts +1 -1
- package/src/features/AgentSetting/store/reducers/config.ts +1 -1
- package/src/features/DataImporter/index.tsx +240 -0
- package/src/features/DebugUI/index.tsx +34 -0
- package/src/features/SideBar/BottomActions.tsx +8 -14
- package/src/hooks/useImportConfig.ts +13 -30
- package/src/hooks/useTTS.ts +1 -1
- package/src/layout/GlobalLayout/index.tsx +16 -1
- package/src/locales/default/common.ts +19 -0
- package/src/locales/default/migration.ts +47 -0
- package/src/locales/default/setting.ts +1 -1
- package/src/locales/resources/en_US.ts +2 -0
- package/src/locales/resources/ja_JP.ts +2 -0
- package/src/locales/resources/ko_KR.ts +2 -0
- package/src/locales/resources/ru_RU.ts +2 -0
- package/src/locales/resources/zh_CN.ts +2 -0
- package/src/locales/resources/zh_TW.ts +2 -0
- package/src/migrations/FromV0ToV1.ts +2 -4
- package/src/migrations/FromV1ToV2/fixtures/input-v1-session.json +191 -0
- package/src/migrations/FromV1ToV2/fixtures/output-v2.json +202 -0
- package/src/migrations/FromV1ToV2/index.ts +82 -0
- package/src/migrations/FromV1ToV2/migrations.test.ts +224 -0
- package/src/migrations/FromV1ToV2/types/v1.ts +78 -0
- package/src/migrations/FromV1ToV2/types/v2.ts +52 -0
- package/src/{utils → migrations}/VersionController.test.ts +1 -3
- package/src/{utils → migrations}/VersionController.ts +5 -2
- package/src/migrations/index.ts +17 -3
- package/src/services/__tests__/file.test.ts +3 -3
- package/src/services/chat.ts +66 -5
- package/src/services/config.ts +184 -0
- package/src/services/debug.ts +13 -0
- package/src/services/file.ts +6 -2
- package/src/services/github.ts +25 -0
- package/src/services/message.ts +79 -0
- package/src/services/session.ts +63 -0
- package/src/services/topic.ts +56 -0
- package/src/store/chat/actions/enhance.ts +95 -0
- package/src/store/{session/slices/chat → chat}/actions/message.ts +225 -247
- package/src/store/{session/slices/chat → chat}/actions/plugin.ts +29 -34
- package/src/store/{session/slices/chat → chat}/actions/share.ts +9 -13
- package/src/store/chat/actions/topic.ts +197 -0
- package/src/store/chat/helpers.ts +23 -0
- package/src/store/chat/index.ts +3 -0
- package/src/store/chat/initialState.ts +48 -0
- package/src/store/chat/reducers/message.test.ts +83 -0
- package/src/store/chat/reducers/message.ts +83 -0
- package/src/store/chat/selectors/__tests__/topic.test.ts +43 -0
- package/src/store/chat/selectors/chat.ts +119 -0
- package/src/store/chat/selectors/index.ts +2 -0
- package/src/store/chat/selectors/topic.ts +28 -0
- package/src/store/chat/store.ts +63 -0
- package/src/store/files/slices/images/action.ts +6 -4
- package/src/store/global/store.ts +1 -1
- package/src/store/middleware/createHyperStorage/index.ts +40 -24
- package/src/store/middleware/createHyperStorage/keyMapper.ts +8 -2
- package/src/store/middleware/createHyperStorage/type.ts +10 -8
- package/src/store/plugin/slices/plugin/action.ts +1 -1
- package/src/store/plugin/store.ts +2 -4
- package/src/store/session/helpers.ts +1 -1
- package/src/store/session/hooks/useSessionHydrated.ts +6 -10
- package/src/store/session/initialState.ts +26 -7
- package/src/store/session/selectors.ts +1 -2
- package/src/store/session/slices/agent/action.ts +26 -35
- package/src/store/session/slices/agent/selectors.ts +32 -37
- package/src/store/session/slices/session/action.ts +81 -88
- package/src/store/session/slices/session/helpers.ts +19 -0
- package/src/store/session/slices/session/initialState.ts +7 -10
- package/src/store/session/slices/session/selectors/export.ts +12 -9
- package/src/store/session/slices/session/selectors/index.ts +2 -35
- package/src/store/session/slices/session/selectors/list.test.ts +25 -203
- package/src/store/session/slices/session/selectors/list.ts +40 -78
- package/src/store/session/store.ts +11 -17
- package/src/types/agent/index.ts +61 -0
- package/src/types/chatMessage.ts +11 -20
- package/src/types/exportConfig.ts +7 -2
- package/src/types/files.ts +2 -2
- package/src/types/llm.ts +1 -1
- package/src/types/meta.ts +24 -12
- package/src/types/openai/chat.ts +9 -1
- package/src/types/openai/functionCall.ts +4 -0
- package/src/types/session.ts +10 -94
- package/src/types/translate.ts +2 -1
- package/src/utils/config.ts +5 -11
- package/src/utils/fetch.ts +8 -2
- package/src/app/chat/features/Conversation/usePluginsInit.ts +0 -14
- package/src/database/core/schema.ts +0 -9
- package/src/helpers/export.ts +0 -69
- package/src/hooks/useExportConfig.ts +0 -6
- package/src/store/session/slices/chat/actions/index.ts +0 -32
- package/src/store/session/slices/chat/actions/topic.ts +0 -171
- package/src/store/session/slices/chat/actions/translate.ts +0 -113
- package/src/store/session/slices/chat/helpers.ts +0 -9
- package/src/store/session/slices/chat/initialState.ts +0 -13
- package/src/store/session/slices/chat/reducers/message.test.ts +0 -319
- package/src/store/session/slices/chat/reducers/message.ts +0 -156
- package/src/store/session/slices/chat/reducers/topic.test.ts +0 -179
- package/src/store/session/slices/chat/reducers/topic.ts +0 -81
- package/src/store/session/slices/chat/selectors/chat.ts +0 -111
- package/src/store/session/slices/chat/selectors/index.ts +0 -26
- package/src/store/session/slices/chat/selectors/topic.ts +0 -30
- package/src/store/session/slices/chat/selectors/utils.test.ts +0 -385
- package/src/store/session/slices/chat/selectors/utils.ts +0 -91
- package/src/store/session/slices/chat/utils.ts +0 -13
- package/src/store/session/slices/session/reducers/session.test.ts +0 -577
- package/src/store/session/slices/session/reducers/session.ts +0 -182
- package/src/types/database/db.ts +0 -4
- /package/src/store/{session/slices/chat → chat}/reducers/files.test.ts +0 -0
- /package/src/store/{session/slices/chat → chat}/reducers/files.ts +0 -0
|
@@ -1,49 +1,48 @@
|
|
|
1
|
-
import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
|
2
1
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
2
|
|
|
4
3
|
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
5
4
|
import { chatService } from '@/services/chat';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { messageService } from '@/services/message';
|
|
6
|
+
import { ChatStore } from '@/store/chat/store';
|
|
7
|
+
import { ChatPluginPayload } from '@/types/chatMessage';
|
|
8
|
+
import { OpenAIFunctionCall } from '@/types/openai/functionCall';
|
|
8
9
|
import { setNamespace } from '@/utils/storeDebug';
|
|
9
10
|
|
|
10
|
-
import { sessionSelectors } from '../../session/selectors';
|
|
11
11
|
import { chatSelectors } from '../selectors';
|
|
12
12
|
|
|
13
|
-
const t = setNamespace('
|
|
13
|
+
const t = setNamespace('plugin');
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* 插件方法
|
|
17
|
-
*/
|
|
18
15
|
export interface ChatPluginAction {
|
|
19
16
|
fillPluginMessageContent: (id: string, content: string) => Promise<void>;
|
|
20
17
|
runPluginDefaultType: (id: string, payload: any) => Promise<void>;
|
|
21
18
|
triggerFunctionCall: (id: string) => Promise<void>;
|
|
22
|
-
updatePluginState: (id: string, key: string, value: any) => void
|
|
19
|
+
updatePluginState: (id: string, key: string, value: any) => Promise<void>;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
export const chatPlugin: StateCreator<
|
|
26
|
-
|
|
23
|
+
ChatStore,
|
|
27
24
|
[['zustand/devtools', never]],
|
|
28
25
|
[],
|
|
29
26
|
ChatPluginAction
|
|
30
27
|
> = (set, get) => ({
|
|
31
28
|
fillPluginMessageContent: async (id, content) => {
|
|
32
|
-
const {
|
|
29
|
+
const { coreProcessMessage, refreshMessages } = get();
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
await messageService.updateMessageContent(id, content);
|
|
32
|
+
await refreshMessages();
|
|
35
33
|
|
|
36
34
|
const chats = chatSelectors.currentChats(get());
|
|
37
35
|
await coreProcessMessage(chats, id);
|
|
38
36
|
},
|
|
39
37
|
runPluginDefaultType: async (id, payload) => {
|
|
40
|
-
const {
|
|
38
|
+
const { refreshMessages, coreProcessMessage, toggleChatLoading } = get();
|
|
41
39
|
let data: string;
|
|
42
40
|
try {
|
|
43
41
|
const abortController = toggleChatLoading(true, id, t('fetchPlugin') as string);
|
|
44
42
|
data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
|
|
45
43
|
} catch (error) {
|
|
46
|
-
|
|
44
|
+
await messageService.updateMessageError(id, error as any);
|
|
45
|
+
await refreshMessages();
|
|
47
46
|
|
|
48
47
|
data = '';
|
|
49
48
|
}
|
|
@@ -51,21 +50,19 @@ export const chatPlugin: StateCreator<
|
|
|
51
50
|
// 如果报错则结束了
|
|
52
51
|
if (!data) return;
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
await messageService.updateMessageContent(id, data);
|
|
54
|
+
await refreshMessages();
|
|
55
55
|
|
|
56
56
|
const chats = chatSelectors.currentChats(get());
|
|
57
57
|
await coreProcessMessage(chats, id);
|
|
58
58
|
},
|
|
59
59
|
triggerFunctionCall: async (id) => {
|
|
60
|
-
const {
|
|
61
|
-
const session = sessionSelectors.currentSession(get());
|
|
60
|
+
const { runPluginDefaultType, refreshMessages } = get();
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const message = session.chats[id];
|
|
62
|
+
const message = chatSelectors.getMessageById(id)(get());
|
|
66
63
|
if (!message) return;
|
|
67
64
|
|
|
68
|
-
let payload
|
|
65
|
+
let payload = { apiName: '', identifier: '' } as ChatPluginPayload;
|
|
69
66
|
|
|
70
67
|
// 识别到内容是 function_call 的情况下
|
|
71
68
|
// 将 function_call 转换为 plugin request payload
|
|
@@ -80,30 +77,28 @@ export const chatPlugin: StateCreator<
|
|
|
80
77
|
apiName,
|
|
81
78
|
arguments: function_call.arguments,
|
|
82
79
|
identifier,
|
|
83
|
-
type: type ?? 'default',
|
|
80
|
+
type: (type ?? 'default') as any,
|
|
84
81
|
};
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
dispatchMessage({ id, key: 'content', type: 'updateMessage', value: '' });
|
|
83
|
+
await messageService.updateMessageContent(id, '');
|
|
88
84
|
} else {
|
|
89
|
-
if (message.plugin)
|
|
90
|
-
payload = message.plugin;
|
|
91
|
-
}
|
|
85
|
+
if (message.plugin) payload = message.plugin;
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
if (!payload.apiName) return;
|
|
95
89
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
await messageService.updateMessageRole(id, 'function');
|
|
91
|
+
await messageService.updateMessagePlugin(id, payload);
|
|
92
|
+
await refreshMessages();
|
|
99
93
|
|
|
100
94
|
if (payload.type === 'standalone') {
|
|
101
|
-
//
|
|
95
|
+
// TODO: need to auth user's settings
|
|
102
96
|
} else runPluginDefaultType(id, payload);
|
|
103
97
|
},
|
|
104
|
-
updatePluginState: (id, key, value) => {
|
|
105
|
-
const {
|
|
98
|
+
updatePluginState: async (id, key, value) => {
|
|
99
|
+
const { refreshMessages } = get();
|
|
106
100
|
|
|
107
|
-
|
|
101
|
+
await messageService.updateMessagePluginState(id, key, value);
|
|
102
|
+
await refreshMessages();
|
|
108
103
|
},
|
|
109
104
|
});
|
|
@@ -4,12 +4,12 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
|
|
6
6
|
import { genShareGPTUrl } from '@/services/shareGPT';
|
|
7
|
-
import {
|
|
7
|
+
import { useSessionStore } from '@/store/session';
|
|
8
|
+
import { agentSelectors } from '@/store/session/selectors';
|
|
8
9
|
import { ShareGPTConversation } from '@/types/share';
|
|
9
10
|
|
|
10
|
-
import { agentSelectors } from '../../agent';
|
|
11
|
-
import { sessionSelectors } from '../../session/selectors';
|
|
12
11
|
import { chatSelectors } from '../selectors';
|
|
12
|
+
import { ChatStore } from '../store';
|
|
13
13
|
|
|
14
14
|
interface ShareMessage {
|
|
15
15
|
from: 'human' | 'gpt';
|
|
@@ -50,18 +50,14 @@ export interface ShareAction {
|
|
|
50
50
|
}) => void;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export const chatShare: StateCreator<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
ShareAction
|
|
58
|
-
> = (set, get) => ({
|
|
53
|
+
export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [], ShareAction> = (
|
|
54
|
+
set,
|
|
55
|
+
get,
|
|
56
|
+
) => ({
|
|
59
57
|
shareToShareGPT: async ({ withSystemRole, withPluginInfo, avatar }) => {
|
|
60
|
-
const session = sessionSelectors.currentSession(get());
|
|
61
|
-
if (!session) return;
|
|
62
58
|
const messages = chatSelectors.currentChats(get());
|
|
63
|
-
const config = agentSelectors.currentAgentConfig(
|
|
64
|
-
const meta = agentSelectors.currentAgentMeta(
|
|
59
|
+
const config = agentSelectors.currentAgentConfig(useSessionStore.getState());
|
|
60
|
+
const meta = agentSelectors.currentAgentMeta(useSessionStore.getState());
|
|
65
61
|
|
|
66
62
|
const defaultMsg: ShareGPTConversation['items'] = [];
|
|
67
63
|
const showSystemRole = withSystemRole && !!config.systemRole;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
|
+
// Note: To make the code more logic and readable, we just disable the auto sort key eslint rule
|
|
3
|
+
// DON'T REMOVE THE FIRST LINE
|
|
4
|
+
import { t } from 'i18next';
|
|
5
|
+
import { produce } from 'immer';
|
|
6
|
+
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
7
|
+
import { StateCreator } from 'zustand/vanilla';
|
|
8
|
+
|
|
9
|
+
import { chainSummaryTitle } from '@/chains/summaryTitle';
|
|
10
|
+
import { LOADING_FLAT } from '@/const/message';
|
|
11
|
+
import { chatService } from '@/services/chat';
|
|
12
|
+
import { messageService } from '@/services/message';
|
|
13
|
+
import { topicService } from '@/services/topic';
|
|
14
|
+
import { ChatStore } from '@/store/chat';
|
|
15
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
16
|
+
import { ChatTopic } from '@/types/topic';
|
|
17
|
+
import { setNamespace } from '@/utils/storeDebug';
|
|
18
|
+
|
|
19
|
+
import { chatSelectors, topicSelectors } from '../selectors';
|
|
20
|
+
|
|
21
|
+
const n = setNamespace('topic');
|
|
22
|
+
|
|
23
|
+
export interface ChatTopicAction {
|
|
24
|
+
favoriteTopic: (id: string, favState: boolean) => Promise<void>;
|
|
25
|
+
openNewTopicOrSaveTopic: () => void;
|
|
26
|
+
refreshTopic: () => Promise<void>;
|
|
27
|
+
removeAllTopics: () => Promise<void>;
|
|
28
|
+
removeSessionTopics: () => Promise<void>;
|
|
29
|
+
removeTopic: (id: string) => Promise<void>;
|
|
30
|
+
removeUnstarredTopic: () => void;
|
|
31
|
+
saveToTopic: () => Promise<string | undefined>;
|
|
32
|
+
summaryTopicTitle: (topicId: string, messages: ChatMessage[]) => Promise<void>;
|
|
33
|
+
switchTopic: (id?: string) => Promise<void>;
|
|
34
|
+
updateTopicTitleInSummary: (id: string, title: string) => void;
|
|
35
|
+
updateTopicLoading: (id?: string) => void;
|
|
36
|
+
updateTopicTitle: (id: string, title: string) => Promise<void>;
|
|
37
|
+
useFetchTopics: (sessionId: string) => SWRResponse<ChatTopic[]>;
|
|
38
|
+
useSearchTopics: (keywords?: string) => SWRResponse<ChatTopic[]>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const chatTopic: StateCreator<
|
|
42
|
+
ChatStore,
|
|
43
|
+
[['zustand/devtools', never]],
|
|
44
|
+
[],
|
|
45
|
+
ChatTopicAction
|
|
46
|
+
> = (set, get) => ({
|
|
47
|
+
// create
|
|
48
|
+
openNewTopicOrSaveTopic: () => {
|
|
49
|
+
const { switchTopic, saveToTopic, activeTopicId } = get();
|
|
50
|
+
const hasTopic = !!activeTopicId;
|
|
51
|
+
|
|
52
|
+
if (hasTopic) switchTopic();
|
|
53
|
+
else {
|
|
54
|
+
saveToTopic();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
saveToTopic: async () => {
|
|
59
|
+
// if there is no message, stop
|
|
60
|
+
const messages = chatSelectors.currentChats(get());
|
|
61
|
+
if (messages.length === 0) return;
|
|
62
|
+
|
|
63
|
+
const { activeId, summaryTopicTitle, refreshTopic, refreshMessages } = get();
|
|
64
|
+
|
|
65
|
+
// 1. create topic
|
|
66
|
+
const topicId = await topicService.createTopic({
|
|
67
|
+
sessionId: activeId,
|
|
68
|
+
title: t('topic.defaultTitle', { ns: 'chat' }),
|
|
69
|
+
});
|
|
70
|
+
await refreshTopic();
|
|
71
|
+
|
|
72
|
+
// 2.add topicId to these message
|
|
73
|
+
await messageService.bindMessagesToTopic(
|
|
74
|
+
topicId,
|
|
75
|
+
messages.map((m) => m.id),
|
|
76
|
+
);
|
|
77
|
+
await refreshMessages();
|
|
78
|
+
|
|
79
|
+
// 3. auto summary topic Title
|
|
80
|
+
// we don't need to wait for summary, just let it run async
|
|
81
|
+
summaryTopicTitle(topicId, messages);
|
|
82
|
+
|
|
83
|
+
return topicId;
|
|
84
|
+
},
|
|
85
|
+
// update
|
|
86
|
+
summaryTopicTitle: async (topicId, messages) => {
|
|
87
|
+
const { updateTopicTitleInSummary, updateTopicLoading, refreshTopic } = get();
|
|
88
|
+
const topic = topicSelectors.getTopicById(topicId)(get());
|
|
89
|
+
if (!topic) return;
|
|
90
|
+
|
|
91
|
+
updateTopicTitleInSummary(topicId, LOADING_FLAT);
|
|
92
|
+
|
|
93
|
+
let output = '';
|
|
94
|
+
|
|
95
|
+
// 自动总结话题标题
|
|
96
|
+
await chatService.fetchPresetTaskResult({
|
|
97
|
+
onError: () => {
|
|
98
|
+
updateTopicTitleInSummary(topicId, topic.title);
|
|
99
|
+
},
|
|
100
|
+
onFinish: (text) => {
|
|
101
|
+
topicService.updateTitle(topicId, text);
|
|
102
|
+
},
|
|
103
|
+
onLoadingChange: (loading) => {
|
|
104
|
+
updateTopicLoading(loading ? topicId : undefined);
|
|
105
|
+
},
|
|
106
|
+
onMessageHandle: (x) => {
|
|
107
|
+
output += x;
|
|
108
|
+
updateTopicTitleInSummary(topicId, output);
|
|
109
|
+
},
|
|
110
|
+
params: await chainSummaryTitle(messages),
|
|
111
|
+
});
|
|
112
|
+
await refreshTopic();
|
|
113
|
+
},
|
|
114
|
+
favoriteTopic: async (id, favState) => {
|
|
115
|
+
await topicService.updateFavorite(id, favState);
|
|
116
|
+
await get().refreshTopic();
|
|
117
|
+
},
|
|
118
|
+
updateTopicTitle: async (id, title) => {
|
|
119
|
+
await topicService.updateTitle(id, title);
|
|
120
|
+
await get().refreshTopic();
|
|
121
|
+
},
|
|
122
|
+
// query
|
|
123
|
+
useFetchTopics: (sessionId) =>
|
|
124
|
+
useSWR<ChatTopic[]>(sessionId, async (sessionId) => topicService.getTopics({ sessionId }), {
|
|
125
|
+
onSuccess: (topics) => {
|
|
126
|
+
set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId }));
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
useSearchTopics: (keywords) =>
|
|
130
|
+
useSWR<ChatTopic[]>(keywords, topicService.searchTopics, {
|
|
131
|
+
onSuccess: (data) => {
|
|
132
|
+
set({ searchTopics: data }, false, n('useSearchTopics(success)', { keywords }));
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
switchTopic: async (id) => {
|
|
136
|
+
set({ activeTopicId: id }, false, n('toggleTopic'));
|
|
137
|
+
|
|
138
|
+
await get().refreshMessages();
|
|
139
|
+
},
|
|
140
|
+
// delete
|
|
141
|
+
removeSessionTopics: async () => {
|
|
142
|
+
const { switchTopic, activeId, refreshTopic } = get();
|
|
143
|
+
|
|
144
|
+
await topicService.removeTopics(activeId);
|
|
145
|
+
await refreshTopic();
|
|
146
|
+
|
|
147
|
+
// switch to default topic
|
|
148
|
+
switchTopic();
|
|
149
|
+
},
|
|
150
|
+
removeAllTopics: async () => {
|
|
151
|
+
const { refreshTopic } = get();
|
|
152
|
+
|
|
153
|
+
await topicService.removeAllTopic();
|
|
154
|
+
await refreshTopic();
|
|
155
|
+
},
|
|
156
|
+
removeTopic: async (id) => {
|
|
157
|
+
const { activeId, switchTopic, refreshTopic } = get();
|
|
158
|
+
|
|
159
|
+
// remove messages in the topic
|
|
160
|
+
await messageService.removeMessages(activeId, id);
|
|
161
|
+
|
|
162
|
+
// remove topic
|
|
163
|
+
await topicService.removeTopic(id);
|
|
164
|
+
await refreshTopic();
|
|
165
|
+
|
|
166
|
+
// switch bach to default topic
|
|
167
|
+
switchTopic();
|
|
168
|
+
},
|
|
169
|
+
removeUnstarredTopic: async () => {
|
|
170
|
+
const { refreshTopic, switchTopic } = get();
|
|
171
|
+
const topics = topicSelectors.currentUnFavTopics(get());
|
|
172
|
+
|
|
173
|
+
await topicService.batchRemoveTopics(topics.map((t) => t.id));
|
|
174
|
+
await refreshTopic();
|
|
175
|
+
|
|
176
|
+
// 切换到默认 topic
|
|
177
|
+
switchTopic();
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Internal process method of the topics
|
|
181
|
+
updateTopicTitleInSummary: (id, title) => {
|
|
182
|
+
const topics = produce(get().topics, (draftState) => {
|
|
183
|
+
const topic = draftState.find((i) => i.id === id);
|
|
184
|
+
|
|
185
|
+
if (!topic) return;
|
|
186
|
+
topic.title = title;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
set({ topics }, false, n(`updateTopicTitleInSummary`, { id, title }));
|
|
190
|
+
},
|
|
191
|
+
updateTopicLoading: (id) => {
|
|
192
|
+
set({ topicLoadingId: id }, false, n('updateTopicLoading'));
|
|
193
|
+
},
|
|
194
|
+
refreshTopic: async () => {
|
|
195
|
+
await mutate(get().activeId);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LobeAgentConfig } from '@/types/agent';
|
|
2
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
3
|
+
import { OpenAIChatMessage } from '@/types/openai/chat';
|
|
4
|
+
import { encodeAsync } from '@/utils/tokenizer';
|
|
5
|
+
|
|
6
|
+
export const getMessagesTokenCount = async (messages: OpenAIChatMessage[]) =>
|
|
7
|
+
encodeAsync(messages.map((m) => m.content).join(''));
|
|
8
|
+
|
|
9
|
+
const getSlicedMessagesWithConfig = (
|
|
10
|
+
messages: ChatMessage[],
|
|
11
|
+
config: LobeAgentConfig,
|
|
12
|
+
): ChatMessage[] => {
|
|
13
|
+
// 如果没有开启历史消息数限制,或者限制为 0,则直接返回
|
|
14
|
+
if (!config.enableHistoryCount || !config.historyCount) return messages;
|
|
15
|
+
|
|
16
|
+
// 如果开启了,则返回尾部的N条消息
|
|
17
|
+
return messages.reverse().slice(0, config.historyCount).reverse();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const chatHelpers = {
|
|
21
|
+
getMessagesTokenCount,
|
|
22
|
+
getSlicedMessagesWithConfig,
|
|
23
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
2
|
+
import { ChatTopic } from '@/types/topic';
|
|
3
|
+
|
|
4
|
+
export interface ChatStoreState {
|
|
5
|
+
abortController?: AbortController;
|
|
6
|
+
/**
|
|
7
|
+
* @title 当前活动的会话
|
|
8
|
+
* @description 当前正在编辑或查看的会话
|
|
9
|
+
*/
|
|
10
|
+
activeId: string;
|
|
11
|
+
activeTopicId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated
|
|
14
|
+
*/
|
|
15
|
+
chatLoadingId?: string;
|
|
16
|
+
inputMessage: string;
|
|
17
|
+
isSearchingTopic: boolean;
|
|
18
|
+
messageLoadingIds: [];
|
|
19
|
+
messages: ChatMessage[];
|
|
20
|
+
/**
|
|
21
|
+
* whether messages have fetched
|
|
22
|
+
*/
|
|
23
|
+
messagesInit: boolean;
|
|
24
|
+
|
|
25
|
+
searchTopics: ChatTopic[];
|
|
26
|
+
shareLoading?: boolean;
|
|
27
|
+
topicLoadingId?: string;
|
|
28
|
+
topicRenamingId?: string;
|
|
29
|
+
topicSearchKeywords: string;
|
|
30
|
+
topics: ChatTopic[];
|
|
31
|
+
/**
|
|
32
|
+
* whether topics have fetched
|
|
33
|
+
*/
|
|
34
|
+
topicsInit: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const initialState: ChatStoreState = {
|
|
38
|
+
activeId: 'inbox',
|
|
39
|
+
inputMessage: '',
|
|
40
|
+
isSearchingTopic: false,
|
|
41
|
+
messageLoadingIds: [],
|
|
42
|
+
messages: [],
|
|
43
|
+
messagesInit: false,
|
|
44
|
+
searchTopics: [],
|
|
45
|
+
topicSearchKeywords: '',
|
|
46
|
+
topics: [],
|
|
47
|
+
topicsInit: false,
|
|
48
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ChatMessage, ChatMessageMap } from '@/types/chatMessage';
|
|
2
|
+
|
|
3
|
+
import { MessageDispatch, messagesReducer } from './message';
|
|
4
|
+
|
|
5
|
+
describe('messagesReducer', () => {
|
|
6
|
+
let initialState: ChatMessage[];
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
initialState = [
|
|
10
|
+
{
|
|
11
|
+
id: 'message1',
|
|
12
|
+
content: 'Hello World',
|
|
13
|
+
createdAt: 1629264000000,
|
|
14
|
+
updatedAt: 1629264000000,
|
|
15
|
+
role: 'user',
|
|
16
|
+
meta: {},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'message2',
|
|
20
|
+
content: 'How are you?',
|
|
21
|
+
createdAt: 1629264000000,
|
|
22
|
+
updatedAt: 1629264000000,
|
|
23
|
+
role: 'system',
|
|
24
|
+
meta: {},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('updateMessage', () => {
|
|
30
|
+
it('should update the specified message with the provided value', () => {
|
|
31
|
+
const payload: MessageDispatch = {
|
|
32
|
+
type: 'updateMessage',
|
|
33
|
+
id: 'message1',
|
|
34
|
+
key: 'content',
|
|
35
|
+
value: 'Updated Message',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const newState = messagesReducer(initialState, payload);
|
|
39
|
+
const newMessage = newState.find((i) => i.id === 'message1');
|
|
40
|
+
|
|
41
|
+
expect(newMessage).not.toBeUndefined();
|
|
42
|
+
expect(newMessage?.content).toBe('Updated Message');
|
|
43
|
+
expect(newMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
47
|
+
const payload: MessageDispatch = {
|
|
48
|
+
type: 'updateMessage',
|
|
49
|
+
id: 'nonexistentMessage',
|
|
50
|
+
key: 'content',
|
|
51
|
+
value: 'Updated Message',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const newState = messagesReducer(initialState, payload);
|
|
55
|
+
|
|
56
|
+
expect(newState).toEqual(initialState);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
60
|
+
const payload: MessageDispatch = {
|
|
61
|
+
type: 'updateMessage',
|
|
62
|
+
id: 'nonexistentMessage',
|
|
63
|
+
key: 'content',
|
|
64
|
+
value: 'Updated Message',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const newState = messagesReducer(initialState, payload);
|
|
68
|
+
|
|
69
|
+
expect(newState).toEqual(initialState);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('unimplemented type', () => {
|
|
74
|
+
it('should throw an error when an unimplemented type is provided', () => {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
const payload: MessageDispatch = { type: 'unimplementedType' };
|
|
77
|
+
|
|
78
|
+
expect(() => messagesReducer(initialState, payload)).toThrowError(
|
|
79
|
+
'暂未实现的 type,请检查 reducer',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import isEqual from 'fast-deep-equal';
|
|
2
|
+
import { produce } from 'immer';
|
|
3
|
+
|
|
4
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
5
|
+
import { merge } from '@/utils/merge';
|
|
6
|
+
|
|
7
|
+
interface UpdateMessage {
|
|
8
|
+
id: string;
|
|
9
|
+
key: keyof ChatMessage;
|
|
10
|
+
type: 'updateMessage';
|
|
11
|
+
value: ChatMessage[keyof ChatMessage];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UpdatePluginState {
|
|
15
|
+
id: string;
|
|
16
|
+
key: string;
|
|
17
|
+
type: 'updatePluginState';
|
|
18
|
+
value: any;
|
|
19
|
+
}
|
|
20
|
+
interface UpdateMessageExtra {
|
|
21
|
+
id: string;
|
|
22
|
+
key: string;
|
|
23
|
+
type: 'updateMessageExtra';
|
|
24
|
+
value: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type MessageDispatch = UpdateMessage | UpdatePluginState | UpdateMessageExtra;
|
|
28
|
+
|
|
29
|
+
export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
|
|
30
|
+
switch (payload.type) {
|
|
31
|
+
case 'updateMessage': {
|
|
32
|
+
return produce(state, (draftState) => {
|
|
33
|
+
const { id, key, value } = payload;
|
|
34
|
+
const message = draftState.find((i) => i.id === id);
|
|
35
|
+
if (!message) return;
|
|
36
|
+
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
message[key] = value;
|
|
39
|
+
message.updatedAt = Date.now();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'updateMessageExtra': {
|
|
44
|
+
return produce(state, (draftState) => {
|
|
45
|
+
const { id, key, value } = payload;
|
|
46
|
+
const message = draftState.find((i) => i.id === id);
|
|
47
|
+
if (!message) return;
|
|
48
|
+
|
|
49
|
+
if (!message.extra) {
|
|
50
|
+
message.extra = { [key]: value } as any;
|
|
51
|
+
} else {
|
|
52
|
+
message.extra[key] = value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
message.updateAt = Date.now();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'updatePluginState': {
|
|
60
|
+
return produce(state, (draftState) => {
|
|
61
|
+
const { id, key, value } = payload;
|
|
62
|
+
const message = draftState.find((i) => i.id === id);
|
|
63
|
+
if (!message) return;
|
|
64
|
+
|
|
65
|
+
let newState;
|
|
66
|
+
if (!message.pluginState) {
|
|
67
|
+
newState = { [key]: value } as any;
|
|
68
|
+
} else {
|
|
69
|
+
newState = merge(message.pluginState, { [key]: value });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isEqual(message.pluginState, newState)) return;
|
|
73
|
+
|
|
74
|
+
message.pluginState = newState;
|
|
75
|
+
message.updatedAt = Date.now();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
default: {
|
|
80
|
+
throw new Error('暂未实现的 type,请检查 reducer');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ChatStore, ChatStoreState } from '@/store/chat';
|
|
4
|
+
import { initialState } from '@/store/chat/initialState';
|
|
5
|
+
import { merge } from '@/utils/merge';
|
|
6
|
+
|
|
7
|
+
import { topicSelectors } from '..';
|
|
8
|
+
|
|
9
|
+
const initialStore = initialState as ChatStore;
|
|
10
|
+
|
|
11
|
+
const mockTopics = [
|
|
12
|
+
{ id: 'topic1', name: 'Topic 1' },
|
|
13
|
+
{ id: 'topic2', name: 'Topic 2' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe('topicSelectors', () => {
|
|
17
|
+
describe('currentTopics', () => {
|
|
18
|
+
it('should return an empty array if there are no topics', () => {
|
|
19
|
+
const topics = topicSelectors.currentTopics(initialStore);
|
|
20
|
+
expect(topics).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return all current topics from the store', () => {
|
|
24
|
+
const state = merge(initialStore, { topics: mockTopics });
|
|
25
|
+
|
|
26
|
+
const topics = topicSelectors.currentTopics(state);
|
|
27
|
+
expect(topics).toEqual(mockTopics);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('currentTopicLength', () => {
|
|
32
|
+
it('should return 0 if there are no topics', () => {
|
|
33
|
+
const length = topicSelectors.currentTopicLength(initialStore);
|
|
34
|
+
expect(length).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return the number of current topics', () => {
|
|
38
|
+
const state = merge(initialStore, { topics: mockTopics });
|
|
39
|
+
const length = topicSelectors.currentTopicLength(state);
|
|
40
|
+
expect(length).toBe(mockTopics.length);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|