@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.
Files changed (149) hide show
  1. package/.env.example +5 -0
  2. package/CHANGELOG.md +58 -0
  3. package/Dockerfile +2 -0
  4. package/Dockerfile.database +2 -0
  5. package/Dockerfile.pglite +2 -0
  6. package/changelog/v1.json +21 -0
  7. package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
  8. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
  9. package/locales/ar/chat.json +8 -2
  10. package/locales/ar/editor.json +47 -0
  11. package/locales/bg-BG/chat.json +8 -2
  12. package/locales/bg-BG/editor.json +47 -0
  13. package/locales/de-DE/chat.json +8 -2
  14. package/locales/de-DE/editor.json +47 -0
  15. package/locales/en-US/chat.json +8 -2
  16. package/locales/en-US/editor.json +47 -0
  17. package/locales/es-ES/chat.json +8 -2
  18. package/locales/es-ES/editor.json +47 -0
  19. package/locales/es-ES/models.json +3 -1
  20. package/locales/fa-IR/chat.json +8 -2
  21. package/locales/fa-IR/editor.json +47 -0
  22. package/locales/fr-FR/chat.json +8 -2
  23. package/locales/fr-FR/editor.json +47 -0
  24. package/locales/it-IT/chat.json +8 -2
  25. package/locales/it-IT/editor.json +47 -0
  26. package/locales/ja-JP/chat.json +8 -2
  27. package/locales/ja-JP/editor.json +47 -0
  28. package/locales/ko-KR/chat.json +8 -2
  29. package/locales/ko-KR/editor.json +47 -0
  30. package/locales/ko-KR/models.json +3 -1
  31. package/locales/nl-NL/chat.json +8 -2
  32. package/locales/nl-NL/editor.json +47 -0
  33. package/locales/nl-NL/models.json +3 -1
  34. package/locales/pl-PL/chat.json +8 -2
  35. package/locales/pl-PL/editor.json +47 -0
  36. package/locales/pt-BR/chat.json +8 -2
  37. package/locales/pt-BR/editor.json +47 -0
  38. package/locales/ru-RU/chat.json +8 -2
  39. package/locales/ru-RU/editor.json +47 -0
  40. package/locales/tr-TR/chat.json +8 -2
  41. package/locales/tr-TR/editor.json +47 -0
  42. package/locales/vi-VN/chat.json +8 -2
  43. package/locales/vi-VN/editor.json +47 -0
  44. package/locales/zh-CN/chat.json +8 -2
  45. package/locales/zh-CN/editor.json +47 -0
  46. package/locales/zh-CN/modelProvider.json +1 -1
  47. package/locales/zh-TW/chat.json +8 -2
  48. package/locales/zh-TW/editor.json +47 -0
  49. package/locales/zh-TW/models.json +3 -1
  50. package/next.config.ts +4 -0
  51. package/package.json +4 -2
  52. package/packages/const/src/layoutTokens.ts +1 -0
  53. package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
  54. package/packages/model-bank/src/aiModels/groq.ts +26 -8
  55. package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
  56. package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
  57. package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
  58. package/packages/model-bank/src/aiModels/novita.ts +40 -9
  59. package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
  60. package/packages/model-bank/src/aiModels/qwen.ts +62 -1
  61. package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
  62. package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
  63. package/packages/model-runtime/src/newapi/index.test.ts +49 -42
  64. package/packages/model-runtime/src/newapi/index.ts +124 -143
  65. package/packages/types/src/index.ts +1 -0
  66. package/packages/utils/src/index.ts +1 -0
  67. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
  68. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
  69. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
  70. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
  71. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
  72. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
  73. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
  74. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
  75. package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
  76. package/src/config/llm.ts +8 -0
  77. package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
  78. package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
  79. package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
  80. package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
  81. package/src/features/ChatInput/ActionBar/config.ts +7 -1
  82. package/src/features/ChatInput/ActionBar/index.tsx +40 -51
  83. package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
  84. package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
  85. package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
  86. package/src/features/ChatInput/Desktop/index.tsx +94 -69
  87. package/src/features/ChatInput/InputEditor/index.tsx +134 -0
  88. package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
  89. package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
  90. package/src/features/ChatInput/Mobile/index.tsx +72 -0
  91. package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
  92. package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
  93. package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
  94. package/src/features/ChatInput/SendArea/index.tsx +36 -0
  95. package/src/features/ChatInput/StoreUpdater.tsx +41 -0
  96. package/src/features/ChatInput/TypoBar/index.tsx +139 -0
  97. package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
  98. package/src/features/ChatInput/index.ts +7 -0
  99. package/src/features/ChatInput/store/action.ts +75 -0
  100. package/src/features/ChatInput/store/index.ts +23 -0
  101. package/src/features/ChatInput/store/initialState.ts +54 -0
  102. package/src/features/ChatInput/store/selectors.ts +5 -0
  103. package/src/features/Conversation/components/BackBottom/style.ts +1 -1
  104. package/src/features/Conversation/components/SkeletonList.tsx +10 -3
  105. package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
  106. package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
  107. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
  108. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
  109. package/src/features/Portal/Thread/Chat/index.tsx +2 -2
  110. package/src/features/Portal/Thread/Header/index.tsx +1 -1
  111. package/src/hooks/useHotkeys/chatScope.ts +5 -3
  112. package/src/layout/GlobalProvider/Editor.tsx +27 -0
  113. package/src/layout/GlobalProvider/Locale.tsx +3 -23
  114. package/src/locales/default/chat.ts +8 -2
  115. package/src/locales/default/editor.ts +47 -0
  116. package/src/locales/default/index.ts +2 -0
  117. package/src/locales/default/modelProvider.ts +1 -1
  118. package/src/services/aiChat.ts +8 -2
  119. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
  120. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
  121. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
  122. package/src/store/chat/slices/aiChat/initialState.ts +19 -0
  123. package/src/store/chat/slices/aiChat/selectors.ts +18 -0
  124. package/src/store/global/action.test.ts +6 -5
  125. package/src/store/global/actions/__tests__/general.test.ts +6 -6
  126. package/src/store/global/actions/workspacePane.ts +6 -0
  127. package/src/store/global/initialState.ts +2 -4
  128. package/src/store/global/selectors/systemStatus.test.ts +1 -2
  129. package/src/store/global/selectors/systemStatus.ts +2 -5
  130. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
  131. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
  132. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
  133. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
  134. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
  135. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
  136. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
  137. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
  138. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
  139. package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
  140. package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
  141. package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
  142. package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
  143. package/src/features/ChatInput/useSend.ts +0 -102
  144. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
  145. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
  146. package/src/libs/trpc/client/types.ts +0 -18
  147. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
  148. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
  149. /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 } = get();
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 { model, provider } = agentSelectors.currentAgentConfig(getAgentStoreState());
123
+ const operationKey = messageMapKey(activeId, activeTopicId);
103
124
 
