@lobehub/chat 0.6.1 → 0.7.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/.eslintrc.js +1 -0
- package/CHANGELOG.md +25 -0
- package/package.json +2 -1
- package/src/pages/chat/[id]/Conversation/ChatList.tsx +3 -0
- package/src/pages/chat/[id]/Conversation/MessageExtra.tsx +37 -0
- package/src/store/session/slices/chat/action.ts +9 -1
- package/src/store/session/slices/chat/messageReducer.test.ts +227 -70
- package/src/store/session/slices/chat/messageReducer.ts +28 -1
- package/src/store/session/slices/chat/selectors/chat.ts +1 -0
- package/vitest.config.ts +4 -3
package/.eslintrc.js
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 0.7.0](https://github.com/lobehub/lobe-chat/compare/v0.6.1...v0.7.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-07-22**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: 支持展示来自模型的标记信息.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: 支持展示来自模型的标记信息 ([e27fae9](https://github.com/lobehub/lobe-chat/commit/e27fae9))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.6.1](https://github.com/lobehub/lobe-chat/compare/v0.6.0...v0.6.1)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2023-07-22**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatbot",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"react-layout-kit": "^1",
|
|
91
91
|
"swr": "^2",
|
|
92
92
|
"ts-md5": "^1",
|
|
93
|
+
"uuid": "^9",
|
|
93
94
|
"zustand": "^4",
|
|
94
95
|
"zustand-utils": "^1"
|
|
95
96
|
},
|
|
@@ -5,6 +5,8 @@ import { shallow } from 'zustand/shallow';
|
|
|
5
5
|
|
|
6
6
|
import { chatSelectors, useSessionStore } from '@/store/session';
|
|
7
7
|
|
|
8
|
+
import MessageExtra from './MessageExtra';
|
|
9
|
+
|
|
8
10
|
const List = () => {
|
|
9
11
|
const data = useSessionStore(chatSelectors.currentChats, isEqual);
|
|
10
12
|
const [deleteMessage, resendMessage] = useSessionStore(
|
|
@@ -28,6 +30,7 @@ const List = () => {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
}}
|
|
33
|
+
renderMessageExtra={MessageExtra}
|
|
31
34
|
style={{ marginTop: 24 }}
|
|
32
35
|
/>
|
|
33
36
|
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Tag } from 'antd';
|
|
2
|
+
import { createStyles } from 'antd-style';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { Flexbox } from 'react-layout-kit';
|
|
5
|
+
import { shallow } from 'zustand/shallow';
|
|
6
|
+
|
|
7
|
+
import { agentSelectors, useSessionStore } from '@/store/session';
|
|
8
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
9
|
+
|
|
10
|
+
const useStyles = createStyles(({ css }) => ({
|
|
11
|
+
container: css`
|
|
12
|
+
margin-top: 8px;
|
|
13
|
+
`,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const MessageExtra = ({ role, extra }: ChatMessage): ReactNode => {
|
|
17
|
+
const { styles } = useStyles();
|
|
18
|
+
|
|
19
|
+
const [model] = useSessionStore((s) => [agentSelectors.currentAgentModel(s)], shallow);
|
|
20
|
+
|
|
21
|
+
// 只有 ai 的 message 才会需要展示
|
|
22
|
+
if (role !== 'assistant') return;
|
|
23
|
+
// 只有当 当前的 model 和 fromModel 不一致时,才需要展示
|
|
24
|
+
if (extra?.fromModel && model === extra?.fromModel) return;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Flexbox className={styles.container}>
|
|
28
|
+
<div>
|
|
29
|
+
<Tag bordered={false} style={{ borderRadius: 6 }}>
|
|
30
|
+
{extra?.fromModel}
|
|
31
|
+
</Tag>
|
|
32
|
+
</div>
|
|
33
|
+
</Flexbox>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default MessageExtra;
|
|
@@ -119,7 +119,7 @@ export const createChatSlice: StateCreator<
|
|
|
119
119
|
const { dispatchMessage, generateMessage } = get();
|
|
120
120
|
|
|
121
121
|
// 添加 systemRole
|
|
122
|
-
const { systemRole } = agentSelectors.currentAgentConfigSafe(get());
|
|
122
|
+
const { systemRole, model } = agentSelectors.currentAgentConfigSafe(get());
|
|
123
123
|
if (systemRole) {
|
|
124
124
|
messages.unshift({ content: systemRole, role: 'system' } as ChatMessage);
|
|
125
125
|
}
|
|
@@ -137,6 +137,14 @@ export const createChatSlice: StateCreator<
|
|
|
137
137
|
type: 'addMessage',
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
// 为模型添加 fromModel 的额外信息
|
|
141
|
+
dispatchMessage({
|
|
142
|
+
id: assistantId,
|
|
143
|
+
key: 'fromModel',
|
|
144
|
+
type: 'updateMessageExtra',
|
|
145
|
+
value: model,
|
|
146
|
+
});
|
|
147
|
+
|
|
140
148
|
let output = '';
|
|
141
149
|
// 生成 ai message
|
|
142
150
|
await generateMessage(messages, {
|
|
@@ -1,70 +1,227 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
import { ChatMessageMap } from '@/types/chatMessage';
|
|
2
|
+
|
|
3
|
+
import { MessageDispatch, messagesReducer } from './messageReducer';
|
|
4
|
+
|
|
5
|
+
describe('messagesReducer', () => {
|
|
6
|
+
let initialState: ChatMessageMap;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
initialState = {
|
|
10
|
+
message1: {
|
|
11
|
+
id: 'message1',
|
|
12
|
+
content: 'Hello World',
|
|
13
|
+
createAt: 1629264000000,
|
|
14
|
+
updateAt: 1629264000000,
|
|
15
|
+
role: 'user',
|
|
16
|
+
},
|
|
17
|
+
message2: {
|
|
18
|
+
id: 'message2',
|
|
19
|
+
content: 'How are you?',
|
|
20
|
+
createAt: 1629264000000,
|
|
21
|
+
updateAt: 1629264000000,
|
|
22
|
+
role: 'system',
|
|
23
|
+
},
|
|
24
|
+
} as unknown as ChatMessageMap;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('addMessage', () => {
|
|
28
|
+
it('should add a new message to the state', () => {
|
|
29
|
+
const payload: MessageDispatch = {
|
|
30
|
+
type: 'addMessage',
|
|
31
|
+
message: 'New Message',
|
|
32
|
+
role: 'user',
|
|
33
|
+
id: 'message3',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const newState = messagesReducer(initialState, payload);
|
|
37
|
+
|
|
38
|
+
expect(Object.keys(newState)).toHaveLength(3);
|
|
39
|
+
expect(newState).toHaveProperty('message1');
|
|
40
|
+
expect(newState).toHaveProperty('message2');
|
|
41
|
+
expect(newState).toHaveProperty('message3');
|
|
42
|
+
expect(newState.message3).toEqual({
|
|
43
|
+
id: 'message3',
|
|
44
|
+
content: 'New Message',
|
|
45
|
+
meta: {},
|
|
46
|
+
createAt: expect.any(Number),
|
|
47
|
+
updateAt: expect.any(Number),
|
|
48
|
+
role: 'user',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should use the provided id when adding a new message', () => {
|
|
53
|
+
const payload: MessageDispatch = {
|
|
54
|
+
type: 'addMessage',
|
|
55
|
+
id: 'customId',
|
|
56
|
+
message: 'New Message',
|
|
57
|
+
role: 'user',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const newState = messagesReducer(initialState, payload);
|
|
61
|
+
|
|
62
|
+
expect(Object.keys(newState)).toHaveLength(3);
|
|
63
|
+
expect(newState).toHaveProperty('message1');
|
|
64
|
+
expect(newState).toHaveProperty('message2');
|
|
65
|
+
expect(newState).toHaveProperty('customId');
|
|
66
|
+
expect(newState.customId).toEqual({
|
|
67
|
+
id: 'customId',
|
|
68
|
+
content: 'New Message',
|
|
69
|
+
meta: {},
|
|
70
|
+
createAt: expect.any(Number),
|
|
71
|
+
updateAt: expect.any(Number),
|
|
72
|
+
role: 'user',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should use the provided parentId when adding a new message', () => {
|
|
77
|
+
const payload: MessageDispatch = {
|
|
78
|
+
type: 'addMessage',
|
|
79
|
+
message: 'New Message',
|
|
80
|
+
id: 'message3',
|
|
81
|
+
role: 'user',
|
|
82
|
+
parentId: 'message1',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const newState = messagesReducer(initialState, payload);
|
|
86
|
+
|
|
87
|
+
expect(newState.message3).toEqual({
|
|
88
|
+
id: 'message3',
|
|
89
|
+
content: 'New Message',
|
|
90
|
+
meta: {},
|
|
91
|
+
createAt: expect.any(Number),
|
|
92
|
+
updateAt: expect.any(Number),
|
|
93
|
+
role: 'user',
|
|
94
|
+
parentId: 'message1',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should use the provided quotaId when adding a new message', () => {
|
|
99
|
+
const payload: MessageDispatch = {
|
|
100
|
+
type: 'addMessage',
|
|
101
|
+
message: 'New Message',
|
|
102
|
+
id: 'message3',
|
|
103
|
+
role: 'user',
|
|
104
|
+
quotaId: 'message2',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const newState = messagesReducer(initialState, payload);
|
|
108
|
+
|
|
109
|
+
expect(newState.message3).toEqual({
|
|
110
|
+
id: 'message3',
|
|
111
|
+
meta: {},
|
|
112
|
+
content: 'New Message',
|
|
113
|
+
createAt: expect.any(Number),
|
|
114
|
+
updateAt: expect.any(Number),
|
|
115
|
+
role: 'user',
|
|
116
|
+
quotaId: 'message2',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('deleteMessage', () => {
|
|
122
|
+
it('should remove the specified message from the state', () => {
|
|
123
|
+
const payload: MessageDispatch = {
|
|
124
|
+
type: 'deleteMessage',
|
|
125
|
+
id: 'message1',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const newState = messagesReducer(initialState, payload);
|
|
129
|
+
|
|
130
|
+
expect(Object.keys(newState)).toHaveLength(1);
|
|
131
|
+
expect(newState).not.toHaveProperty('message1');
|
|
132
|
+
expect(newState).toHaveProperty('message2');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
136
|
+
const payload: MessageDispatch = {
|
|
137
|
+
type: 'deleteMessage',
|
|
138
|
+
id: 'nonexistentMessage',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const newState = messagesReducer(initialState, payload);
|
|
142
|
+
|
|
143
|
+
expect(newState).toEqual(initialState);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('updateMessage', () => {
|
|
148
|
+
it('should update the specified message with the provided value', () => {
|
|
149
|
+
const payload: MessageDispatch = {
|
|
150
|
+
type: 'updateMessage',
|
|
151
|
+
id: 'message1',
|
|
152
|
+
key: 'content',
|
|
153
|
+
value: 'Updated Message',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const newState = messagesReducer(initialState, payload);
|
|
157
|
+
|
|
158
|
+
expect(newState.message1.content).toBe('Updated Message');
|
|
159
|
+
expect(newState.message1.updateAt).toBeGreaterThan(initialState.message1.updateAt);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
163
|
+
const payload: MessageDispatch = {
|
|
164
|
+
type: 'updateMessage',
|
|
165
|
+
id: 'nonexistentMessage',
|
|
166
|
+
key: 'content',
|
|
167
|
+
value: 'Updated Message',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const newState = messagesReducer(initialState, payload);
|
|
171
|
+
|
|
172
|
+
expect(newState).toEqual(initialState);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('updateMessageExtra', () => {
|
|
177
|
+
it('should update the specified message extra property with the provided value', () => {
|
|
178
|
+
const payload: MessageDispatch = {
|
|
179
|
+
type: 'updateMessageExtra',
|
|
180
|
+
id: 'message1',
|
|
181
|
+
key: 'translate',
|
|
182
|
+
value: { target: 'en', to: 'zh' },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const newState = messagesReducer(initialState, payload);
|
|
186
|
+
|
|
187
|
+
expect(newState.message1.extra!.translate).toEqual({ target: 'en', to: 'zh' });
|
|
188
|
+
expect(newState.message1.updateAt).toBeGreaterThan(initialState.message1.updateAt);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
192
|
+
const payload: MessageDispatch = {
|
|
193
|
+
type: 'updateMessageExtra',
|
|
194
|
+
id: 'nonexistentMessage',
|
|
195
|
+
key: 'translate',
|
|
196
|
+
value: { target: 'en', to: 'zh' },
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const newState = messagesReducer(initialState, payload);
|
|
200
|
+
|
|
201
|
+
expect(newState).toEqual(initialState);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('resetMessages', () => {
|
|
206
|
+
it('should reset the state to an empty object', () => {
|
|
207
|
+
const payload: MessageDispatch = {
|
|
208
|
+
type: 'resetMessages',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const newState = messagesReducer(initialState, payload);
|
|
212
|
+
|
|
213
|
+
expect(newState).toEqual({});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('unimplemented type', () => {
|
|
218
|
+
it('should throw an error when an unimplemented type is provided', () => {
|
|
219
|
+
// @ts-ignore
|
|
220
|
+
const payload: MessageDispatch = { type: 'unimplementedType' };
|
|
221
|
+
|
|
222
|
+
expect(() => messagesReducer(initialState, payload)).toThrowError(
|
|
223
|
+
'暂未实现的 type,请检查 reducer',
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -30,8 +30,19 @@ interface UpdateMessage {
|
|
|
30
30
|
type: 'updateMessage';
|
|
31
31
|
value: ChatMessage[keyof ChatMessage];
|
|
32
32
|
}
|
|
33
|
+
interface UpdateMessageExtra {
|
|
34
|
+
id: string;
|
|
35
|
+
key: string;
|
|
36
|
+
type: 'updateMessageExtra';
|
|
37
|
+
value: any;
|
|
38
|
+
}
|
|
33
39
|
|
|
34
|
-
export type MessageDispatch =
|
|
40
|
+
export type MessageDispatch =
|
|
41
|
+
| AddMessage
|
|
42
|
+
| DeleteMessage
|
|
43
|
+
| ResetMessages
|
|
44
|
+
| UpdateMessage
|
|
45
|
+
| UpdateMessageExtra;
|
|
35
46
|
|
|
36
47
|
export const messagesReducer = (
|
|
37
48
|
state: ChatMessageMap,
|
|
@@ -73,6 +84,22 @@ export const messagesReducer = (
|
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
case 'updateMessageExtra': {
|
|
88
|
+
return produce(state, (draftState) => {
|
|
89
|
+
const { id, key, value } = payload;
|
|
90
|
+
const message = draftState[id];
|
|
91
|
+
if (!message) return;
|
|
92
|
+
|
|
93
|
+
if (!message.extra) {
|
|
94
|
+
message.extra = { [key]: value } as any;
|
|
95
|
+
} else {
|
|
96
|
+
message.extra[key] = value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
message.updateAt = Date.now();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
case 'resetMessages': {
|
|
77
104
|
return {};
|
|
78
105
|
}
|
package/vitest.config.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
1
2
|
import { defineConfig } from 'vitest/config';
|
|
2
3
|
|
|
3
4
|
export default defineConfig({
|
|
4
5
|
test: {
|
|
5
|
-
environment: 'jsdom',
|
|
6
|
-
globals: true,
|
|
7
6
|
alias: {
|
|
8
|
-
'@': './src',
|
|
7
|
+
'@': resolve(__dirname, './src'),
|
|
9
8
|
},
|
|
9
|
+
environment: 'jsdom',
|
|
10
|
+
globals: true,
|
|
10
11
|
},
|
|
11
12
|
});
|