@lobehub/chat 1.123.4 → 1.124.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/.env.example +5 -0
- package/CHANGELOG.md +58 -0
- package/Dockerfile +2 -0
- package/Dockerfile.database +2 -0
- package/Dockerfile.pglite +2 -0
- package/changelog/v1.json +21 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
- package/locales/ar/chat.json +8 -2
- package/locales/ar/editor.json +47 -0
- package/locales/bg-BG/chat.json +8 -2
- package/locales/bg-BG/editor.json +47 -0
- package/locales/de-DE/chat.json +8 -2
- package/locales/de-DE/editor.json +47 -0
- package/locales/en-US/chat.json +8 -2
- package/locales/en-US/editor.json +47 -0
- package/locales/es-ES/chat.json +8 -2
- package/locales/es-ES/editor.json +47 -0
- package/locales/es-ES/models.json +3 -1
- package/locales/fa-IR/chat.json +8 -2
- package/locales/fa-IR/editor.json +47 -0
- package/locales/fr-FR/chat.json +8 -2
- package/locales/fr-FR/editor.json +47 -0
- package/locales/it-IT/chat.json +8 -2
- package/locales/it-IT/editor.json +47 -0
- package/locales/ja-JP/chat.json +8 -2
- package/locales/ja-JP/editor.json +47 -0
- package/locales/ko-KR/chat.json +8 -2
- package/locales/ko-KR/editor.json +47 -0
- package/locales/ko-KR/models.json +3 -1
- package/locales/nl-NL/chat.json +8 -2
- package/locales/nl-NL/editor.json +47 -0
- package/locales/nl-NL/models.json +3 -1
- package/locales/pl-PL/chat.json +8 -2
- package/locales/pl-PL/editor.json +47 -0
- package/locales/pt-BR/chat.json +8 -2
- package/locales/pt-BR/editor.json +47 -0
- package/locales/ru-RU/chat.json +8 -2
- package/locales/ru-RU/editor.json +47 -0
- package/locales/tr-TR/chat.json +8 -2
- package/locales/tr-TR/editor.json +47 -0
- package/locales/vi-VN/chat.json +8 -2
- package/locales/vi-VN/editor.json +47 -0
- package/locales/zh-CN/chat.json +8 -2
- package/locales/zh-CN/editor.json +47 -0
- package/locales/zh-CN/modelProvider.json +1 -1
- package/locales/zh-TW/chat.json +8 -2
- package/locales/zh-TW/editor.json +47 -0
- package/locales/zh-TW/models.json +3 -1
- package/next.config.ts +4 -0
- package/package.json +4 -2
- package/packages/const/src/layoutTokens.ts +1 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
- package/packages/model-bank/src/aiModels/groq.ts +26 -8
- package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
- package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
- package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
- package/packages/model-bank/src/aiModels/novita.ts +40 -9
- package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
- package/packages/model-bank/src/aiModels/qwen.ts +62 -1
- package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
- package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
- package/packages/model-runtime/src/newapi/index.test.ts +49 -42
- package/packages/model-runtime/src/newapi/index.ts +124 -143
- package/packages/types/src/index.ts +1 -0
- package/packages/utils/src/index.ts +1 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
- package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
- package/src/config/llm.ts +8 -0
- package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
- package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
- package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
- package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
- package/src/features/ChatInput/ActionBar/config.ts +7 -1
- package/src/features/ChatInput/ActionBar/index.tsx +40 -51
- package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
- package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
- package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
- package/src/features/ChatInput/Desktop/index.tsx +94 -69
- package/src/features/ChatInput/InputEditor/index.tsx +134 -0
- package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
- package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
- package/src/features/ChatInput/Mobile/index.tsx +72 -0
- package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
- package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
- package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
- package/src/features/ChatInput/SendArea/index.tsx +36 -0
- package/src/features/ChatInput/StoreUpdater.tsx +41 -0
- package/src/features/ChatInput/TypoBar/index.tsx +139 -0
- package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
- package/src/features/ChatInput/index.ts +7 -0
- package/src/features/ChatInput/store/action.ts +75 -0
- package/src/features/ChatInput/store/index.ts +23 -0
- package/src/features/ChatInput/store/initialState.ts +54 -0
- package/src/features/ChatInput/store/selectors.ts +5 -0
- package/src/features/Conversation/components/BackBottom/style.ts +1 -1
- package/src/features/Conversation/components/SkeletonList.tsx +10 -3
- package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
- package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
- package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
- package/src/features/Portal/Thread/Chat/index.tsx +2 -2
- package/src/features/Portal/Thread/Header/index.tsx +1 -1
- package/src/hooks/useHotkeys/chatScope.ts +5 -3
- package/src/layout/GlobalProvider/Editor.tsx +27 -0
- package/src/layout/GlobalProvider/Locale.tsx +3 -23
- package/src/locales/default/chat.ts +8 -2
- package/src/locales/default/editor.ts +47 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/modelProvider.ts +1 -1
- package/src/services/aiChat.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
- package/src/store/chat/slices/aiChat/initialState.ts +19 -0
- package/src/store/chat/slices/aiChat/selectors.ts +18 -0
- package/src/store/global/action.test.ts +6 -5
- package/src/store/global/actions/__tests__/general.test.ts +6 -6
- package/src/store/global/actions/workspacePane.ts +6 -0
- package/src/store/global/initialState.ts +2 -4
- package/src/store/global/selectors/systemStatus.test.ts +1 -2
- package/src/store/global/selectors/systemStatus.ts +2 -5
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
- package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
- package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
- package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
- package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
- package/src/features/ChatInput/useSend.ts +0 -102
- package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
- package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
- package/src/libs/trpc/client/types.ts +0 -18
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -7,9 +7,12 @@ import {
|
|
7
7
|
ChatTopic,
|
8
8
|
MessageSemanticSearchChunk,
|
9
9
|
SendMessageParams,
|
10
|
+
SendMessageServerResponse,
|
10
11
|
TraceNameMap,
|
11
12
|
} from '@lobechat/types';
|
13
|
+
import { TRPCClientError } from '@trpc/client';
|
12
14
|
import { t } from 'i18next';
|
15
|
+
import { produce } from 'immer';
|
13
16
|
import { StateCreator } from 'zustand/vanilla';
|
14
17
|
|
15
18
|
import { aiChatService } from '@/services/aiChat';
|
@@ -18,6 +21,7 @@ import { messageService } from '@/services/message';
|
|
18
21
|
import { getAgentStoreState } from '@/store/agent';
|
19
22
|
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/slices/chat';
|
20
23
|
import { aiModelSelectors, aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
|
24
|
+
import { MainSendMessageOperation } from '@/store/chat/slices/aiChat/initialState';
|
21
25
|
import type { ChatStore } from '@/store/chat/store';
|
22
26
|
import { getSessionStoreState } from '@/store/session';
|
23
27
|
import { WebBrowsingManifest } from '@/tools/web-browsing';
|
@@ -33,6 +37,11 @@ export interface AIGenerateV2Action {
|
|
33
37
|
* Sends a new message to the AI chat system
|
34
38
|
*/
|
35
39
|
sendMessageInServer: (params: SendMessageParams) => Promise<void>;
|
40
|
+
/**
|
41
|
+
* Cancels sendMessageInServer operation for a specific topic/session
|
42
|
+
*/
|
43
|
+
cancelSendMessageInServer: (topicId?: string) => void;
|
44
|
+
clearSendMessageError: () => void;
|
36
45
|
internal_refreshAiChat: (params: {
|
37
46
|
topics?: ChatTopic[];
|
38
47
|
messages: ChatMessage[];
|
@@ -57,6 +66,19 @@ export interface AIGenerateV2Action {
|
|
57
66
|
inPortalThread?: boolean;
|
58
67
|
traceId?: string;
|
59
68
|
}) => Promise<void>;
|
69
|
+
/**
|
70
|
+
* Toggle sendMessageInServer operation state
|
71
|
+
*/
|
72
|
+
internal_toggleSendMessageOperation: (
|
73
|
+
key: string | { sessionId: string; topicId?: string | null },
|
74
|
+
loading: boolean,
|
75
|
+
cancelReason?: string,
|
76
|
+
) => AbortController | undefined;
|
77
|
+
internal_updateSendMessageOperation: (
|
78
|
+
key: string | { sessionId: string; topicId?: string | null },
|
79
|
+
value: Partial<MainSendMessageOperation> | null,
|
80
|
+
actionName?: any,
|
81
|
+
) => void;
|
60
82
|
}
|
61
83
|
|
62
84
|
export const generateAIChatV2: StateCreator<
|
@@ -66,7 +88,8 @@ export const generateAIChatV2: StateCreator<
|
|
66
88
|
AIGenerateV2Action
|
67
89
|
> = (set, get) => ({
|
68
90
|
sendMessageInServer: async ({ message, files, onlyAddUserMessage, isWelcomeQuestion }) => {
|
69
|
-
const { activeTopicId, activeId, activeThreadId, internal_execAgentRuntime } =
|
91
|
+
const { activeTopicId, activeId, activeThreadId, internal_execAgentRuntime, mainInputEditor } =
|
92
|
+
get();
|
70
93
|
if (!activeId) return;
|
71
94
|
|
72
95
|
const fileIdList = files?.map((f) => f.id);
|
@@ -95,44 +118,78 @@ export const generateAIChatV2: StateCreator<
|
|
95
118
|
topicId: activeTopicId,
|
96
119
|
threadId: activeThreadId,
|
97
120
|
});
|
98
|
-
|
99
121
|
get().internal_toggleMessageLoading(true, tempId);
|
100
|
-
set({ isCreatingMessage: true }, false, 'creatingMessage/start');
|
101
122
|
|
102
|
-
const
|
123
|
+
const operationKey = messageMapKey(activeId, activeTopicId);
|
103
124
|
|
104
|
-
|
105
|
-
|
106
|
-
content: message,
|
107
|
-
files: fileIdList,
|
108
|
-
},
|
109
|
-
// if there is activeTopicId,then add topicId to message
|
110
|
-
topicId: activeTopicId,
|
111
|
-
threadId: activeThreadId,
|
112
|
-
newTopic: !activeTopicId
|
113
|
-
? {
|
114
|
-
topicMessageIds: messages.map((m) => m.id),
|
115
|
-
title: t('defaultTitle', { ns: 'topic' }),
|
116
|
-
}
|
117
|
-
: undefined,
|
118
|
-
sessionId: activeId === INBOX_SESSION_ID ? undefined : activeId,
|
119
|
-
newAssistantMessage: { model, provider: provider! },
|
120
|
-
});
|
125
|
+
// Start tracking sendMessageInServer operation with AbortController
|
126
|
+
const abortController = get().internal_toggleSendMessageOperation(operationKey, true)!;
|
121
127
|
|
122
|
-
|
123
|
-
get().
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
});
|
129
|
-
get().internal_dispatchMessage({ type: 'deleteMessage', id: tempId });
|
128
|
+
const jsonState = mainInputEditor?.getJSONState();
|
129
|
+
get().internal_updateSendMessageOperation(
|
130
|
+
operationKey,
|
131
|
+
{ inputSendErrorMsg: undefined, inputEditorTempState: jsonState },
|
132
|
+
'creatingMessage/start',
|
133
|
+
);
|
130
134
|
|
131
|
-
|
132
|
-
|
135
|
+
let data: SendMessageServerResponse | undefined;
|
136
|
+
try {
|
137
|
+
const { model, provider } = agentSelectors.currentAgentConfig(getAgentStoreState());
|
138
|
+
data = await aiChatService.sendMessageInServer(
|
139
|
+
{
|
140
|
+
newUserMessage: {
|
141
|
+
content: message,
|
142
|
+
files: fileIdList,
|
143
|
+
},
|
144
|
+
// if there is activeTopicId,then add topicId to message
|
145
|
+
topicId: activeTopicId,
|
146
|
+
threadId: activeThreadId,
|
147
|
+
newTopic: !activeTopicId
|
148
|
+
? {
|
149
|
+
topicMessageIds: messages.map((m) => m.id),
|
150
|
+
title: t('defaultTitle', { ns: 'topic' }),
|
151
|
+
}
|
152
|
+
: undefined,
|
153
|
+
sessionId: activeId === INBOX_SESSION_ID ? undefined : activeId,
|
154
|
+
newAssistantMessage: { model, provider: provider! },
|
155
|
+
},
|
156
|
+
abortController,
|
157
|
+
);
|
158
|
+
// refresh the total data
|
159
|
+
get().internal_refreshAiChat({
|
160
|
+
messages: data.messages,
|
161
|
+
topics: data.topics,
|
162
|
+
sessionId: activeId,
|
163
|
+
topicId: data.topicId,
|
164
|
+
});
|
165
|
+
|
166
|
+
if (!activeTopicId) {
|
167
|
+
await get().switchTopic(data.topicId!, true);
|
168
|
+
}
|
169
|
+
} catch (e) {
|
170
|
+
if (e instanceof TRPCClientError) {
|
171
|
+
const isAbort = e.message.includes('aborted') || e.name === 'AbortError';
|
172
|
+
// Check if error is due to cancellation
|
173
|
+
if (!isAbort) {
|
174
|
+
get().internal_updateSendMessageOperation(operationKey, { inputSendErrorMsg: e.message });
|
175
|
+
get().mainInputEditor?.setJSONState(jsonState);
|
176
|
+
}
|
177
|
+
}
|
178
|
+
} finally {
|
179
|
+
// Stop tracking sendMessageInServer operation
|
180
|
+
get().internal_toggleSendMessageOperation(operationKey, false);
|
133
181
|
}
|
134
182
|
|
183
|
+
// remove temporally message
|
184
|
+
get().internal_dispatchMessage({ type: 'deleteMessage', id: tempId });
|
135
185
|
get().internal_toggleMessageLoading(false, tempId);
|
186
|
+
get().internal_updateSendMessageOperation(
|
187
|
+
operationKey,
|
188
|
+
{ inputEditorTempState: null },
|
189
|
+
'creatingMessage/finished',
|
190
|
+
);
|
191
|
+
|
192
|
+
if (!data) return;
|
136
193
|
|
137
194
|
// update assistant update to make it rerank
|
138
195
|
getSessionStoreState().triggerSessionUpdate(get().activeId);
|
@@ -143,6 +200,7 @@ export const generateAIChatV2: StateCreator<
|
|
143
200
|
.activeBaseChats(get())
|
144
201
|
.filter((item) => item.id !== data.assistantMessageId);
|
145
202
|
|
203
|
+
if (data.topicId) get().internal_updateTopicLoading(data.topicId, true);
|
146
204
|
try {
|
147
205
|
await internal_execAgentRuntime({
|
148
206
|
messages: baseMessages,
|
@@ -152,7 +210,6 @@ export const generateAIChatV2: StateCreator<
|
|
152
210
|
ragQuery: get().internal_shouldUseRAG() ? message : undefined,
|
153
211
|
threadId: activeThreadId,
|
154
212
|
});
|
155
|
-
set({ isCreatingMessage: false }, false, 'creatingMessage/stop');
|
156
213
|
|
157
214
|
const summaryTitle = async () => {
|
158
215
|
// check activeTopic and then auto update topic title
|
@@ -161,9 +218,9 @@ export const generateAIChatV2: StateCreator<
|
|
161
218
|
return;
|
162
219
|
}
|
163
220
|
|
164
|
-
if (!
|
221
|
+
if (!data.topicId) return;
|
165
222
|
|
166
|
-
const topic = topicSelectors.getTopicById(
|
223
|
+
const topic = topicSelectors.getTopicById(data.topicId)(get());
|
167
224
|
|
168
225
|
if (topic && !topic.title) {
|
169
226
|
const chats = chatSelectors.getBaseChatsByKey(messageMapKey(activeId, topic.id))(get());
|
@@ -181,10 +238,39 @@ export const generateAIChatV2: StateCreator<
|
|
181
238
|
await Promise.all([summaryTitle(), addFilesToAgent()]);
|
182
239
|
} catch (e) {
|
183
240
|
console.error(e);
|
184
|
-
|
241
|
+
} finally {
|
242
|
+
if (data.topicId) get().internal_updateTopicLoading(data.topicId, false);
|
185
243
|
}
|
186
244
|
},
|
187
245
|
|
246
|
+
cancelSendMessageInServer: (topicId?: string) => {
|
247
|
+
const { activeId, activeTopicId } = get();
|
248
|
+
|
249
|
+
// Determine which operation to cancel
|
250
|
+
const targetTopicId = topicId ?? activeTopicId;
|
251
|
+
const operationKey = messageMapKey(activeId, targetTopicId);
|
252
|
+
|
253
|
+
// Cancel the specific operation
|
254
|
+
get().internal_toggleSendMessageOperation(
|
255
|
+
operationKey,
|
256
|
+
false,
|
257
|
+
'User cancelled sendMessageInServer operation',
|
258
|
+
);
|
259
|
+
|
260
|
+
// Only clear creating message state if it's the active session
|
261
|
+
if (operationKey === messageMapKey(activeId, activeTopicId)) {
|
262
|
+
const editorTempState = get().mainSendMessageOperations[operationKey]?.inputEditorTempState;
|
263
|
+
|
264
|
+
if (editorTempState) get().mainInputEditor?.setJSONState(editorTempState);
|
265
|
+
}
|
266
|
+
},
|
267
|
+
clearSendMessageError: () => {
|
268
|
+
get().internal_updateSendMessageOperation(
|
269
|
+
{ sessionId: get().activeId, topicId: get().activeTopicId },
|
270
|
+
null,
|
271
|
+
'clearSendMessageError',
|
272
|
+
);
|
273
|
+
},
|
188
274
|
internal_refreshAiChat: ({ topics, messages, sessionId, topicId }) => {
|
189
275
|
set(
|
190
276
|
{
|
@@ -407,4 +493,58 @@ export const generateAIChatV2: StateCreator<
|
|
407
493
|
await get().internal_summaryHistory(historyMessages);
|
408
494
|
}
|
409
495
|
},
|
496
|
+
|
497
|
+
internal_updateSendMessageOperation: (key, value, actionName) => {
|
498
|
+
const operationKey = typeof key === 'string' ? key : messageMapKey(key.sessionId, key.topicId);
|
499
|
+
|
500
|
+
set(
|
501
|
+
produce((draft) => {
|
502
|
+
if (!draft.mainSendMessageOperations[operationKey])
|
503
|
+
draft.mainSendMessageOperations[operationKey] = value;
|
504
|
+
else {
|
505
|
+
if (value === null) {
|
506
|
+
delete draft.mainSendMessageOperations[operationKey];
|
507
|
+
} else {
|
508
|
+
draft.mainSendMessageOperations[operationKey] = {
|
509
|
+
...draft.mainSendMessageOperations[operationKey],
|
510
|
+
...value,
|
511
|
+
};
|
512
|
+
}
|
513
|
+
}
|
514
|
+
}),
|
515
|
+
false,
|
516
|
+
actionName ?? n('updateSendMessageOperation', { operationKey, value }),
|
517
|
+
);
|
518
|
+
},
|
519
|
+
internal_toggleSendMessageOperation: (key, loading: boolean, cancelReason?: string) => {
|
520
|
+
if (loading) {
|
521
|
+
const abortController = new AbortController();
|
522
|
+
|
523
|
+
get().internal_updateSendMessageOperation(
|
524
|
+
key,
|
525
|
+
{ isLoading: true, abortController },
|
526
|
+
n('toggleSendMessageOperation(start)', { key }),
|
527
|
+
);
|
528
|
+
|
529
|
+
return abortController;
|
530
|
+
} else {
|
531
|
+
const operationKey =
|
532
|
+
typeof key === 'string' ? key : messageMapKey(key.sessionId, key.topicId);
|
533
|
+
|
534
|
+
const operation = get().mainSendMessageOperations[operationKey];
|
535
|
+
|
536
|
+
// If cancelReason is provided, abort the operation first
|
537
|
+
if (cancelReason && operation?.isLoading) {
|
538
|
+
operation.abortController?.abort(cancelReason);
|
539
|
+
}
|
540
|
+
|
541
|
+
get().internal_updateSendMessageOperation(
|
542
|
+
key,
|
543
|
+
{ isLoading: false, abortController: null },
|
544
|
+
n('toggleSendMessageOperation(stop)', { key, cancelReason }),
|
545
|
+
);
|
546
|
+
|
547
|
+
return undefined;
|
548
|
+
}
|
549
|
+
},
|
410
550
|
});
|
@@ -1,3 +1,12 @@
|
|
1
|
+
import type { ChatInputEditor } from '@/features/ChatInput';
|
2
|
+
|
3
|
+
export interface MainSendMessageOperation {
|
4
|
+
abortController?: AbortController | null;
|
5
|
+
inputEditorTempState?: any | null;
|
6
|
+
inputSendErrorMsg?: string;
|
7
|
+
isLoading: boolean;
|
8
|
+
}
|
9
|
+
|
1
10
|
export interface ChatAIChatState {
|
2
11
|
/**
|
3
12
|
* is the AI message is generating
|
@@ -6,6 +15,12 @@ export interface ChatAIChatState {
|
|
6
15
|
chatLoadingIdsAbortController?: AbortController;
|
7
16
|
inputFiles: File[];
|
8
17
|
inputMessage: string;
|
18
|
+
mainInputEditor: ChatInputEditor | null;
|
19
|
+
/**
|
20
|
+
* sendMessageInServer operations map, keyed by sessionId|topicId
|
21
|
+
* Contains both loading state and AbortController
|
22
|
+
*/
|
23
|
+
mainSendMessageOperations: Record<string, MainSendMessageOperation>;
|
9
24
|
messageInToolsCallingIds: string[];
|
10
25
|
/**
|
11
26
|
* is the message is in RAG flow
|
@@ -17,6 +32,7 @@ export interface ChatAIChatState {
|
|
17
32
|
*/
|
18
33
|
reasoningLoadingIds: string[];
|
19
34
|
searchWorkflowLoadingIds: string[];
|
35
|
+
threadInputEditor: ChatInputEditor | null;
|
20
36
|
/**
|
21
37
|
* the tool calling stream ids
|
22
38
|
*/
|
@@ -27,10 +43,13 @@ export const initialAiChatState: ChatAIChatState = {
|
|
27
43
|
chatLoadingIds: [],
|
28
44
|
inputFiles: [],
|
29
45
|
inputMessage: '',
|
46
|
+
mainInputEditor: null,
|
47
|
+
mainSendMessageOperations: {},
|
30
48
|
messageInToolsCallingIds: [],
|
31
49
|
messageRAGLoadingIds: [],
|
32
50
|
pluginApiLoadingIds: [],
|
33
51
|
reasoningLoadingIds: [],
|
34
52
|
searchWorkflowLoadingIds: [],
|
53
|
+
threadInputEditor: null,
|
35
54
|
toolCallingStreamIds: {},
|
36
55
|
};
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
2
|
+
|
1
3
|
import type { ChatStoreState } from '../../initialState';
|
2
4
|
|
3
5
|
const isMessageInReasoning = (id: string) => (s: ChatStoreState) =>
|
@@ -9,8 +11,24 @@ const isMessageInSearchWorkflow = (id: string) => (s: ChatStoreState) =>
|
|
9
11
|
const isIntentUnderstanding = (id: string) => (s: ChatStoreState) =>
|
10
12
|
isMessageInSearchWorkflow(id)(s);
|
11
13
|
|
14
|
+
const isCurrentSendMessageLoading = (s: ChatStoreState) => {
|
15
|
+
const operationKey = messageMapKey(s.activeId, s.activeTopicId);
|
16
|
+
return s.mainSendMessageOperations[operationKey]?.isLoading || false;
|
17
|
+
};
|
18
|
+
|
19
|
+
const isCurrentSendMessageError = (s: ChatStoreState) => {
|
20
|
+
const operationKey = messageMapKey(s.activeId, s.activeTopicId);
|
21
|
+
return s.mainSendMessageOperations[operationKey]?.inputSendErrorMsg;
|
22
|
+
};
|
23
|
+
|
24
|
+
const isSendMessageLoadingForTopic = (topicKey: string) => (s: ChatStoreState) =>
|
25
|
+
s.mainSendMessageOperations[topicKey]?.isLoading ?? false;
|
26
|
+
|
12
27
|
export const aiChatSelectors = {
|
28
|
+
isCurrentSendMessageError,
|
29
|
+
isCurrentSendMessageLoading,
|
13
30
|
isIntentUnderstanding,
|
14
31
|
isMessageInReasoning,
|
15
32
|
isMessageInSearchWorkflow,
|
33
|
+
isSendMessageLoadingForTopic,
|
16
34
|
};
|
@@ -252,13 +252,14 @@ describe('createPreferenceSlice', () => {
|
|
252
252
|
describe('updatePreference', () => {
|
253
253
|
it('should update status', () => {
|
254
254
|
const { result } = renderHook(() => useGlobalStore());
|
255
|
-
const status = {
|
255
|
+
const status = { wideScreen: false };
|
256
256
|
|
257
257
|
act(() => {
|
258
|
+
useGlobalStore.setState({ isStatusInit: true });
|
258
259
|
result.current.updateSystemStatus(status);
|
259
260
|
});
|
260
261
|
|
261
|
-
expect(result.current.status.
|
262
|
+
expect(result.current.status.wideScreen).toEqual(false);
|
262
263
|
});
|
263
264
|
});
|
264
265
|
|
@@ -394,7 +395,7 @@ describe('createPreferenceSlice', () => {
|
|
394
395
|
it('should update with data', async () => {
|
395
396
|
const { result } = renderHook(() => useGlobalStore());
|
396
397
|
vi.spyOn(useGlobalStore.getState().statusStorage, 'getFromLocalStorage').mockReturnValueOnce({
|
397
|
-
|
398
|
+
wideScreen: false,
|
398
399
|
} as any);
|
399
400
|
|
400
401
|
const { result: hooks } = renderHook(() => result.current.useInitSystemStatus(), {
|
@@ -402,10 +403,10 @@ describe('createPreferenceSlice', () => {
|
|
402
403
|
});
|
403
404
|
|
404
405
|
await waitFor(() => {
|
405
|
-
expect(hooks.current.data).toEqual({
|
406
|
+
expect(hooks.current.data).toEqual({ wideScreen: false });
|
406
407
|
});
|
407
408
|
|
408
|
-
expect(result.current.status.
|
409
|
+
expect(result.current.status.wideScreen).toEqual(false);
|
409
410
|
});
|
410
411
|
});
|
411
412
|
|
@@ -34,7 +34,7 @@ describe('generalActionSlice', () => {
|
|
34
34
|
const { result } = renderHook(() => useGlobalStore());
|
35
35
|
|
36
36
|
act(() => {
|
37
|
-
result.current.updateSystemStatus({
|
37
|
+
result.current.updateSystemStatus({ wideScreen: false });
|
38
38
|
});
|
39
39
|
|
40
40
|
expect(result.current.status).toEqual(initialState.status);
|
@@ -45,10 +45,10 @@ describe('generalActionSlice', () => {
|
|
45
45
|
|
46
46
|
act(() => {
|
47
47
|
useGlobalStore.setState({ isStatusInit: true });
|
48
|
-
result.current.updateSystemStatus({
|
48
|
+
result.current.updateSystemStatus({ wideScreen: false });
|
49
49
|
});
|
50
50
|
|
51
|
-
expect(result.current.status.
|
51
|
+
expect(result.current.status.wideScreen).toBe(false);
|
52
52
|
});
|
53
53
|
|
54
54
|
it('should not update if new status equals current status', () => {
|
@@ -57,7 +57,7 @@ describe('generalActionSlice', () => {
|
|
57
57
|
|
58
58
|
act(() => {
|
59
59
|
useGlobalStore.setState({ isStatusInit: true });
|
60
|
-
result.current.updateSystemStatus({
|
60
|
+
result.current.updateSystemStatus({ wideScreen: initialState.status.wideScreen });
|
61
61
|
});
|
62
62
|
|
63
63
|
expect(saveToLocalStorageSpy).not.toHaveBeenCalled();
|
@@ -69,11 +69,11 @@ describe('generalActionSlice', () => {
|
|
69
69
|
|
70
70
|
act(() => {
|
71
71
|
useGlobalStore.setState({ isStatusInit: true });
|
72
|
-
result.current.updateSystemStatus({
|
72
|
+
result.current.updateSystemStatus({ wideScreen: false });
|
73
73
|
});
|
74
74
|
|
75
75
|
expect(saveToLocalStorageSpy).toHaveBeenCalledWith(
|
76
|
-
expect.objectContaining({
|
76
|
+
expect.objectContaining({ wideScreen: false }),
|
77
77
|
);
|
78
78
|
});
|
79
79
|
|
@@ -16,6 +16,7 @@ export interface GlobalWorkspacePaneAction {
|
|
16
16
|
toggleMobilePortal: (visible?: boolean) => void;
|
17
17
|
toggleMobileTopic: (visible?: boolean) => void;
|
18
18
|
toggleSystemRole: (visible?: boolean) => void;
|
19
|
+
toggleWideScreen: (enable?: boolean) => void;
|
19
20
|
toggleZenMode: () => void;
|
20
21
|
}
|
21
22
|
|
@@ -80,6 +81,11 @@ export const globalWorkspaceSlice: StateCreator<
|
|
80
81
|
|
81
82
|
get().updateSystemStatus({ showSystemRole }, n('toggleMobileTopic', newValue));
|
82
83
|
},
|
84
|
+
toggleWideScreen: (newValue) => {
|
85
|
+
const wideScreen = typeof newValue === 'boolean' ? newValue : !get().status.wideScreen;
|
86
|
+
|
87
|
+
get().updateSystemStatus({ wideScreen }, n('toggleWideScreen', newValue));
|
88
|
+
},
|
83
89
|
toggleZenMode: () => {
|
84
90
|
const { status } = get();
|
85
91
|
const nextZenMode = !status.zenMode;
|
@@ -55,7 +55,6 @@ export interface SystemStatus {
|
|
55
55
|
hideThreadLimitAlert?: boolean;
|
56
56
|
imagePanelWidth: number;
|
57
57
|
imageTopicPanelWidth?: number;
|
58
|
-
inputHeight: number;
|
59
58
|
/**
|
60
59
|
* 应用初始化时不启用 PGLite,只有当用户手动开启时才启用
|
61
60
|
*/
|
@@ -79,7 +78,7 @@ export interface SystemStatus {
|
|
79
78
|
* theme mode
|
80
79
|
*/
|
81
80
|
themeMode?: ThemeMode;
|
82
|
-
|
81
|
+
wideScreen?: boolean;
|
83
82
|
zenMode?: boolean;
|
84
83
|
}
|
85
84
|
|
@@ -114,7 +113,6 @@ export const INITIAL_STATUS = {
|
|
114
113
|
hideThreadLimitAlert: false,
|
115
114
|
imagePanelWidth: 320,
|
116
115
|
imageTopicPanelWidth: 80,
|
117
|
-
inputHeight: 200,
|
118
116
|
mobileShowTopic: false,
|
119
117
|
portalWidth: 400,
|
120
118
|
sessionsWidth: 320,
|
@@ -127,7 +125,7 @@ export const INITIAL_STATUS = {
|
|
127
125
|
showSystemRole: false,
|
128
126
|
systemRoleExpandedMap: {},
|
129
127
|
themeMode: 'auto',
|
130
|
-
|
128
|
+
wideScreen: true,
|
131
129
|
zenMode: false,
|
132
130
|
} satisfies SystemStatus;
|
133
131
|
|
@@ -69,8 +69,7 @@ describe('systemStatusSelectors', () => {
|
|
69
69
|
expect(systemStatusSelectors.sessionWidth(s)).toBe(300);
|
70
70
|
expect(systemStatusSelectors.portalWidth(s)).toBe(500);
|
71
71
|
expect(systemStatusSelectors.filePanelWidth(s)).toBe(400);
|
72
|
-
expect(systemStatusSelectors.
|
73
|
-
expect(systemStatusSelectors.threadInputHeight(s)).toBe(100);
|
72
|
+
expect(systemStatusSelectors.wideScreen(s)).toBe(true);
|
74
73
|
});
|
75
74
|
|
76
75
|
it('should handle zen mode effects', () => {
|
@@ -28,9 +28,7 @@ const portalWidth = (s: GlobalState) => s.status.portalWidth || 400;
|
|
28
28
|
const filePanelWidth = (s: GlobalState) => s.status.filePanelWidth;
|
29
29
|
const imagePanelWidth = (s: GlobalState) => s.status.imagePanelWidth;
|
30
30
|
const imageTopicPanelWidth = (s: GlobalState) => s.status.imageTopicPanelWidth;
|
31
|
-
const
|
32
|
-
const threadInputHeight = (s: GlobalState) => s.status.threadInputHeight;
|
33
|
-
|
31
|
+
const wideScreen = (s: GlobalState) => s.status.wideScreen;
|
34
32
|
const isPgliteNotEnabled = (s: GlobalState) =>
|
35
33
|
isUsePgliteDB && !isServerMode && s.isStatusInit && !s.status.isEnablePglite;
|
36
34
|
|
@@ -69,7 +67,6 @@ export const systemStatusSelectors = {
|
|
69
67
|
imagePanelWidth,
|
70
68
|
imageTopicPanelWidth,
|
71
69
|
inZenMode,
|
72
|
-
inputHeight,
|
73
70
|
isDBInited,
|
74
71
|
isPgliteInited,
|
75
72
|
isPgliteNotEnabled,
|
@@ -90,5 +87,5 @@ export const systemStatusSelectors = {
|
|
90
87
|
showSystemRole,
|
91
88
|
systemStatus,
|
92
89
|
themeMode,
|
93
|
-
|
90
|
+
wideScreen,
|
94
91
|
};
|
@@ -1,104 +0,0 @@
|
|
1
|
-
import { Button, Dropdown, Hotkey, Icon } from '@lobehub/ui';
|
2
|
-
import { createStyles } from 'antd-style';
|
3
|
-
import { BotMessageSquare, LucideCheck, LucideChevronDown, MessageSquarePlus } from 'lucide-react';
|
4
|
-
import { memo } from 'react';
|
5
|
-
import { useTranslation } from 'react-i18next';
|
6
|
-
import { Flexbox } from 'react-layout-kit';
|
7
|
-
|
8
|
-
import { useSendMessage } from '@/features/ChatInput/useSend';
|
9
|
-
import { useChatStore } from '@/store/chat';
|
10
|
-
import { useUserStore } from '@/store/user';
|
11
|
-
import { preferenceSelectors, settingsSelectors } from '@/store/user/selectors';
|
12
|
-
import { HotkeyEnum } from '@/types/hotkey';
|
13
|
-
|
14
|
-
const useStyles = createStyles(({ css, prefixCls }) => {
|
15
|
-
return {
|
16
|
-
arrow: css`
|
17
|
-
&.${prefixCls}-btn.${prefixCls}-btn-icon-only {
|
18
|
-
width: 28px;
|
19
|
-
}
|
20
|
-
`,
|
21
|
-
};
|
22
|
-
});
|
23
|
-
|
24
|
-
interface SendMoreProps {
|
25
|
-
disabled?: boolean;
|
26
|
-
isMac?: boolean;
|
27
|
-
}
|
28
|
-
|
29
|
-
const SendMore = memo<SendMoreProps>(({ disabled, isMac }) => {
|
30
|
-
const { t } = useTranslation('chat');
|
31
|
-
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.AddUserMessage));
|
32
|
-
const { styles } = useStyles();
|
33
|
-
|
34
|
-
const [useCmdEnterToSend, updatePreference] = useUserStore((s) => [
|
35
|
-
preferenceSelectors.useCmdEnterToSend(s),
|
36
|
-
s.updatePreference,
|
37
|
-
]);
|
38
|
-
const addAIMessage = useChatStore((s) => s.addAIMessage);
|
39
|
-
|
40
|
-
const { send: sendMessage } = useSendMessage();
|
41
|
-
|
42
|
-
return (
|
43
|
-
<Dropdown
|
44
|
-
disabled={disabled}
|
45
|
-
menu={{
|
46
|
-
items: [
|
47
|
-
{
|
48
|
-
icon: !useCmdEnterToSend ? <Icon icon={LucideCheck} /> : <div />,
|
49
|
-
key: 'sendWithEnter',
|
50
|
-
label: t('input.sendWithEnter'),
|
51
|
-
onClick: () => {
|
52
|
-
updatePreference({ useCmdEnterToSend: false });
|
53
|
-
},
|
54
|
-
},
|
55
|
-
{
|
56
|
-
icon: useCmdEnterToSend ? <Icon icon={LucideCheck} /> : <div />,
|
57
|
-
key: 'sendWithCmdEnter',
|
58
|
-
label: t('input.sendWithCmdEnter', {
|
59
|
-
meta: typeof isMac === 'boolean' ? (isMac ? '⌘' : 'Ctrl') : '…',
|
60
|
-
}),
|
61
|
-
onClick: () => {
|
62
|
-
updatePreference({ useCmdEnterToSend: true });
|
63
|
-
},
|
64
|
-
},
|
65
|
-
{ type: 'divider' },
|
66
|
-
{
|
67
|
-
icon: <Icon icon={BotMessageSquare} />,
|
68
|
-
key: 'addAi',
|
69
|
-
label: t('input.addAi'),
|
70
|
-
onClick: () => {
|
71
|
-
addAIMessage();
|
72
|
-
},
|
73
|
-
},
|
74
|
-
{
|
75
|
-
icon: <Icon icon={MessageSquarePlus} />,
|
76
|
-
key: 'addUser',
|
77
|
-
label: (
|
78
|
-
<Flexbox align={'center'} gap={24} horizontal>
|
79
|
-
{t('input.addUser')}
|
80
|
-
<Hotkey keys={hotkey} />
|
81
|
-
</Flexbox>
|
82
|
-
),
|
83
|
-
onClick: () => {
|
84
|
-
sendMessage({ onlyAddUserMessage: true });
|
85
|
-
},
|
86
|
-
},
|
87
|
-
],
|
88
|
-
}}
|
89
|
-
placement={'topRight'}
|
90
|
-
trigger={['hover']}
|
91
|
-
>
|
92
|
-
<Button
|
93
|
-
aria-label={t('input.more')}
|
94
|
-
className={styles.arrow}
|
95
|
-
icon={LucideChevronDown}
|
96
|
-
type={'primary'}
|
97
|
-
/>
|
98
|
-
</Dropdown>
|
99
|
-
);
|
100
|
-
});
|
101
|
-
|
102
|
-
SendMore.displayName = 'SendMore';
|
103
|
-
|
104
|
-
export default SendMore;
|