104
- const data = await aiChatService.sendMessageInServer({
105
- newUserMessage: {
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
- // refresh the total data
123
- get().internal_refreshAiChat({
124
- messages: data.messages,
125
- topics: data.topics,
126
- sessionId: activeId,
127
- topicId: data.topicId,
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
- if (!activeTopicId) {
132
- await get().switchTopic(data.topicId!, true);
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 (!activeTopicId) return;
221
+ if (!data.topicId) return;
165
222
 
166
- const topic = topicSelectors.getTopicById(activeTopicId)(get());
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
- set({ isCreatingMessage: false }, false, 'creatingMessage/stop');
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 = { inputHeight: 200 };
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.inputHeight).toEqual(200);
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
- inputHeight: 300,
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({ inputHeight: 300 });
406
+ expect(hooks.current.data).toEqual({ wideScreen: false });
406
407
  });
407
408
 
408
- expect(result.current.status.inputHeight).toEqual(300);
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({ inputHeight: 200 });
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({ inputHeight: 200 });
48
+ result.current.updateSystemStatus({ wideScreen: false });
49
49
  });
50
50
 
51
- expect(result.current.status.inputHeight).toBe(200);
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({ inputHeight: initialState.status.inputHeight });
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({ inputHeight: 300 });
72
+ result.current.updateSystemStatus({ wideScreen: false });
73
73
  });
74
74
 
75
75
  expect(saveToLocalStorageSpy).toHaveBeenCalledWith(
76
- expect.objectContaining({ inputHeight: 300 }),
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
- threadInputHeight: number;
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
- threadInputHeight: 200,
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.inputHeight(s)).toBe(150);
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 inputHeight = (s: GlobalState) => s.status.inputHeight;
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
- threadInputHeight,
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;