@lobehub/chat 0.6.0 → 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 CHANGED
@@ -3,4 +3,5 @@ const config = require('@lobehub/lint').eslint;
3
3
  config.extends.push('plugin:@next/next/recommended');
4
4
  //config.extends.push('plugin:@next/next/core-web-vitals');
5
5
 
6
+ config.rules['unicorn/no-negated-condition'] = 0;
6
7
  module.exports = config;
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 0.6.1](https://github.com/lobehub/lobe-chat/compare/v0.6.0...v0.6.1)
31
+
32
+ <sup>Released on **2023-07-22**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Add deps.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Add deps ([3dc45fe](https://github.com/lobehub/lobe-chat/commit/3dc45fe))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 0.6.0](https://github.com/lobehub/lobe-chat/compare/v0.5.0...v0.6.0)
6
56
 
7
57
  <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.6.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",
@@ -71,6 +71,7 @@
71
71
  "antd-style": "^3",
72
72
  "brotli-wasm": "^1",
73
73
  "chroma-js": "^2",
74
+ "emoji-mart": "^5",
74
75
  "fast-deep-equal": "^3",
75
76
  "gpt-tokenizer": "^2",
76
77
  "i18next": "^23",
@@ -89,6 +90,7 @@
89
90
  "react-layout-kit": "^1",
90
91
  "swr": "^2",
91
92
  "ts-md5": "^1",
93
+ "uuid": "^9",
92
94
  "zustand": "^4",
93
95
  "zustand-utils": "^1"
94
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
- test('placeholder', () => {});
2
- // describe('messagesReducer', () => {
3
- // let initialState: ChatMessage[];
4
- //
5
- // beforeEach(() => {
6
- // initialState = [
7
- // { role: 'user', content: 'Hello!' },
8
- // { role: 'assistant', content: 'Hi there!' },
9
- // ];
10
- // });
11
- //
12
- // it('should add a message', () => {
13
- // const newMessage: ChatMessage = { role: 'user', content: 'How are you?' };
14
- // const action: MessageDispatch = { type: 'addMessage', message: newMessage };
15
- // const newState = messagesReducer(initialState, action);
16
- // expect(newState).toEqual([...initialState, newMessage]);
17
- // });
18
- //
19
- // it('should delete a message', () => {
20
- // const action: MessageDispatch = { type: 'deleteMessage', index: 1 };
21
- // const newState = messagesReducer(initialState, action);
22
- // expect(newState).toEqual([{ role: 'user', content: 'Hello!' }]);
23
- // });
24
- //
25
- // it('should update a message', () => {
26
- // const action: MessageDispatch = { type: 'updateMessage', index: 1, message: 'I am fine!' };
27
- // const newState = messagesReducer(initialState, action);
28
- // expect(newState).toEqual([
29
- // { role: 'user', content: 'Hello!' },
30
- // { role: 'assistant', content: 'I am fine!' },
31
- // ]);
32
- // });
33
- //
34
- // it('should add a user message', () => {
35
- // const action: MessageDispatch = { type: 'addUserMessage', message: 'Goodbye!' };
36
- // const newState = messagesReducer(initialState, action);
37
- // expect(newState).toEqual([
38
- // { role: 'user', content: 'Hello!' },
39
- // { role: 'assistant', content: 'Hi there!' },
40
- // { role: 'user', content: 'Goodbye!' },
41
- // ]);
42
- // });
43
- //
44
- // it('should set error message correctly', () => {
45
- // const action: MessageDispatch = {
46
- // type: 'setErrorMessage',
47
- // error: { message: 'Not Found', status: 404, type: 'chatbot' },
48
- // index: 0,
49
- // };
50
- // const newState = messagesReducer(initialState, action);
51
- // expect(newState).toEqual([
52
- // {
53
- // role: 'user',
54
- // content: 'Hello!',
55
- // error: { message: 'Not Found', status: 404, type: 'chatbot' },
56
- // },
57
- // { role: 'assistant', content: 'Hi there!' },
58
- // ]);
59
- // });
60
- //
61
- // it('should update the latest bot message', () => {
62
- // const responseStream = ['I', ' am', ' a', ' bot.'];
63
- // const action: MessageDispatch = { type: 'updateLatestBotMessage', responseStream };
64
- // const newState = messagesReducer(initialState, action);
65
- // expect(newState).toEqual([
66
- // { role: 'user', content: 'Hello!' },
67
- // { role: 'assistant', content: 'I am a bot.' },
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 = AddMessage | DeleteMessage | ResetMessages | UpdateMessage;
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
  }
@@ -37,6 +37,7 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
37
37
  }
38
38
  };
39
39
 
40
+ // 基于添加逻辑进行重排序
40
41
  for (const item of basic) {
41
42
  // 先判存在与否,不存在就加入
42
43
  addItem(item);
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
  });