@lobehub/chat 0.18.2 → 0.20.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 +59 -0
- package/package.json +1 -1
- package/src/locales/default/common.ts +4 -2
- package/src/pages/chat/[id]/Config/SideBar.tsx +10 -6
- package/src/pages/chat/[id]/Config/Topic.tsx +27 -0
- package/src/pages/chat/[id]/Config/TopicItem.tsx +80 -0
- package/src/pages/chat/[id]/Conversation/Input/index.tsx +7 -4
- package/src/pages/chat/[id]/Header.tsx +14 -10
- package/src/pages/chat/layout.tsx +10 -4
- package/src/prompts/chat.ts +19 -0
- package/src/store/session/selectors.ts +1 -1
- package/src/store/session/slices/agentConfig/selectors.ts +4 -0
- package/src/store/session/slices/chat/actions/index.ts +21 -0
- package/src/store/session/slices/chat/{action.ts → actions/message.ts} +34 -32
- package/src/store/session/slices/chat/actions/topic.ts +98 -0
- package/src/store/session/slices/chat/index.ts +1 -1
- package/src/store/session/slices/chat/initialState.ts +2 -5
- package/src/store/session/slices/chat/{messageReducer.test.ts → reducers/message.test.ts} +93 -1
- package/src/store/session/slices/chat/{messageReducer.ts → reducers/message.ts} +16 -1
- package/src/store/session/slices/chat/reducers/topic.test.ts +179 -0
- package/src/store/session/slices/chat/reducers/topic.ts +59 -0
- package/src/store/session/slices/chat/selectors/chat.ts +8 -4
- package/src/store/session/slices/chat/selectors/index.ts +5 -0
- package/src/store/session/slices/chat/selectors/topic.ts +16 -0
- package/src/store/session/slices/chat/selectors/utils.test.ts +82 -2
- package/src/store/session/slices/chat/selectors/utils.ts +8 -2
- package/src/store/session/slices/session/reducers/session.ts +25 -1
- package/src/types/chatMessage.ts +4 -2
- package/src/types/session.ts +2 -1
- package/src/types/topic.ts +8 -0
- package/src/pages/chat/[id]/Config/ConfigCell.tsx +0 -68
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
export interface ChatState {
|
|
2
|
+
activeTopicId?: string;
|
|
2
3
|
chatLoading: boolean;
|
|
3
|
-
|
|
4
|
+
topicLoadingId?: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export const initialChatState: ChatState = {
|
|
7
8
|
chatLoading: false,
|
|
8
|
-
|
|
9
|
-
// activeId: null,
|
|
10
|
-
// searchKeywords: '',
|
|
11
|
-
//
|
|
12
9
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChatMessageMap } from '@/types/chatMessage';
|
|
2
2
|
|
|
3
|
-
import { MessageDispatch, messagesReducer } from './
|
|
3
|
+
import { MessageDispatch, messagesReducer } from './message';
|
|
4
4
|
|
|
5
5
|
describe('messagesReducer', () => {
|
|
6
6
|
let initialState: ChatMessageMap;
|
|
@@ -116,6 +116,30 @@ describe('messagesReducer', () => {
|
|
|
116
116
|
quotaId: 'message2',
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
it('should use the provided parentId and quotaId when adding a new message', () => {
|
|
121
|
+
const payload: MessageDispatch = {
|
|
122
|
+
type: 'addMessage',
|
|
123
|
+
message: 'New Message',
|
|
124
|
+
id: 'message3',
|
|
125
|
+
role: 'user',
|
|
126
|
+
parentId: 'message1',
|
|
127
|
+
quotaId: 'message2',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const newState = messagesReducer(initialState, payload);
|
|
131
|
+
|
|
132
|
+
expect(newState.message3).toEqual({
|
|
133
|
+
id: 'message3',
|
|
134
|
+
content: 'New Message',
|
|
135
|
+
meta: {},
|
|
136
|
+
createAt: expect.any(Number),
|
|
137
|
+
updateAt: expect.any(Number),
|
|
138
|
+
role: 'user',
|
|
139
|
+
parentId: 'message1',
|
|
140
|
+
quotaId: 'message2',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
119
143
|
});
|
|
120
144
|
|
|
121
145
|
describe('deleteMessage', () => {
|
|
@@ -142,6 +166,17 @@ describe('messagesReducer', () => {
|
|
|
142
166
|
|
|
143
167
|
expect(newState).toEqual(initialState);
|
|
144
168
|
});
|
|
169
|
+
|
|
170
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
171
|
+
const payload: MessageDispatch = {
|
|
172
|
+
type: 'deleteMessage',
|
|
173
|
+
id: 'nonexistentMessage',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const newState = messagesReducer(initialState, payload);
|
|
177
|
+
|
|
178
|
+
expect(newState).toEqual(initialState);
|
|
179
|
+
});
|
|
145
180
|
});
|
|
146
181
|
|
|
147
182
|
describe('updateMessage', () => {
|
|
@@ -171,6 +206,19 @@ describe('messagesReducer', () => {
|
|
|
171
206
|
|
|
172
207
|
expect(newState).toEqual(initialState);
|
|
173
208
|
});
|
|
209
|
+
|
|
210
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
211
|
+
const payload: MessageDispatch = {
|
|
212
|
+
type: 'updateMessage',
|
|
213
|
+
id: 'nonexistentMessage',
|
|
214
|
+
key: 'content',
|
|
215
|
+
value: 'Updated Message',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const newState = messagesReducer(initialState, payload);
|
|
219
|
+
|
|
220
|
+
expect(newState).toEqual(initialState);
|
|
221
|
+
});
|
|
174
222
|
});
|
|
175
223
|
|
|
176
224
|
describe('updateMessageExtra', () => {
|
|
@@ -200,6 +248,19 @@ describe('messagesReducer', () => {
|
|
|
200
248
|
|
|
201
249
|
expect(newState).toEqual(initialState);
|
|
202
250
|
});
|
|
251
|
+
|
|
252
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
253
|
+
const payload: MessageDispatch = {
|
|
254
|
+
type: 'updateMessageExtra',
|
|
255
|
+
id: 'nonexistentMessage',
|
|
256
|
+
key: 'translate',
|
|
257
|
+
value: { target: 'en', to: 'zh' },
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const newState = messagesReducer(initialState, payload);
|
|
261
|
+
|
|
262
|
+
expect(newState).toEqual(initialState);
|
|
263
|
+
});
|
|
203
264
|
});
|
|
204
265
|
|
|
205
266
|
describe('resetMessages', () => {
|
|
@@ -212,6 +273,37 @@ describe('messagesReducer', () => {
|
|
|
212
273
|
|
|
213
274
|
expect(newState).toEqual({});
|
|
214
275
|
});
|
|
276
|
+
|
|
277
|
+
it('should delete messages with the specified topicId', () => {
|
|
278
|
+
const initialState = {
|
|
279
|
+
message1: {
|
|
280
|
+
id: 'message1',
|
|
281
|
+
content: 'Hello World',
|
|
282
|
+
createAt: 1629264000000,
|
|
283
|
+
updateAt: 1629264000000,
|
|
284
|
+
role: 'user',
|
|
285
|
+
topicId: 'topic1',
|
|
286
|
+
},
|
|
287
|
+
message2: {
|
|
288
|
+
id: 'message2',
|
|
289
|
+
content: 'How are you?',
|
|
290
|
+
createAt: 1629264000000,
|
|
291
|
+
updateAt: 1629264000000,
|
|
292
|
+
role: 'system',
|
|
293
|
+
},
|
|
294
|
+
} as unknown as ChatMessageMap;
|
|
295
|
+
|
|
296
|
+
const payload: MessageDispatch = {
|
|
297
|
+
type: 'resetMessages',
|
|
298
|
+
topicId: 'topic1',
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const newState = messagesReducer(initialState, payload);
|
|
302
|
+
|
|
303
|
+
expect(Object.keys(newState)).toHaveLength(1);
|
|
304
|
+
expect(newState).not.toHaveProperty('message1');
|
|
305
|
+
expect(newState).toHaveProperty('message2');
|
|
306
|
+
});
|
|
215
307
|
});
|
|
216
308
|
|
|
217
309
|
describe('unimplemented type', () => {
|
|
@@ -21,6 +21,7 @@ interface DeleteMessage {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface ResetMessages {
|
|
24
|
+
topicId?: string;
|
|
24
25
|
type: 'resetMessages';
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -101,7 +102,21 @@ export const messagesReducer = (
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
case 'resetMessages': {
|
|
104
|
-
return {
|
|
105
|
+
return produce(state, (draftState) => {
|
|
106
|
+
const { topicId } = payload;
|
|
107
|
+
|
|
108
|
+
const messages = Object.values(draftState).filter((message) => {
|
|
109
|
+
// 如果没有 topicId,说明是清空默认对话里的消息
|
|
110
|
+
if (!topicId) return !message.topicId;
|
|
111
|
+
|
|
112
|
+
return message.topicId === topicId;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 删除上述找到的消息
|
|
116
|
+
for (const message of messages) {
|
|
117
|
+
delete draftState[message.id];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
default: {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { ChatTopic, ChatTopicMap } from '@/types/topic';
|
|
4
|
+
|
|
5
|
+
import { ChatTopicDispatch, topicReducer } from './topic';
|
|
6
|
+
|
|
7
|
+
describe('topicReducer', () => {
|
|
8
|
+
let state: ChatTopicMap;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
state = {};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('addChatTopic', () => {
|
|
15
|
+
it('should add a new ChatTopic object to state', () => {
|
|
16
|
+
const payload: ChatTopicDispatch = {
|
|
17
|
+
type: 'addChatTopic',
|
|
18
|
+
topic: {
|
|
19
|
+
id: '1',
|
|
20
|
+
title: 'Test Topic',
|
|
21
|
+
createAt: Date.now(),
|
|
22
|
+
updateAt: Date.now(),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const newState = topicReducer(state, payload);
|
|
27
|
+
|
|
28
|
+
expect(newState).toMatchObject({
|
|
29
|
+
'1': payload.topic,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should add a ChatTopic object with correct id', () => {
|
|
34
|
+
const payload: ChatTopicDispatch = {
|
|
35
|
+
type: 'addChatTopic',
|
|
36
|
+
topic: {
|
|
37
|
+
id: '1',
|
|
38
|
+
title: 'Test Topic',
|
|
39
|
+
createAt: Date.now(),
|
|
40
|
+
updateAt: Date.now(),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const newState = topicReducer(state, payload);
|
|
45
|
+
|
|
46
|
+
expect(newState['1']).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should add a ChatTopic object with correct properties', () => {
|
|
50
|
+
const payload: ChatTopicDispatch = {
|
|
51
|
+
type: 'addChatTopic',
|
|
52
|
+
topic: {
|
|
53
|
+
id: '1',
|
|
54
|
+
title: 'Test Topic',
|
|
55
|
+
createAt: Date.now(),
|
|
56
|
+
updateAt: Date.now(),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const newState = topicReducer(state, payload);
|
|
61
|
+
|
|
62
|
+
expect(newState['1']).toMatchObject(payload.topic);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('updateChatTopic', () => {
|
|
67
|
+
it('should update the ChatTopic object in state', () => {
|
|
68
|
+
const topic: ChatTopic = {
|
|
69
|
+
id: '1',
|
|
70
|
+
title: 'Test Topic',
|
|
71
|
+
createAt: Date.now(),
|
|
72
|
+
updateAt: Date.now(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
state['1'] = topic;
|
|
76
|
+
|
|
77
|
+
const payload: ChatTopicDispatch = {
|
|
78
|
+
type: 'updateChatTopic',
|
|
79
|
+
id: '1',
|
|
80
|
+
key: 'title',
|
|
81
|
+
value: 'Updated Topic',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const newState = topicReducer(state, payload);
|
|
85
|
+
|
|
86
|
+
expect(newState['1'].title).toBe('Updated Topic');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should update the ChatTopic object with correct properties', () => {
|
|
90
|
+
const topic: ChatTopic = {
|
|
91
|
+
id: '1',
|
|
92
|
+
title: 'Test Topic',
|
|
93
|
+
createAt: Date.now() - 1,
|
|
94
|
+
updateAt: Date.now() - 1, // 设定比当前时间前面一点
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
state['1'] = topic;
|
|
98
|
+
|
|
99
|
+
const payload: ChatTopicDispatch = {
|
|
100
|
+
type: 'updateChatTopic',
|
|
101
|
+
id: '1',
|
|
102
|
+
key: 'title',
|
|
103
|
+
value: 'Updated Topic',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const newState = topicReducer(state, payload);
|
|
107
|
+
|
|
108
|
+
expect(newState['1'].updateAt).toBeGreaterThan(topic.updateAt);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('deleteChatTopic', () => {
|
|
113
|
+
it('should delete the specified ChatTopic object from state', () => {
|
|
114
|
+
const topic: ChatTopic = {
|
|
115
|
+
id: '1',
|
|
116
|
+
title: 'Test Topic',
|
|
117
|
+
createAt: Date.now(),
|
|
118
|
+
updateAt: Date.now(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
state['1'] = topic;
|
|
122
|
+
|
|
123
|
+
const payload: ChatTopicDispatch = {
|
|
124
|
+
type: 'deleteChatTopic',
|
|
125
|
+
id: '1',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const newState = topicReducer(state, payload);
|
|
129
|
+
|
|
130
|
+
expect(newState['1']).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('default', () => {
|
|
135
|
+
it('should return the original state object', () => {
|
|
136
|
+
const payload = {
|
|
137
|
+
type: 'unknown',
|
|
138
|
+
} as unknown as ChatTopicDispatch;
|
|
139
|
+
|
|
140
|
+
const newState = topicReducer(state, payload);
|
|
141
|
+
|
|
142
|
+
expect(newState).toBe(state);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('produce', () => {
|
|
147
|
+
it('should generate immutable state object', () => {
|
|
148
|
+
const payload: ChatTopicDispatch = {
|
|
149
|
+
type: 'addChatTopic',
|
|
150
|
+
topic: {
|
|
151
|
+
id: '1',
|
|
152
|
+
title: 'Test Topic',
|
|
153
|
+
createAt: Date.now(),
|
|
154
|
+
updateAt: Date.now(),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const newState = topicReducer(state, payload);
|
|
159
|
+
|
|
160
|
+
expect(newState).not.toBe(state);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should not modify the original state object', () => {
|
|
164
|
+
const payload: ChatTopicDispatch = {
|
|
165
|
+
type: 'addChatTopic',
|
|
166
|
+
topic: {
|
|
167
|
+
id: '1',
|
|
168
|
+
title: 'Test Topic',
|
|
169
|
+
createAt: Date.now(),
|
|
170
|
+
updateAt: Date.now(),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const newState = topicReducer(state, payload);
|
|
175
|
+
|
|
176
|
+
expect(state).toMatchObject({});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { ChatTopic, ChatTopicMap } from '@/types/topic';
|
|
4
|
+
|
|
5
|
+
interface AddChatTopicAction {
|
|
6
|
+
topic: ChatTopic;
|
|
7
|
+
type: 'addChatTopic';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UpdateChatTopicAction {
|
|
11
|
+
id: string;
|
|
12
|
+
key: keyof ChatTopic;
|
|
13
|
+
type: 'updateChatTopic';
|
|
14
|
+
value: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DeleteChatTopicAction {
|
|
18
|
+
id: string;
|
|
19
|
+
type: 'deleteChatTopic';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction | DeleteChatTopicAction;
|
|
23
|
+
|
|
24
|
+
export const topicReducer = (state: ChatTopicMap, payload: ChatTopicDispatch): ChatTopicMap => {
|
|
25
|
+
switch (payload.type) {
|
|
26
|
+
case 'addChatTopic': {
|
|
27
|
+
return produce(state, (draftState) => {
|
|
28
|
+
draftState[payload.topic.id] = payload.topic;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case 'updateChatTopic': {
|
|
33
|
+
return produce(state, (draftState) => {
|
|
34
|
+
const { key, value, id } = payload;
|
|
35
|
+
|
|
36
|
+
if (!draftState[id]) return;
|
|
37
|
+
|
|
38
|
+
const topic = draftState[id];
|
|
39
|
+
|
|
40
|
+
if (value !== undefined) {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
topic[key] = value;
|
|
43
|
+
|
|
44
|
+
topic.updateAt = Date.now();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'deleteChatTopic': {
|
|
50
|
+
return produce(state, (draftState) => {
|
|
51
|
+
delete draftState[payload.id];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
default: {
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -12,10 +12,14 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
|
|
|
12
12
|
const session = sessionSelectors.currentSession(s);
|
|
13
13
|
if (!session) return [];
|
|
14
14
|
|
|
15
|
-
return organizeChats(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
return organizeChats(
|
|
16
|
+
session,
|
|
17
|
+
{
|
|
18
|
+
assistant: agentSelectors.currentAgentAvatar(s),
|
|
19
|
+
user: useSettings.getState().settings.avatar || DEFAULT_USER_AVATAR,
|
|
20
|
+
},
|
|
21
|
+
s.activeTopicId,
|
|
22
|
+
);
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
export const systemRoleSel = (s: SessionStore): string => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { currentChats } from './chat';
|
|
2
2
|
import { chatsTokenCount, systemRoleTokenCount, totalTokenCount } from './token';
|
|
3
|
+
import { currentTopics } from './topic';
|
|
3
4
|
|
|
4
5
|
export const chatSelectors = {
|
|
5
6
|
chatsTokenCount,
|
|
@@ -7,3 +8,7 @@ export const chatSelectors = {
|
|
|
7
8
|
systemRoleTokenCount,
|
|
8
9
|
totalTokenCount,
|
|
9
10
|
};
|
|
11
|
+
|
|
12
|
+
export const topicSelectors = {
|
|
13
|
+
currentTopics,
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 展示在聊天框中的消息
|
|
2
|
+
import { SessionStore, sessionSelectors } from '@/store/session';
|
|
3
|
+
import { ChatTopic } from '@/types/topic';
|
|
4
|
+
|
|
5
|
+
export const currentTopics = (s: SessionStore): ChatTopic[] => {
|
|
6
|
+
const session = sessionSelectors.currentSession(s);
|
|
7
|
+
if (!session) return [];
|
|
8
|
+
|
|
9
|
+
const topics = Object.values(session.topics || {});
|
|
10
|
+
|
|
11
|
+
// 按时间倒序
|
|
12
|
+
const favTopics = topics.filter((t) => t.favorite).sort((a, b) => b.updateAt - a.updateAt);
|
|
13
|
+
const defaultTopics = topics.filter((t) => !t.favorite).sort((a, b) => b.updateAt - a.updateAt);
|
|
14
|
+
|
|
15
|
+
return [...favTopics, ...defaultTopics];
|
|
16
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { beforeEach } from 'vitest';
|
|
1
|
+
import { beforeEach, describe } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
3
4
|
import { LobeAgentSession } from '@/types/session';
|
|
4
5
|
|
|
5
6
|
import { organizeChats } from './utils';
|
|
@@ -67,7 +68,7 @@ describe('organizeChats', () => {
|
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
it('should filter out archived messages', () => {
|
|
70
|
-
session.chats['2'].
|
|
71
|
+
session.chats['2'].topicId = '123';
|
|
71
72
|
const result = organizeChats(session, { assistant: '', user: '' });
|
|
72
73
|
expect(result.length).toBe(2);
|
|
73
74
|
expect(result[0].id).toBe('1');
|
|
@@ -261,4 +262,83 @@ describe('organizeChats', () => {
|
|
|
261
262
|
},
|
|
262
263
|
]);
|
|
263
264
|
});
|
|
265
|
+
|
|
266
|
+
const avatar = {
|
|
267
|
+
assistant: 'assistant-avatar',
|
|
268
|
+
user: 'user-avatar',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
it('should organize chats in chronological order when topicId is not provided', () => {
|
|
272
|
+
const result = organizeChats(session, avatar);
|
|
273
|
+
|
|
274
|
+
expect(result.length).toBe(3);
|
|
275
|
+
expect(result[0].id).toBe('1');
|
|
276
|
+
expect(result[1].id).toBe('2');
|
|
277
|
+
expect(result[2].id).toBe('3');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should only return chats with specified topicId when topicId is provided', () => {
|
|
281
|
+
const result = organizeChats(session, avatar, 'topic-id');
|
|
282
|
+
|
|
283
|
+
expect(result.length).toBe(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('user Meta', () => {
|
|
287
|
+
it('should return correct meta for user role', () => {
|
|
288
|
+
const result = organizeChats(session, avatar);
|
|
289
|
+
const meta = result[1].meta;
|
|
290
|
+
|
|
291
|
+
expect(meta.avatar).toBe(avatar.user);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should return correct meta for assistant role', () => {
|
|
295
|
+
const result = organizeChats(session, avatar);
|
|
296
|
+
const meta = result[0].meta;
|
|
297
|
+
|
|
298
|
+
expect(meta.avatar).toBe(avatar.assistant);
|
|
299
|
+
expect(meta.title).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('should return correct meta for function role', () => {
|
|
303
|
+
it('找不到插件', () => {
|
|
304
|
+
const message = {
|
|
305
|
+
id: '4',
|
|
306
|
+
createAt: 1927785600004,
|
|
307
|
+
updateAt: 1927785600004,
|
|
308
|
+
role: 'function',
|
|
309
|
+
function_call: {
|
|
310
|
+
name: 'plugin-name',
|
|
311
|
+
},
|
|
312
|
+
} as ChatMessage;
|
|
313
|
+
|
|
314
|
+
session.chats[message.id] = message;
|
|
315
|
+
|
|
316
|
+
const result = organizeChats(session, avatar);
|
|
317
|
+
const meta = result[3].meta;
|
|
318
|
+
|
|
319
|
+
expect(meta.avatar).toBe('🧩');
|
|
320
|
+
expect(meta.title).toBe('plugin-unknown');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('找到的插件', () => {
|
|
324
|
+
const message = {
|
|
325
|
+
id: '4',
|
|
326
|
+
createAt: 1927785600004,
|
|
327
|
+
updateAt: 1927785600004,
|
|
328
|
+
role: 'function',
|
|
329
|
+
function_call: {
|
|
330
|
+
name: 'realtimeWeather',
|
|
331
|
+
},
|
|
332
|
+
} as ChatMessage;
|
|
333
|
+
|
|
334
|
+
session.chats[message.id] = message;
|
|
335
|
+
|
|
336
|
+
const result = organizeChats(session, avatar);
|
|
337
|
+
const meta = result[3].meta;
|
|
338
|
+
|
|
339
|
+
expect(meta.avatar).toBe('☂️');
|
|
340
|
+
expect(meta.title).toBe('realtimeWeather');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
264
344
|
});
|
|
@@ -5,6 +5,7 @@ import { LobeAgentSession } from '@/types/session';
|
|
|
5
5
|
export const organizeChats = (
|
|
6
6
|
session: LobeAgentSession,
|
|
7
7
|
avatar: { assistant: string; user: string },
|
|
8
|
+
topicId?: string,
|
|
8
9
|
) => {
|
|
9
10
|
const getMeta = (message: ChatMessage) => {
|
|
10
11
|
switch (message.role) {
|
|
@@ -38,8 +39,13 @@ export const organizeChats = (
|
|
|
38
39
|
const basic = Object.values<ChatMessage>(session.chats)
|
|
39
40
|
// 首先按照时间顺序排序,越早的在越前面
|
|
40
41
|
.sort((pre, next) => pre.createAt - next.createAt)
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
.filter((m) => {
|
|
43
|
+
// 过滤掉包含 topicId 的消息,有主题的消息不应该出现在聊天框中
|
|
44
|
+
if (!topicId) return !m.topicId;
|
|
45
|
+
|
|
46
|
+
// 或者当话题 id 一致时,再展示话题
|
|
47
|
+
return m.topicId === topicId;
|
|
48
|
+
})
|
|
43
49
|
// 映射头像关系
|
|
44
50
|
.map((m) => {
|
|
45
51
|
return {
|
|
@@ -3,6 +3,7 @@ import { produce } from 'immer';
|
|
|
3
3
|
import { ChatMessageMap } from '@/types/chatMessage';
|
|
4
4
|
import { MetaData } from '@/types/meta';
|
|
5
5
|
import { LobeAgentConfig, LobeAgentSession, LobeSessions } from '@/types/session';
|
|
6
|
+
import { ChatTopicMap } from '@/types/topic';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @title 添加会话
|
|
@@ -37,6 +38,19 @@ interface UpdateSessionChat {
|
|
|
37
38
|
type: 'updateSessionChat';
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @title 更新会话聊天上下文
|
|
43
|
+
*/
|
|
44
|
+
interface UpdateSessionTopic {
|
|
45
|
+
/**
|
|
46
|
+
* 会话 ID
|
|
47
|
+
*/
|
|
48
|
+
id: string;
|
|
49
|
+
topics: ChatTopicMap;
|
|
50
|
+
|
|
51
|
+
type: 'updateSessionTopic';
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
interface UpdateSessionMeta {
|
|
41
55
|
id: string;
|
|
42
56
|
key: keyof MetaData;
|
|
@@ -55,7 +69,8 @@ export type SessionDispatch =
|
|
|
55
69
|
| UpdateSessionChat
|
|
56
70
|
| RemoveSession
|
|
57
71
|
| UpdateSessionMeta
|
|
58
|
-
| UpdateSessionAgentConfig
|
|
72
|
+
| UpdateSessionAgentConfig
|
|
73
|
+
| UpdateSessionTopic;
|
|
59
74
|
|
|
60
75
|
export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
|
|
61
76
|
switch (payload.type) {
|
|
@@ -96,6 +111,15 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
|
|
|
96
111
|
});
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
case 'updateSessionTopic': {
|
|
115
|
+
return produce(state, (draft) => {
|
|
116
|
+
const chat = draft[payload.id];
|
|
117
|
+
if (!chat) return;
|
|
118
|
+
|
|
119
|
+
chat.topics = payload.topics;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
case 'updateSessionConfig': {
|
|
100
124
|
return produce(state, (draft) => {
|
|
101
125
|
const { id, config } = payload;
|
package/src/types/chatMessage.ts
CHANGED
|
@@ -14,8 +14,6 @@ export interface ChatMessageError {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface ChatMessage extends BaseDataModel {
|
|
17
|
-
archive?: boolean;
|
|
18
|
-
|
|
19
17
|
/**
|
|
20
18
|
* @title 内容
|
|
21
19
|
* @description 消息内容
|
|
@@ -43,6 +41,10 @@ export interface ChatMessage extends BaseDataModel {
|
|
|
43
41
|
* @description 消息发送者的角色
|
|
44
42
|
*/
|
|
45
43
|
role: LLMRoleType;
|
|
44
|
+
/**
|
|
45
|
+
* 保存到主题的消息
|
|
46
|
+
*/
|
|
47
|
+
topicId?: string;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export type ChatMessageMap = Record<string, ChatMessage>;
|
package/src/types/session.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChatMessageMap } from './chatMessage';
|
|
2
2
|
import { LLMExample, LLMParams, LanguageModel } from './llm';
|
|
3
3
|
import { BaseDataModel } from './meta';
|
|
4
|
+
import { ChatTopicMap } from './topic';
|
|
4
5
|
|
|
5
6
|
export enum LobeSessionType {
|
|
6
7
|
/**
|
|
@@ -18,7 +19,7 @@ interface LobeSessionBase extends BaseDataModel {
|
|
|
18
19
|
* 聊天记录
|
|
19
20
|
*/
|
|
20
21
|
chats: ChatMessageMap;
|
|
21
|
-
|
|
22
|
+
topics?: ChatTopicMap;
|
|
22
23
|
/**
|
|
23
24
|
* 每个会话的类别
|
|
24
25
|
*/
|