@lobehub/chat 0.147.21 → 0.147.22
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 +17 -0
- package/package.json +2 -2
- package/public/favicon-32x32.ico +0 -0
- package/public/favicon.ico +0 -0
- package/public/icons/apple-touch-icon.png +0 -0
- package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +2 -2
- package/src/app/metadata.ts +3 -5
- package/src/features/ChatInput/Topic/index.tsx +6 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +8 -3
- package/src/libs/swr/index.ts +9 -0
- package/src/store/chat/slices/message/action.ts +80 -42
- package/src/store/chat/slices/message/initialState.ts +1 -1
- package/src/store/chat/slices/message/reducer.ts +32 -1
- package/src/store/chat/slices/topic/action.test.ts +25 -2
- package/src/store/chat/slices/topic/action.ts +24 -7
- package/src/store/chat/slices/topic/reducer.test.ts +141 -0
- package/src/store/chat/slices/topic/reducer.ts +67 -0
- package/src/store/session/slices/session/action.ts +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.147.22](https://github.com/lobehub/lobe-chat/compare/v0.147.21...v0.147.22)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-04-19**</sup>
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
<details>
|
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
13
|
+
|
|
14
|
+
</details>
|
|
15
|
+
|
|
16
|
+
<div align="right">
|
|
17
|
+
|
|
18
|
+
[](#readme-top)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
5
22
|
### [Version 0.147.21](https://github.com/lobehub/lobe-chat/compare/v0.147.20...v0.147.21)
|
|
6
23
|
|
|
7
24
|
<sup>Released on **2024-04-19**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.147.
|
|
3
|
+
"version": "0.147.22",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@aws-sdk/client-bedrock-runtime": "^3.549.0",
|
|
87
87
|
"@azure/openai": "^1.0.0-beta.12",
|
|
88
88
|
"@cfworker/json-schema": "^1.12.8",
|
|
89
|
-
"@google/generative-ai": "^0.
|
|
89
|
+
"@google/generative-ai": "^0.7.0",
|
|
90
90
|
"@icons-pack/react-simple-icons": "^9.4.0",
|
|
91
91
|
"@lobehub/chat-plugin-sdk": "latest",
|
|
92
92
|
"@lobehub/chat-plugins-gateway": "latest",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -114,8 +114,8 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
|
|
114
114
|
modal.confirm({
|
|
115
115
|
centered: true,
|
|
116
116
|
okButtonProps: { danger: true },
|
|
117
|
-
onOk: () => {
|
|
118
|
-
removeTopic(id);
|
|
117
|
+
onOk: async () => {
|
|
118
|
+
await removeTopic(id);
|
|
119
119
|
},
|
|
120
120
|
title: t('topic.confirmRemoveTopic', { ns: 'chat' }),
|
|
121
121
|
});
|
package/src/app/metadata.ts
CHANGED
|
@@ -22,11 +22,9 @@ const metadata: Metadata = {
|
|
|
22
22
|
},
|
|
23
23
|
description,
|
|
24
24
|
icons: {
|
|
25
|
-
apple:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
shortcut:
|
|
29
|
-
'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/favicon.ico',
|
|
25
|
+
apple:'icons/apple-touch-icon.png',
|
|
26
|
+
icon:'favicon.ico',
|
|
27
|
+
shortcut:'favicon-32x32.ico',
|
|
30
28
|
},
|
|
31
29
|
manifest: noManifest ? undefined : '/manifest.json',
|
|
32
30
|
metadataBase: new URL(SITE_URL),
|
|
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
7
7
|
|
|
8
8
|
import HotKeys from '@/components/HotKeys';
|
|
9
9
|
import { PREFIX_KEY, SAVE_TOPIC_KEY } from '@/const/hotkeys';
|
|
10
|
+
import { useActionSWR } from '@/libs/swr';
|
|
10
11
|
import { useChatStore } from '@/store/chat';
|
|
11
12
|
|
|
12
13
|
const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
@@ -16,20 +17,23 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
|
16
17
|
s.openNewTopicOrSaveTopic,
|
|
17
18
|
]);
|
|
18
19
|
|
|
20
|
+
const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
|
|
21
|
+
|
|
19
22
|
const icon = hasTopic ? LucideMessageSquarePlus : LucideGalleryVerticalEnd;
|
|
20
23
|
const Render = mobile ? ActionIcon : Button;
|
|
21
24
|
const iconRender: any = mobile ? icon : <Icon icon={icon} />;
|
|
22
25
|
const desc = t(hasTopic ? 'topic.openNewTopic' : 'topic.saveCurrentMessages');
|
|
23
26
|
|
|
24
27
|
const hotkeys = [PREFIX_KEY, SAVE_TOPIC_KEY].join('+');
|
|
25
|
-
|
|
28
|
+
|
|
29
|
+
useHotkeys(hotkeys, () => mutate(), {
|
|
26
30
|
enableOnFormTags: true,
|
|
27
31
|
preventDefault: true,
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
return (
|
|
31
35
|
<Tooltip title={<HotKeys desc={desc} keys={hotkeys} />}>
|
|
32
|
-
<Render aria-label={desc} icon={iconRender} onClick={
|
|
36
|
+
<Render aria-label={desc} icon={iconRender} loading={isValidating} onClick={() => mutate()} />
|
|
33
37
|
</Tooltip>
|
|
34
38
|
);
|
|
35
39
|
});
|
|
@@ -19,6 +19,9 @@ import ActionsBar from './ActionsBar';
|
|
|
19
19
|
import HistoryDivider from './HistoryDivider';
|
|
20
20
|
|
|
21
21
|
const useStyles = createStyles(({ css, prefixCls }) => ({
|
|
22
|
+
loading: css`
|
|
23
|
+
opacity: 0.6;
|
|
24
|
+
`,
|
|
22
25
|
message: css`
|
|
23
26
|
// prevent the textarea too long
|
|
24
27
|
.${prefixCls}-input {
|
|
@@ -35,7 +38,7 @@ export interface ChatListItemProps {
|
|
|
35
38
|
const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
36
39
|
const fontSize = useGlobalStore((s) => settingsSelectors.currentSettings(s).fontSize);
|
|
37
40
|
const { t } = useTranslation('common');
|
|
38
|
-
const { styles } = useStyles();
|
|
41
|
+
const { styles, cx } = useStyles();
|
|
39
42
|
const [editing, setEditing] = useState(false);
|
|
40
43
|
const [type = 'chat'] = useSessionStore((s) => {
|
|
41
44
|
const config = agentSelectors.currentAgentConfig(s);
|
|
@@ -54,10 +57,12 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
|
54
57
|
const historyLength = useChatStore((s) => chatSelectors.currentChats(s).length);
|
|
55
58
|
|
|
56
59
|
const [loading, updateMessageContent] = useChatStore((s) => [
|
|
57
|
-
s.chatLoadingId === id,
|
|
60
|
+
s.chatLoadingId === id || s.messageLoadingIds.includes(id),
|
|
58
61
|
s.modifyMessageContent,
|
|
59
62
|
]);
|
|
60
63
|
|
|
64
|
+
const [isMessageLoading] = useChatStore((s) => [s.messageLoadingIds.includes(id)]);
|
|
65
|
+
|
|
61
66
|
const onAvatarsClick = useAvatarsClick();
|
|
62
67
|
|
|
63
68
|
const RenderMessage = useCallback(
|
|
@@ -110,7 +115,7 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
|
110
115
|
<ChatItem
|
|
111
116
|
actions={<ActionsBar index={index} setEditing={setEditing} />}
|
|
112
117
|
avatar={item.meta}
|
|
113
|
-
className={styles.message}
|
|
118
|
+
className={cx(styles.message, isMessageLoading && styles.loading)}
|
|
114
119
|
editing={editing}
|
|
115
120
|
error={error}
|
|
116
121
|
errorMessage={<ErrorMessageExtra data={item} />}
|
package/src/libs/swr/index.ts
CHANGED
|
@@ -32,3 +32,12 @@ export const useActionSWR: SWRHook = (key, fetch, config) =>
|
|
|
32
32
|
revalidateOnReconnect: false,
|
|
33
33
|
...config,
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
export interface SWRRefreshParams<T, A = (...args: any[]) => any> {
|
|
37
|
+
action: A;
|
|
38
|
+
optimisticData?: (data: T | undefined) => T;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SWRefreshMethod<T> = <A extends (...args: any[]) => Promise<any>>(
|
|
42
|
+
params?: SWRRefreshParams<T, A>,
|
|
43
|
+
) => ReturnType<A>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
2
|
// Disable the auto sort key eslint rule to make the code more logic and readable
|
|
3
3
|
import { copyToClipboard } from '@lobehub/ui';
|
|
4
|
+
import { produce } from 'immer';
|
|
4
5
|
import { template } from 'lodash-es';
|
|
5
6
|
import { SWRResponse, mutate } from 'swr';
|
|
6
7
|
import { StateCreator } from 'zustand/vanilla';
|
|
@@ -19,6 +20,7 @@ import { agentSelectors } from '@/store/session/selectors';
|
|
|
19
20
|
import { ChatMessage } from '@/types/message';
|
|
20
21
|
import { TraceEventPayloads } from '@/types/trace';
|
|
21
22
|
import { setNamespace } from '@/utils/storeDebug';
|
|
23
|
+
import { nanoid } from '@/utils/uuid';
|
|
22
24
|
|
|
23
25
|
import { chatSelectors } from '../../selectors';
|
|
24
26
|
import { MessageDispatch, messagesReducer } from './reducer';
|
|
@@ -97,6 +99,7 @@ export interface ChatMessageAction {
|
|
|
97
99
|
id?: string,
|
|
98
100
|
action?: string,
|
|
99
101
|
) => AbortController | undefined;
|
|
102
|
+
toggleMessageLoading: (loading: boolean, id: string) => void;
|
|
100
103
|
refreshMessages: () => Promise<void>;
|
|
101
104
|
// TODO: 后续 smoothMessage 实现考虑落到 sse 这一层
|
|
102
105
|
createSmoothMessage: (id: string) => {
|
|
@@ -111,6 +114,7 @@ export interface ChatMessageAction {
|
|
|
111
114
|
* @param content
|
|
112
115
|
*/
|
|
113
116
|
internalUpdateMessageContent: (id: string, content: string) => Promise<void>;
|
|
117
|
+
internalCreateMessage: (params: CreateMessageParams) => Promise<string>;
|
|
114
118
|
internalResendMessage: (id: string, traceId?: string) => Promise<void>;
|
|
115
119
|
internalTraceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
|
|
116
120
|
}
|
|
@@ -130,6 +134,7 @@ export const chatMessage: StateCreator<
|
|
|
130
134
|
ChatMessageAction
|
|
131
135
|
> = (set, get) => ({
|
|
132
136
|
deleteMessage: async (id) => {
|
|
137
|
+
get().dispatchMessage({ type: 'deleteMessage', id });
|
|
133
138
|
await messageService.removeMessage(id);
|
|
134
139
|
await get().refreshMessages();
|
|
135
140
|
},
|
|
@@ -167,43 +172,6 @@ export const chatMessage: StateCreator<
|
|
|
167
172
|
await messageService.removeAllMessages();
|
|
168
173
|
await refreshMessages();
|
|
169
174
|
},
|
|
170
|
-
internalResendMessage: async (messageId, traceId) => {
|
|
171
|
-
// 1. 构造所有相关的历史记录
|
|
172
|
-
const chats = chatSelectors.currentChats(get());
|
|
173
|
-
|
|
174
|
-
const currentIndex = chats.findIndex((c) => c.id === messageId);
|
|
175
|
-
if (currentIndex < 0) return;
|
|
176
|
-
|
|
177
|
-
const currentMessage = chats[currentIndex];
|
|
178
|
-
|
|
179
|
-
let contextMessages: ChatMessage[] = [];
|
|
180
|
-
|
|
181
|
-
switch (currentMessage.role) {
|
|
182
|
-
case 'function':
|
|
183
|
-
case 'user': {
|
|
184
|
-
contextMessages = chats.slice(0, currentIndex + 1);
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
case 'assistant': {
|
|
188
|
-
// 消息是 AI 发出的因此需要找到它的 user 消息
|
|
189
|
-
const userId = currentMessage.parentId;
|
|
190
|
-
const userIndex = chats.findIndex((c) => c.id === userId);
|
|
191
|
-
// 如果消息没有 parentId,那么同 user/function 模式
|
|
192
|
-
contextMessages = chats.slice(0, userIndex < 0 ? currentIndex + 1 : userIndex + 1);
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (contextMessages.length <= 0) return;
|
|
198
|
-
|
|
199
|
-
const { coreProcessMessage } = get();
|
|
200
|
-
|
|
201
|
-
const latestMsg = contextMessages.filter((s) => s.role === 'user').at(-1);
|
|
202
|
-
|
|
203
|
-
if (!latestMsg) return;
|
|
204
|
-
|
|
205
|
-
await coreProcessMessage(contextMessages, latestMsg.id, traceId);
|
|
206
|
-
},
|
|
207
175
|
sendMessage: async ({ message, files, onlyAddUserMessage }) => {
|
|
208
176
|
const { coreProcessMessage, activeTopicId, activeId } = get();
|
|
209
177
|
if (!activeId) return;
|
|
@@ -223,8 +191,7 @@ export const chatMessage: StateCreator<
|
|
|
223
191
|
topicId: activeTopicId,
|
|
224
192
|
};
|
|
225
193
|
|
|
226
|
-
const id = await
|
|
227
|
-
await get().refreshMessages();
|
|
194
|
+
const id = await get().internalCreateMessage(newMessage);
|
|
228
195
|
|
|
229
196
|
// if only add user message, then stop
|
|
230
197
|
if (onlyAddUserMessage) return;
|
|
@@ -315,8 +282,7 @@ export const chatMessage: StateCreator<
|
|
|
315
282
|
topicId: activeTopicId, // if there is activeTopicId,then add it to topicId
|
|
316
283
|
};
|
|
317
284
|
|
|
318
|
-
const mid = await
|
|
319
|
-
await refreshMessages();
|
|
285
|
+
const mid = await get().internalCreateMessage(assistantMessage);
|
|
320
286
|
|
|
321
287
|
// 2. fetch the AI response
|
|
322
288
|
const { isFunctionCall, content, functionCallAtEnd, functionCallContent, traceId } =
|
|
@@ -344,7 +310,7 @@ export const chatMessage: StateCreator<
|
|
|
344
310
|
traceId,
|
|
345
311
|
};
|
|
346
312
|
|
|
347
|
-
functionId = await
|
|
313
|
+
functionId = await get().internalCreateMessage(functionMessage);
|
|
348
314
|
}
|
|
349
315
|
|
|
350
316
|
await refreshMessages();
|
|
@@ -533,6 +499,62 @@ export const chatMessage: StateCreator<
|
|
|
533
499
|
window.removeEventListener('beforeunload', preventLeavingFn);
|
|
534
500
|
}
|
|
535
501
|
},
|
|
502
|
+
toggleMessageLoading: (loading, id) => {
|
|
503
|
+
set(
|
|
504
|
+
{
|
|
505
|
+
messageLoadingIds: produce(get().messageLoadingIds, (draft) => {
|
|
506
|
+
if (loading) {
|
|
507
|
+
draft.push(id);
|
|
508
|
+
} else {
|
|
509
|
+
const index = draft.indexOf(id);
|
|
510
|
+
|
|
511
|
+
if (index >= 0) draft.splice(index, 1);
|
|
512
|
+
}
|
|
513
|
+
}),
|
|
514
|
+
},
|
|
515
|
+
false,
|
|
516
|
+
'toggleMessageLoading',
|
|
517
|
+
);
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
internalResendMessage: async (messageId, traceId) => {
|
|
521
|
+
// 1. 构造所有相关的历史记录
|
|
522
|
+
const chats = chatSelectors.currentChats(get());
|
|
523
|
+
|
|
524
|
+
const currentIndex = chats.findIndex((c) => c.id === messageId);
|
|
525
|
+
if (currentIndex < 0) return;
|
|
526
|
+
|
|
527
|
+
const currentMessage = chats[currentIndex];
|
|
528
|
+
|
|
529
|
+
let contextMessages: ChatMessage[] = [];
|
|
530
|
+
|
|
531
|
+
switch (currentMessage.role) {
|
|
532
|
+
case 'function':
|
|
533
|
+
case 'user': {
|
|
534
|
+
contextMessages = chats.slice(0, currentIndex + 1);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case 'assistant': {
|
|
538
|
+
// 消息是 AI 发出的因此需要找到它的 user 消息
|
|
539
|
+
const userId = currentMessage.parentId;
|
|
540
|
+
const userIndex = chats.findIndex((c) => c.id === userId);
|
|
541
|
+
// 如果消息没有 parentId,那么同 user/function 模式
|
|
542
|
+
contextMessages = chats.slice(0, userIndex < 0 ? currentIndex + 1 : userIndex + 1);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (contextMessages.length <= 0) return;
|
|
548
|
+
|
|
549
|
+
const { coreProcessMessage } = get();
|
|
550
|
+
|
|
551
|
+
const latestMsg = contextMessages.filter((s) => s.role === 'user').at(-1);
|
|
552
|
+
|
|
553
|
+
if (!latestMsg) return;
|
|
554
|
+
|
|
555
|
+
await coreProcessMessage(contextMessages, latestMsg.id, traceId);
|
|
556
|
+
},
|
|
557
|
+
|
|
536
558
|
internalUpdateMessageContent: async (id, content) => {
|
|
537
559
|
const { dispatchMessage, refreshMessages } = get();
|
|
538
560
|
|
|
@@ -545,6 +567,22 @@ export const chatMessage: StateCreator<
|
|
|
545
567
|
await refreshMessages();
|
|
546
568
|
},
|
|
547
569
|
|
|
570
|
+
internalCreateMessage: async (message) => {
|
|
571
|
+
const { dispatchMessage, refreshMessages, toggleMessageLoading } = get();
|
|
572
|
+
|
|
573
|
+
// use optimistic update to avoid the slow waiting
|
|
574
|
+
const tempId = 'tmp_' + nanoid();
|
|
575
|
+
dispatchMessage({ type: 'createMessage', id: tempId, value: message });
|
|
576
|
+
|
|
577
|
+
toggleMessageLoading(true, tempId);
|
|
578
|
+
const id = await messageService.createMessage(message);
|
|
579
|
+
|
|
580
|
+
await refreshMessages();
|
|
581
|
+
toggleMessageLoading(false, tempId);
|
|
582
|
+
|
|
583
|
+
return id;
|
|
584
|
+
},
|
|
585
|
+
|
|
548
586
|
createSmoothMessage: (id) => {
|
|
549
587
|
const { dispatchMessage } = get();
|
|
550
588
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import isEqual from 'fast-deep-equal';
|
|
2
2
|
import { produce } from 'immer';
|
|
3
3
|
|
|
4
|
+
import { CreateMessageParams } from '@/services/message';
|
|
4
5
|
import { ChatMessage } from '@/types/message';
|
|
5
6
|
import { merge } from '@/utils/merge';
|
|
6
7
|
|
|
@@ -10,6 +11,15 @@ interface UpdateMessage {
|
|
|
10
11
|
type: 'updateMessage';
|
|
11
12
|
value: ChatMessage[keyof ChatMessage];
|
|
12
13
|
}
|
|
14
|
+
interface CreateMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
type: 'createMessage';
|
|
17
|
+
value: CreateMessageParams;
|
|
18
|
+
}
|
|
19
|
+
interface DeleteMessage {
|
|
20
|
+
id: string;
|
|
21
|
+
type: 'deleteMessage';
|
|
22
|
+
}
|
|
13
23
|
|
|
14
24
|
interface UpdatePluginState {
|
|
15
25
|
id: string;
|
|
@@ -24,7 +34,12 @@ interface UpdateMessageExtra {
|
|
|
24
34
|
value: any;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
export type MessageDispatch =
|
|
37
|
+
export type MessageDispatch =
|
|
38
|
+
| CreateMessage
|
|
39
|
+
| UpdateMessage
|
|
40
|
+
| UpdatePluginState
|
|
41
|
+
| UpdateMessageExtra
|
|
42
|
+
| DeleteMessage;
|
|
28
43
|
|
|
29
44
|
export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
|
|
30
45
|
switch (payload.type) {
|
|
@@ -76,6 +91,22 @@ export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch):
|
|
|
76
91
|
});
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
case 'createMessage': {
|
|
95
|
+
return produce(state, (draftState) => {
|
|
96
|
+
const { value, id } = payload;
|
|
97
|
+
|
|
98
|
+
draftState.push({ ...value, createdAt: Date.now(), id, meta: {}, updatedAt: Date.now() });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
case 'deleteMessage': {
|
|
102
|
+
return produce(state, (draft) => {
|
|
103
|
+
const { id } = payload;
|
|
104
|
+
|
|
105
|
+
const index = draft.findIndex((m) => m.id === id);
|
|
106
|
+
|
|
107
|
+
if (index >= 0) draft.splice(index, 1);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
79
110
|
default: {
|
|
80
111
|
throw new Error('暂未实现的 type,请检查 reducer');
|
|
81
112
|
}
|
|
@@ -149,7 +149,9 @@ describe('topic action', () => {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
// Check if mutate has been called with the active session ID
|
|
152
|
-
expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId]
|
|
152
|
+
expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId], undefined, {
|
|
153
|
+
populateCache: false,
|
|
154
|
+
});
|
|
153
155
|
});
|
|
154
156
|
|
|
155
157
|
it('should handle errors during refreshing topics', async () => {
|
|
@@ -314,7 +316,7 @@ describe('topic action', () => {
|
|
|
314
316
|
const activeId = 'test-session-id';
|
|
315
317
|
|
|
316
318
|
await act(async () => {
|
|
317
|
-
useChatStore.setState({ activeId });
|
|
319
|
+
useChatStore.setState({ activeId, activeTopicId: topicId });
|
|
318
320
|
});
|
|
319
321
|
|
|
320
322
|
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
@@ -329,6 +331,27 @@ describe('topic action', () => {
|
|
|
329
331
|
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
330
332
|
expect(switchTopicSpy).toHaveBeenCalled();
|
|
331
333
|
});
|
|
334
|
+
it('should remove a specific topic and its messages, then not refresh the topic list', async () => {
|
|
335
|
+
const topicId = 'topic-1';
|
|
336
|
+
const { result } = renderHook(() => useChatStore());
|
|
337
|
+
const activeId = 'test-session-id';
|
|
338
|
+
|
|
339
|
+
await act(async () => {
|
|
340
|
+
useChatStore.setState({ activeId });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
344
|
+
const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
|
|
345
|
+
|
|
346
|
+
await act(async () => {
|
|
347
|
+
await result.current.removeTopic(topicId);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
|
|
351
|
+
expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
|
|
352
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
353
|
+
expect(switchTopicSpy).not.toHaveBeenCalled();
|
|
354
|
+
});
|
|
332
355
|
});
|
|
333
356
|
describe('removeUnstarredTopic', () => {
|
|
334
357
|
it('should remove unstarred topics and refresh the topic list', async () => {
|
|
@@ -9,11 +9,11 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
9
9
|
import { chainSummaryTitle } from '@/chains/summaryTitle';
|
|
10
10
|
import { LOADING_FLAT } from '@/const/message';
|
|
11
11
|
import { TraceNameMap } from '@/const/trace';
|
|
12
|
-
import { useClientDataSWR } from '@/libs/swr';
|
|
12
|
+
import { SWRefreshMethod, useClientDataSWR } from '@/libs/swr';
|
|
13
13
|
import { chatService } from '@/services/chat';
|
|
14
14
|
import { messageService } from '@/services/message';
|
|
15
15
|
import { topicService } from '@/services/topic';
|
|
16
|
-
import { ChatStore } from '@/store/chat';
|
|
16
|
+
import type { ChatStore } from '@/store/chat';
|
|
17
17
|
import { ChatMessage } from '@/types/message';
|
|
18
18
|
import { ChatTopic } from '@/types/topic';
|
|
19
19
|
import { setNamespace } from '@/utils/storeDebug';
|
|
@@ -29,7 +29,7 @@ const SWR_USE_SEARCH_TOPIC = 'SWR_USE_SEARCH_TOPIC';
|
|
|
29
29
|
export interface ChatTopicAction {
|
|
30
30
|
favoriteTopic: (id: string, favState: boolean) => Promise<void>;
|
|
31
31
|
openNewTopicOrSaveTopic: () => Promise<void>;
|
|
32
|
-
refreshTopic:
|
|
32
|
+
refreshTopic: SWRefreshMethod<ChatTopic[]>;
|
|
33
33
|
removeAllTopics: () => Promise<void>;
|
|
34
34
|
removeSessionTopics: () => Promise<void>;
|
|
35
35
|
removeTopic: (id: string) => Promise<void>;
|
|
@@ -78,6 +78,17 @@ export const chatTopic: StateCreator<
|
|
|
78
78
|
messages: messages.map((m) => m.id),
|
|
79
79
|
});
|
|
80
80
|
await refreshTopic();
|
|
81
|
+
// TODO: 优化为乐观更新
|
|
82
|
+
// const params: CreateTopicParams = {
|
|
83
|
+
// sessionId: activeId,
|
|
84
|
+
// title: t('topic.defaultTitle', { ns: 'chat' }),
|
|
85
|
+
// messages: messages.map((m) => m.id),
|
|
86
|
+
// };
|
|
87
|
+
|
|
88
|
+
// const topicId = await refreshTopic({
|
|
89
|
+
// action: async () => topicService.createTopic(params),
|
|
90
|
+
// optimisticData: (data) => topicReducer(data, { type: 'addTopic', value: params }),
|
|
91
|
+
// });
|
|
81
92
|
|
|
82
93
|
// 2. auto summary topic Title
|
|
83
94
|
// we don't need to wait for summary, just let it run async
|
|
@@ -189,9 +200,10 @@ export const chatTopic: StateCreator<
|
|
|
189
200
|
await refreshTopic();
|
|
190
201
|
},
|
|
191
202
|
removeTopic: async (id) => {
|
|
192
|
-
const { activeId, switchTopic, refreshTopic } = get();
|
|
203
|
+
const { activeId, activeTopicId, switchTopic, refreshTopic } = get();
|
|
193
204
|
|
|
194
205
|
// remove messages in the topic
|
|
206
|
+
// TODO: Need to remove because server service don't need to call it
|
|
195
207
|
await messageService.removeMessages(activeId, id);
|
|
196
208
|
|
|
197
209
|
// remove topic
|
|
@@ -199,7 +211,7 @@ export const chatTopic: StateCreator<
|
|
|
199
211
|
await refreshTopic();
|
|
200
212
|
|
|
201
213
|
// switch bach to default topic
|
|
202
|
-
switchTopic();
|
|
214
|
+
if (activeTopicId === id) switchTopic();
|
|
203
215
|
},
|
|
204
216
|
removeUnstarredTopic: async () => {
|
|
205
217
|
const { refreshTopic, switchTopic } = get();
|
|
@@ -226,7 +238,12 @@ export const chatTopic: StateCreator<
|
|
|
226
238
|
updateTopicLoading: (id) => {
|
|
227
239
|
set({ topicLoadingId: id }, false, n('updateTopicLoading'));
|
|
228
240
|
},
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
// TODO: I don't know why this ts error, so have to ignore it
|
|
242
|
+
// @ts-ignore
|
|
243
|
+
refreshTopic: async (params) => {
|
|
244
|
+
return mutate([SWR_USE_FETCH_TOPIC, get().activeId], params?.action, {
|
|
245
|
+
optimisticData: params?.optimisticData,
|
|
246
|
+
populateCache: false,
|
|
247
|
+
});
|
|
231
248
|
},
|
|
232
249
|
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
import { expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { ChatTopic } from '@/types/topic';
|
|
5
|
+
|
|
6
|
+
import { ChatTopicDispatch, topicReducer } from './reducer';
|
|
7
|
+
|
|
8
|
+
describe('topicReducer', () => {
|
|
9
|
+
let state: ChatTopic[];
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
state = [];
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('addTopic', () => {
|
|
16
|
+
it('should add a new ChatTopic object to state', () => {
|
|
17
|
+
const payload: ChatTopicDispatch = {
|
|
18
|
+
type: 'addTopic',
|
|
19
|
+
value: {
|
|
20
|
+
title: 'Test Topic',
|
|
21
|
+
sessionId: '',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const newState = topicReducer(state, payload);
|
|
26
|
+
|
|
27
|
+
expect(newState[0].id).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('updateTopic', () => {
|
|
32
|
+
it('should update the ChatTopic object in state', () => {
|
|
33
|
+
const topic: ChatTopic = {
|
|
34
|
+
id: '1',
|
|
35
|
+
title: 'Test Topic',
|
|
36
|
+
createdAt: Date.now(),
|
|
37
|
+
updatedAt: Date.now(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
state.push(topic);
|
|
41
|
+
|
|
42
|
+
const payload: ChatTopicDispatch = {
|
|
43
|
+
type: 'updateTopic',
|
|
44
|
+
id: '1',
|
|
45
|
+
key: 'title',
|
|
46
|
+
value: 'Updated Topic',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const newState = topicReducer(state, payload);
|
|
50
|
+
|
|
51
|
+
expect(newState[0].title).toBe('Updated Topic');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should update the ChatTopic object with correct properties', () => {
|
|
55
|
+
const topic: ChatTopic = {
|
|
56
|
+
id: '1',
|
|
57
|
+
title: 'Test Topic',
|
|
58
|
+
createdAt: Date.now() - 1,
|
|
59
|
+
updatedAt: Date.now() - 1, // 设定比当前时间前面一点
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
state.push(topic);
|
|
63
|
+
|
|
64
|
+
const payload: ChatTopicDispatch = {
|
|
65
|
+
type: 'updateTopic',
|
|
66
|
+
id: '1',
|
|
67
|
+
key: 'title',
|
|
68
|
+
value: 'Updated Topic',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const newState = topicReducer(state, payload);
|
|
72
|
+
|
|
73
|
+
expect(newState[0].updatedAt).toBeGreaterThan(topic.updatedAt);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('deleteTopic', () => {
|
|
78
|
+
it('should delete the specified ChatTopic object from state', () => {
|
|
79
|
+
const topic: ChatTopic = {
|
|
80
|
+
id: '1',
|
|
81
|
+
title: 'Test Topic',
|
|
82
|
+
createdAt: Date.now(),
|
|
83
|
+
updatedAt: Date.now(),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
state.push(topic);
|
|
87
|
+
|
|
88
|
+
const payload: ChatTopicDispatch = {
|
|
89
|
+
type: 'deleteTopic',
|
|
90
|
+
id: '1',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const newState = topicReducer(state, payload);
|
|
94
|
+
|
|
95
|
+
expect(newState).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('default', () => {
|
|
100
|
+
it('should return the original state object', () => {
|
|
101
|
+
const payload = {
|
|
102
|
+
type: 'unknown',
|
|
103
|
+
} as unknown as ChatTopicDispatch;
|
|
104
|
+
|
|
105
|
+
const newState = topicReducer(state, payload);
|
|
106
|
+
|
|
107
|
+
expect(newState).toBe(state);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('produce', () => {
|
|
112
|
+
it('should generate immutable state object', () => {
|
|
113
|
+
const payload: ChatTopicDispatch = {
|
|
114
|
+
type: 'addTopic',
|
|
115
|
+
value: {
|
|
116
|
+
title: 'Test Topic',
|
|
117
|
+
sessionId: '1',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const newState = topicReducer(state, payload);
|
|
122
|
+
|
|
123
|
+
expect(newState).not.toBe(state);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should not modify the original state object', () => {
|
|
127
|
+
const payload: ChatTopicDispatch = {
|
|
128
|
+
type: 'addTopic',
|
|
129
|
+
value: {
|
|
130
|
+
title: 'Test Topic',
|
|
131
|
+
|
|
132
|
+
sessionId: '123',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const newState = topicReducer(state, payload);
|
|
137
|
+
|
|
138
|
+
expect(state).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { CreateTopicParams } from '@/services/topic/type';
|
|
4
|
+
import { ChatTopic } from '@/types/topic';
|
|
5
|
+
|
|
6
|
+
interface AddChatTopicAction {
|
|
7
|
+
type: 'addTopic';
|
|
8
|
+
value: CreateTopicParams;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UpdateChatTopicAction {
|
|
12
|
+
id: string;
|
|
13
|
+
key: keyof ChatTopic;
|
|
14
|
+
type: 'updateTopic';
|
|
15
|
+
value: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface DeleteChatTopicAction {
|
|
19
|
+
id: string;
|
|
20
|
+
type: 'deleteTopic';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction | DeleteChatTopicAction;
|
|
24
|
+
|
|
25
|
+
export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch): ChatTopic[] => {
|
|
26
|
+
switch (payload.type) {
|
|
27
|
+
case 'addTopic': {
|
|
28
|
+
return produce(state, (draftState) => {
|
|
29
|
+
draftState.unshift({
|
|
30
|
+
...payload.value,
|
|
31
|
+
createdAt: Date.now(),
|
|
32
|
+
id: Date.now().toString(),
|
|
33
|
+
sessionId: payload.value.sessionId ? payload.value.sessionId : undefined,
|
|
34
|
+
updatedAt: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'updateTopic': {
|
|
40
|
+
return produce(state, (draftState) => {
|
|
41
|
+
const { key, value, id } = payload;
|
|
42
|
+
const topicIndex = draftState.findIndex((topic) => topic.id === id);
|
|
43
|
+
|
|
44
|
+
if (topicIndex !== -1) {
|
|
45
|
+
const updatedTopic = { ...draftState[topicIndex] };
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
updatedTopic[key] = value;
|
|
48
|
+
draftState[topicIndex] = updatedTopic;
|
|
49
|
+
updatedTopic.updatedAt = Date.now();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case 'deleteTopic': {
|
|
55
|
+
return produce(state, (draftState) => {
|
|
56
|
+
const topicIndex = draftState.findIndex((topic) => topic.id === payload.id);
|
|
57
|
+
if (topicIndex !== -1) {
|
|
58
|
+
draftState.splice(topicIndex, 1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
default: {
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -6,7 +6,7 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
6
6
|
|
|
7
7
|
import { message } from '@/components/AntdStaticMethods';
|
|
8
8
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
9
|
-
import { useClientDataSWR } from '@/libs/swr';
|
|
9
|
+
import { SWRRefreshParams, useClientDataSWR } from '@/libs/swr';
|
|
10
10
|
import { sessionService } from '@/services/session';
|
|
11
11
|
import { useGlobalStore } from '@/store/global';
|
|
12
12
|
import { settingsSelectors } from '@/store/global/selectors';
|
|
@@ -51,10 +51,7 @@ export interface SessionAction {
|
|
|
51
51
|
/**
|
|
52
52
|
* re-fetch the data
|
|
53
53
|
*/
|
|
54
|
-
refreshSessions: (params?:
|
|
55
|
-
action: () => Promise<void>;
|
|
56
|
-
optimisticData?: (data: ChatSessionList) => ChatSessionList;
|
|
57
|
-
}) => Promise<void>;
|
|
54
|
+
refreshSessions: (params?: SWRRefreshParams<ChatSessionList>) => Promise<void>;
|
|
58
55
|
|
|
59
56
|
/**
|
|
60
57
|
* remove session
|
|
@@ -145,6 +142,8 @@ export const createSessionSlice: StateCreator<
|
|
|
145
142
|
},
|
|
146
143
|
// 乐观更新
|
|
147
144
|
optimisticData: produce((draft) => {
|
|
145
|
+
if (!draft) return;
|
|
146
|
+
|
|
148
147
|
const session = draft.all.find((i) => i.id === sessionId);
|
|
149
148
|
if (!session) return;
|
|
150
149
|
|