@lobehub/chat 1.123.3 → 1.124.0
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/changelog/v1.json +18 -0
- package/locales/ar/chat.json +6 -2
- package/locales/ar/editor.json +47 -0
- package/locales/bg-BG/chat.json +6 -2
- package/locales/bg-BG/editor.json +47 -0
- package/locales/de-DE/chat.json +6 -2
- package/locales/de-DE/editor.json +47 -0
- package/locales/en-US/chat.json +6 -2
- package/locales/en-US/editor.json +47 -0
- package/locales/es-ES/chat.json +6 -2
- package/locales/es-ES/editor.json +47 -0
- package/locales/es-ES/models.json +3 -1
- package/locales/fa-IR/chat.json +6 -2
- package/locales/fa-IR/editor.json +47 -0
- package/locales/fr-FR/chat.json +6 -2
- package/locales/fr-FR/editor.json +47 -0
- package/locales/it-IT/chat.json +6 -2
- package/locales/it-IT/editor.json +47 -0
- package/locales/ja-JP/chat.json +6 -2
- package/locales/ja-JP/editor.json +47 -0
- package/locales/ko-KR/chat.json +6 -2
- package/locales/ko-KR/editor.json +47 -0
- package/locales/ko-KR/models.json +3 -1
- package/locales/nl-NL/chat.json +6 -2
- package/locales/nl-NL/editor.json +47 -0
- package/locales/nl-NL/models.json +3 -1
- package/locales/pl-PL/chat.json +6 -2
- package/locales/pl-PL/editor.json +47 -0
- package/locales/pt-BR/chat.json +6 -2
- package/locales/pt-BR/editor.json +47 -0
- package/locales/ru-RU/chat.json +6 -2
- package/locales/ru-RU/editor.json +47 -0
- package/locales/tr-TR/chat.json +6 -2
- package/locales/tr-TR/editor.json +47 -0
- package/locales/vi-VN/chat.json +6 -2
- package/locales/vi-VN/editor.json +47 -0
- package/locales/zh-CN/chat.json +6 -2
- package/locales/zh-CN/editor.json +47 -0
- package/locales/zh-TW/chat.json +6 -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/types/src/index.ts +1 -0
- package/packages/utils/src/index.ts +1 -0
- package/src/app/(backend)/webapi/chat/[provider]/route.ts +1 -1
- package/src/app/(backend)/webapi/chat/vertexai/route.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/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 +81 -68
- 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/libs/trpc/client/lambda.ts +76 -63
- package/src/locales/default/chat.ts +7 -2
- package/src/locales/default/editor.ts +47 -0
- package/src/locales/default/index.ts +2 -0
- package/src/services/aiChat.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +42 -33
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +174 -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/(backend)/webapi/chat/anthropic/route.test.ts +0 -30
- package/src/app/(backend)/webapi/chat/anthropic/route.ts +0 -21
- package/src/app/(backend)/webapi/chat/google/route.test.ts +0 -35
- package/src/app/(backend)/webapi/chat/google/route.ts +0 -25
- package/src/app/(backend)/webapi/chat/groq/route.test.ts +0 -29
- package/src/app/(backend)/webapi/chat/groq/route.ts +0 -21
- package/src/app/(backend)/webapi/chat/openai/route.test.ts +0 -30
- package/src/app/(backend)/webapi/chat/openai/route.ts +0 -26
- 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
@@ -187,18 +187,21 @@ describe('generateAIChatV2 actions', () => {
|
|
187
187
|
await result.current.sendMessage({ message, files });
|
188
188
|
});
|
189
189
|
|
190
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
191
|
-
|
192
|
-
|
193
|
-
|
190
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
191
|
+
{
|
192
|
+
newAssistantMessage: {
|
193
|
+
model: DEFAULT_MODEL,
|
194
|
+
provider: DEFAULT_PROVIDER,
|
195
|
+
},
|
196
|
+
newUserMessage: {
|
197
|
+
content: message,
|
198
|
+
files: files.map((f) => f.id),
|
199
|
+
},
|
200
|
+
sessionId: mockState.activeId,
|
201
|
+
topicId: mockState.activeTopicId,
|
194
202
|
},
|
195
|
-
|
196
|
-
|
197
|
-
files: files.map((f) => f.id),
|
198
|
-
},
|
199
|
-
sessionId: mockState.activeId,
|
200
|
-
topicId: mockState.activeTopicId,
|
201
|
-
});
|
203
|
+
expect.anything(),
|
204
|
+
);
|
202
205
|
expect(result.current.internal_execAgentRuntime).toHaveBeenCalled();
|
203
206
|
});
|
204
207
|
|
@@ -270,18 +273,21 @@ describe('generateAIChatV2 actions', () => {
|
|
270
273
|
await result.current.sendMessage({ message: '', files: [{ id: 'file-1' }] as any });
|
271
274
|
});
|
272
275
|
|
273
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
276
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
277
|
+
{
|
278
|
+
newAssistantMessage: {
|
279
|
+
model: DEFAULT_MODEL,
|
280
|
+
provider: DEFAULT_PROVIDER,
|
281
|
+
},
|
282
|
+
newUserMessage: {
|
283
|
+
content: '',
|
284
|
+
files: ['file-1'],
|
285
|
+
},
|
286
|
+
sessionId: 'session-id',
|
287
|
+
topicId: 'topic-id',
|
281
288
|
},
|
282
|
-
|
283
|
-
|
284
|
-
});
|
289
|
+
expect.anything(),
|
290
|
+
);
|
285
291
|
});
|
286
292
|
|
287
293
|
it('当同时有文件和消息内容时,正确发送消息并关联文件', async () => {
|
@@ -291,18 +297,21 @@ describe('generateAIChatV2 actions', () => {
|
|
291
297
|
await result.current.sendMessage({ message: 'test', files: [{ id: 'file-1' }] as any });
|
292
298
|
});
|
293
299
|
|
294
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
300
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
301
|
+
{
|
302
|
+
newAssistantMessage: {
|
303
|
+
model: DEFAULT_MODEL,
|
304
|
+
provider: DEFAULT_PROVIDER,
|
305
|
+
},
|
306
|
+
newUserMessage: {
|
307
|
+
content: 'test',
|
308
|
+
files: ['file-1'],
|
309
|
+
},
|
310
|
+
sessionId: 'session-id',
|
311
|
+
topicId: 'topic-id',
|
302
312
|
},
|
303
|
-
|
304
|
-
|
305
|
-
});
|
313
|
+
expect.anything(),
|
314
|
+
);
|
306
315
|
});
|
307
316
|
|
308
317
|
it('当 createMessage 抛出错误时,正确处理错误而不影响整个应用', async () => {
|
@@ -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
|
-
|
128
|
+
const jsonState = mainInputEditor?.getJSONState();
|
129
|
+
get().internal_updateSendMessageOperation(
|
130
|
+
operationKey,
|
131
|
+
{ inputSendErrorMsg: undefined, inputEditorTempState: jsonState },
|
132
|
+
'creatingMessage/start',
|
133
|
+
);
|
134
|
+
|
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
|
+
});
|
130
165
|
|
131
|
-
|
132
|
-
|
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,38 @@ 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
|
+
get().mainInputEditor?.setJSONState(editorTempState);
|
264
|
+
}
|
265
|
+
},
|
266
|
+
clearSendMessageError: () => {
|
267
|
+
get().internal_updateSendMessageOperation(
|
268
|
+
{ sessionId: get().activeId, topicId: get().activeTopicId },
|
269
|
+
null,
|
270
|
+
'clearSendMessageError',
|
271
|
+
);
|
272
|
+
},
|
188
273
|
internal_refreshAiChat: ({ topics, messages, sessionId, topicId }) => {
|
189
274
|
set(
|
190
275
|
{
|
@@ -407,4 +492,58 @@ export const generateAIChatV2: StateCreator<
|
|
407
492
|
await get().internal_summaryHistory(historyMessages);
|
408
493
|
}
|
409
494
|
},
|
495
|
+
|
496
|
+
internal_updateSendMessageOperation: (key, value, actionName) => {
|
497
|
+
const operationKey = typeof key === 'string' ? key : messageMapKey(key.sessionId, key.topicId);
|
498
|
+
|
499
|
+
set(
|
500
|
+
produce((draft) => {
|
501
|
+
if (!draft.mainSendMessageOperations[operationKey])
|
502
|
+
draft.mainSendMessageOperations[operationKey] = value;
|
503
|
+
else {
|
504
|
+
if (value === null) {
|
505
|
+
delete draft.mainSendMessageOperations[operationKey];
|
506
|
+
} else {
|
507
|
+
draft.mainSendMessageOperations[operationKey] = {
|
508
|
+
...draft.mainSendMessageOperations[operationKey],
|
509
|
+
...value,
|
510
|
+
};
|
511
|
+
}
|
512
|
+
}
|
513
|
+
}),
|
514
|
+
false,
|
515
|
+
actionName ?? n('updateSendMessageOperation', { operationKey, value }),
|
516
|
+
);
|
517
|
+
},
|
518
|
+
internal_toggleSendMessageOperation: (key, loading: boolean, cancelReason?: string) => {
|
519
|
+
if (loading) {
|
520
|
+
const abortController = new AbortController();
|
521
|
+
|
522
|
+
get().internal_updateSendMessageOperation(
|
523
|
+
key,
|
524
|
+
{ isLoading: true, abortController },
|
525
|
+
n('toggleSendMessageOperation(start)', { key }),
|
526
|
+
);
|
527
|
+
|
528
|
+
return abortController;
|
529
|
+
} else {
|
530
|
+
const operationKey =
|
531
|
+
typeof key === 'string' ? key : messageMapKey(key.sessionId, key.topicId);
|
532
|
+
|
533
|
+
const operation = get().mainSendMessageOperations[operationKey];
|
534
|
+
|
535
|
+
// If cancelReason is provided, abort the operation first
|
536
|
+
if (cancelReason && operation?.isLoading) {
|
537
|
+
operation.abortController?.abort(cancelReason);
|
538
|
+
}
|
539
|
+
|
540
|
+
get().internal_updateSendMessageOperation(
|
541
|
+
key,
|
542
|
+
{ isLoading: false, abortController: null },
|
543
|
+
n('toggleSendMessageOperation(stop)', { key, cancelReason }),
|
544
|
+
);
|
545
|
+
|
546
|
+
return undefined;
|
547
|
+
}
|
548
|
+
},
|
410
549
|
});
|
@@ -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,30 +0,0 @@
|
|
1
|
-
// @vitest-environment edge-runtime
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
3
|
-
|
4
|
-
import { POST as UniverseRoute } from '../[provider]/route';
|
5
|
-
import { POST, preferredRegion, runtime } from './route';
|
6
|
-
|
7
|
-
// 模拟 '../[provider]/route'
|
8
|
-
vi.mock('../[provider]/route', () => ({
|
9
|
-
POST: vi.fn().mockResolvedValue('mocked response'),
|
10
|
-
}));
|
11
|
-
|
12
|
-
describe('Configuration tests', () => {
|
13
|
-
it('should have runtime set to "edge"', () => {
|
14
|
-
expect(runtime).toBe('edge');
|
15
|
-
});
|
16
|
-
|
17
|
-
it('should contain specific regions in preferredRegion', () => {
|
18
|
-
expect(preferredRegion).not.contain(['hk1']);
|
19
|
-
});
|
20
|
-
});
|
21
|
-
|
22
|
-
describe('Anthropic POST function tests', () => {
|
23
|
-
it('should call UniverseRoute with correct parameters', async () => {
|
24
|
-
const mockRequest = new Request('https://example.com', { method: 'POST' });
|
25
|
-
await POST(mockRequest);
|
26
|
-
expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
|
27
|
-
params: Promise.resolve({ provider: 'anthropic' }),
|
28
|
-
});
|
29
|
-
});
|
30
|
-
});
|