@lobehub/chat 0.100.5 → 0.101.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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/__mocks__/zustand/traditional.ts +25 -0
- package/package.json +7 -3
- package/src/app/api/files/image/imgur.ts +72 -0
- package/src/app/api/files/image/route.ts +42 -0
- package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
- package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
- package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +3 -3
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +38 -30
- package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
- package/src/app/chat/components/FileList/FileItem.tsx +69 -0
- package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
- package/src/app/chat/components/FileList/index.tsx +63 -0
- package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +13 -4
- package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
- package/src/app/chat/{(desktop)/features → features}/ChatInput/useSend.ts +5 -1
- package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
- package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
- package/src/config/__tests__/client.test.ts +76 -0
- package/src/config/__tests__/server.test.ts +52 -0
- package/src/config/server.ts +8 -0
- package/src/const/layoutTokens.ts +1 -1
- package/src/const/llm.ts +3 -0
- package/src/const/settings.ts +2 -0
- package/src/database/core/__tests__/db.test.ts +77 -0
- package/src/database/core/__tests__/model.test.ts +55 -0
- package/src/database/core/db.ts +25 -0
- package/src/database/core/index.ts +1 -0
- package/src/database/core/model.ts +49 -0
- package/src/database/core/schema.ts +9 -0
- package/src/database/models/file.test.ts +71 -0
- package/src/database/models/file.ts +26 -0
- package/src/locales/default/chat.ts +5 -0
- package/src/services/__tests__/file.test.ts +88 -0
- package/src/services/chatModel.ts +11 -6
- package/src/services/file.ts +36 -0
- package/src/store/files/index.ts +2 -0
- package/src/store/files/initialState.ts +7 -0
- package/src/store/files/selectors.test.ts +75 -0
- package/src/store/files/selectors.ts +34 -0
- package/src/store/files/slices/images/action.test.ts +186 -0
- package/src/store/files/slices/images/action.ts +83 -0
- package/src/store/files/slices/images/index.ts +3 -0
- package/src/store/files/slices/images/initialState.ts +11 -0
- package/src/store/files/store.ts +27 -0
- package/src/store/global/selectors/settings.test.ts +1 -3
- package/src/store/session/slices/agentConfig/selectors.ts +7 -1
- package/src/store/session/slices/chat/actions/message.ts +78 -17
- package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
- package/src/store/session/slices/chat/reducers/files.ts +37 -0
- package/src/store/session/slices/session/initialState.ts +1 -0
- package/src/store/session/slices/session/reducers/session.test.ts +98 -1
- package/src/store/session/slices/session/reducers/session.ts +26 -1
- package/src/types/chatMessage.ts +3 -2
- package/src/types/database/db.ts +4 -0
- package/src/types/database/files.ts +38 -0
- package/src/types/files.ts +8 -0
- package/src/types/openai/chat.ts +15 -1
- package/src/types/session.ts +1 -0
- package/tests/setup.ts +8 -0
- package/vitest.config.ts +2 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -63
|
@@ -206,6 +206,103 @@ describe('sessionsReducer', () => {
|
|
|
206
206
|
});
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
describe('updateSessionFiles', () => {
|
|
210
|
+
it('should update session files when valid id, key, and value are provided', () => {
|
|
211
|
+
const state: LobeSessions = {
|
|
212
|
+
'session-id': {
|
|
213
|
+
id: 'session-id',
|
|
214
|
+
config: {
|
|
215
|
+
model: 'gpt-3.5-turbo',
|
|
216
|
+
params: {},
|
|
217
|
+
systemRole: 'system-role',
|
|
218
|
+
},
|
|
219
|
+
type: 'agent',
|
|
220
|
+
meta: {
|
|
221
|
+
avatar: 'avatar-url',
|
|
222
|
+
backgroundColor: 'background-color',
|
|
223
|
+
description: 'description',
|
|
224
|
+
tags: ['tag1', 'tag2'],
|
|
225
|
+
title: 'title',
|
|
226
|
+
},
|
|
227
|
+
} as LobeAgentSession,
|
|
228
|
+
};
|
|
229
|
+
const id = 'session-id';
|
|
230
|
+
const files = ['new-file'];
|
|
231
|
+
const payload: SessionDispatch = { type: 'updateSessionFiles', id, files };
|
|
232
|
+
|
|
233
|
+
const newState = sessionsReducer(state, payload);
|
|
234
|
+
|
|
235
|
+
expect(newState).toEqual({
|
|
236
|
+
'session-id': {
|
|
237
|
+
...state['session-id'],
|
|
238
|
+
files: ['new-file'],
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should not change state when invalid id, key, and value are provided', () => {
|
|
244
|
+
const state: LobeSessions = {
|
|
245
|
+
'session-id': {
|
|
246
|
+
id: 'session-id',
|
|
247
|
+
config: {
|
|
248
|
+
model: 'gpt-3.5-turbo',
|
|
249
|
+
params: {},
|
|
250
|
+
systemRole: 'system-role',
|
|
251
|
+
},
|
|
252
|
+
type: 'agent',
|
|
253
|
+
meta: {
|
|
254
|
+
avatar: 'avatar-url',
|
|
255
|
+
backgroundColor: 'background-color',
|
|
256
|
+
description: 'description',
|
|
257
|
+
tags: ['tag1', 'tag2'],
|
|
258
|
+
title: 'title',
|
|
259
|
+
},
|
|
260
|
+
} as LobeAgentSession,
|
|
261
|
+
};
|
|
262
|
+
const id = 'non-existent-id';
|
|
263
|
+
const files = ['new-file'];
|
|
264
|
+
const payload: SessionDispatch = { type: 'updateSessionFiles', id, files };
|
|
265
|
+
|
|
266
|
+
const newState = sessionsReducer(state, payload);
|
|
267
|
+
|
|
268
|
+
expect(newState).toEqual(state);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should not change state when valid id, invalid files are provided', () => {
|
|
272
|
+
const state: LobeSessions = {
|
|
273
|
+
'session-id': {
|
|
274
|
+
id: 'session-id',
|
|
275
|
+
config: {
|
|
276
|
+
model: 'gpt-3.5-turbo',
|
|
277
|
+
params: {},
|
|
278
|
+
systemRole: 'system-role',
|
|
279
|
+
},
|
|
280
|
+
type: 'agent',
|
|
281
|
+
meta: {
|
|
282
|
+
avatar: 'avatar-url',
|
|
283
|
+
backgroundColor: 'background-color',
|
|
284
|
+
description: 'description',
|
|
285
|
+
tags: ['tag1', 'tag2'],
|
|
286
|
+
title: 'title',
|
|
287
|
+
},
|
|
288
|
+
} as LobeAgentSession,
|
|
289
|
+
};
|
|
290
|
+
const id = 'session-id';
|
|
291
|
+
|
|
292
|
+
const errFile1 = 123213412 as any;
|
|
293
|
+
const res1 = sessionsReducer(state, { type: 'updateSessionFiles', id, files: errFile1 });
|
|
294
|
+
expect(res1).toEqual(state);
|
|
295
|
+
|
|
296
|
+
const errFile2 = undefined as any;
|
|
297
|
+
const res2 = sessionsReducer(state, { type: 'updateSessionFiles', id, files: errFile2 });
|
|
298
|
+
expect(res2).toEqual(state);
|
|
299
|
+
|
|
300
|
+
const errFile3 = ['state', 123] as any;
|
|
301
|
+
const res3 = sessionsReducer(state, { type: 'updateSessionFiles', id, files: errFile3 });
|
|
302
|
+
expect(res3).toEqual(state);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
209
306
|
describe('updateSessionConfig', () => {
|
|
210
307
|
it('should update session config when valid id and partial config are provided', () => {
|
|
211
308
|
const state: LobeSessions = {
|
|
@@ -345,7 +442,7 @@ describe('sessionsReducer', () => {
|
|
|
345
442
|
});
|
|
346
443
|
});
|
|
347
444
|
|
|
348
|
-
|
|
445
|
+
it('should not change state when invalid operation type is provided', () => {
|
|
349
446
|
const state: LobeSessions = {
|
|
350
447
|
session1: {
|
|
351
448
|
id: 'session1',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { produce } from 'immer';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
import { ChatMessageMap } from '@/types/chatMessage';
|
|
4
5
|
import { MetaData } from '@/types/meta';
|
|
@@ -39,7 +40,7 @@ interface UpdateSessionChat {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
*
|
|
43
|
+
* 更新会话聊天上下文
|
|
43
44
|
*/
|
|
44
45
|
interface UpdateSessionTopic {
|
|
45
46
|
/**
|
|
@@ -51,6 +52,15 @@ interface UpdateSessionTopic {
|
|
|
51
52
|
type: 'updateSessionTopic';
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 更新会话聊天上下文
|
|
57
|
+
*/
|
|
58
|
+
interface UpdateSessionFiles {
|
|
59
|
+
files: string[];
|
|
60
|
+
id: string;
|
|
61
|
+
type: 'updateSessionFiles';
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
interface UpdateSessionMeta {
|
|
55
65
|
id: string;
|
|
56
66
|
key: keyof MetaData;
|
|
@@ -76,6 +86,7 @@ export type SessionDispatch =
|
|
|
76
86
|
| UpdateSessionMeta
|
|
77
87
|
| UpdateSessionAgentConfig
|
|
78
88
|
| UpdateSessionTopic
|
|
89
|
+
| UpdateSessionFiles
|
|
79
90
|
| ToggleSessionPinned;
|
|
80
91
|
|
|
81
92
|
export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
|
|
@@ -136,6 +147,20 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
|
|
|
136
147
|
});
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
case 'updateSessionFiles': {
|
|
151
|
+
return produce(state, (draft) => {
|
|
152
|
+
const session = draft[payload.id];
|
|
153
|
+
if (!session || !payload.files) return;
|
|
154
|
+
|
|
155
|
+
const schema = z.array(z.string());
|
|
156
|
+
|
|
157
|
+
const { success } = schema.safeParse(payload.files);
|
|
158
|
+
if (!success) return;
|
|
159
|
+
|
|
160
|
+
session.files = payload.files;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
case 'updateSessionConfig': {
|
|
140
165
|
return produce(state, (draft) => {
|
|
141
166
|
const { id, config } = payload;
|
package/src/types/chatMessage.ts
CHANGED
|
@@ -36,18 +36,19 @@ export interface ChatMessage extends BaseDataModel {
|
|
|
36
36
|
translate?: ChatTranslate;
|
|
37
37
|
} & Record<string, any>;
|
|
38
38
|
|
|
39
|
+
files?: string[];
|
|
39
40
|
/**
|
|
40
41
|
* replace with plugin
|
|
41
42
|
* @deprecated
|
|
42
43
|
*/
|
|
43
44
|
function_call?: OpenAIFunctionCall;
|
|
45
|
+
|
|
44
46
|
name?: string;
|
|
45
47
|
|
|
46
48
|
parentId?: string;
|
|
47
|
-
|
|
48
49
|
plugin?: PluginRequestPayload;
|
|
49
|
-
pluginState?: any;
|
|
50
50
|
|
|
51
|
+
pluginState?: any;
|
|
51
52
|
// 引用
|
|
52
53
|
quotaId?: string;
|
|
53
54
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const LocalFileSchema = z.object({
|
|
4
|
+
/**
|
|
5
|
+
* create Time
|
|
6
|
+
*/
|
|
7
|
+
createdAt: z.number().optional(),
|
|
8
|
+
/**
|
|
9
|
+
* file data array buffer
|
|
10
|
+
*/
|
|
11
|
+
data: z.instanceof(ArrayBuffer),
|
|
12
|
+
/**
|
|
13
|
+
* file type
|
|
14
|
+
* @example 'image/png'
|
|
15
|
+
*/
|
|
16
|
+
fileType: z.string(),
|
|
17
|
+
/**
|
|
18
|
+
* file name
|
|
19
|
+
* @example 'test.png'
|
|
20
|
+
*/
|
|
21
|
+
name: z.string(),
|
|
22
|
+
/**
|
|
23
|
+
* the mode database save the file
|
|
24
|
+
* local mean save the raw file into data
|
|
25
|
+
* url mean upload the file to a cdn and then save the url
|
|
26
|
+
*/
|
|
27
|
+
saveMode: z.enum(['local', 'url']),
|
|
28
|
+
/**
|
|
29
|
+
* file size
|
|
30
|
+
*/
|
|
31
|
+
size: z.number(),
|
|
32
|
+
/**
|
|
33
|
+
* file url if saveMode is url
|
|
34
|
+
*/
|
|
35
|
+
url: z.string().url().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type LocalFile = z.infer<typeof LocalFileSchema>;
|
package/src/types/openai/chat.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { OpenAIFunctionCall } from '@/types/chatMessage';
|
|
2
2
|
import { LLMRoleType } from '@/types/llm';
|
|
3
3
|
|
|
4
|
+
interface UserMessageContentPartText {
|
|
5
|
+
text: string;
|
|
6
|
+
type: 'text';
|
|
7
|
+
}
|
|
8
|
+
interface UserMessageContentPartImage {
|
|
9
|
+
image_url: {
|
|
10
|
+
detail?: 'auto' | 'low' | 'high';
|
|
11
|
+
url: string;
|
|
12
|
+
};
|
|
13
|
+
type: 'image_url';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type UserMessageContentPart = UserMessageContentPartText | UserMessageContentPartImage;
|
|
17
|
+
|
|
4
18
|
export interface OpenAIChatMessage {
|
|
5
19
|
/**
|
|
6
20
|
* @title 内容
|
|
7
21
|
* @description 消息内容
|
|
8
22
|
*/
|
|
9
|
-
content: string;
|
|
23
|
+
content: string | UserMessageContentPart[];
|
|
10
24
|
|
|
11
25
|
function_call?: OpenAIFunctionCall;
|
|
12
26
|
name?: string;
|
package/src/types/session.ts
CHANGED
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
// remove antd hash on test
|
|
3
|
+
import { theme } from 'antd';
|
|
4
|
+
// mock indexedDB to test with dexie
|
|
5
|
+
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
|
|
6
|
+
import 'fake-indexeddb/auto';
|
|
7
|
+
|
|
8
|
+
theme.defaultConfig.hashed = false;
|
package/vitest.config.ts
CHANGED
|
@@ -7,10 +7,12 @@ export default defineConfig({
|
|
|
7
7
|
'@': resolve(__dirname, './src'),
|
|
8
8
|
},
|
|
9
9
|
coverage: {
|
|
10
|
+
exclude: ['__mocks__/**'],
|
|
10
11
|
provider: 'v8',
|
|
11
12
|
reporter: ['text', 'json', 'lcov', 'text-summary'],
|
|
12
13
|
},
|
|
13
14
|
environment: 'jsdom',
|
|
14
15
|
globals: true,
|
|
16
|
+
setupFiles: './tests/setup.ts',
|
|
15
17
|
},
|
|
16
18
|
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
|
2
|
-
import { Button } from 'antd';
|
|
3
|
-
import { createStyles } from 'antd-style';
|
|
4
|
-
import { ArrowBigUp, CornerDownLeft, Loader2 } from 'lucide-react';
|
|
5
|
-
import { memo } from 'react';
|
|
6
|
-
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { Flexbox } from 'react-layout-kit';
|
|
8
|
-
|
|
9
|
-
import SaveTopic from '@/app/chat/features/ChatInput/Topic';
|
|
10
|
-
import { useSessionStore } from '@/store/session';
|
|
11
|
-
|
|
12
|
-
import { useSendMessage } from './useSend';
|
|
13
|
-
|
|
14
|
-
const useStyles = createStyles(({ css }) => ({
|
|
15
|
-
footerBar: css`
|
|
16
|
-
display: flex;
|
|
17
|
-
flex: none;
|
|
18
|
-
gap: 8px;
|
|
19
|
-
align-items: center;
|
|
20
|
-
justify-content: flex-end;
|
|
21
|
-
|
|
22
|
-
padding: 0 24px;
|
|
23
|
-
`,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
const Footer = memo(() => {
|
|
27
|
-
const { t } = useTranslation('chat');
|
|
28
|
-
const { styles, theme } = useStyles();
|
|
29
|
-
const [loading, onStop] = useSessionStore((s) => [!!s.chatLoadingId, s.stopGenerateMessage]);
|
|
30
|
-
|
|
31
|
-
const onSend = useSendMessage();
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className={styles.footerBar}>
|
|
35
|
-
<Flexbox
|
|
36
|
-
gap={4}
|
|
37
|
-
horizontal
|
|
38
|
-
style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
|
|
39
|
-
>
|
|
40
|
-
<Icon icon={CornerDownLeft} />
|
|
41
|
-
<span>{t('send')}</span>
|
|
42
|
-
<span>/</span>
|
|
43
|
-
<Flexbox horizontal>
|
|
44
|
-
<Icon icon={ArrowBigUp} />
|
|
45
|
-
<Icon icon={CornerDownLeft} />
|
|
46
|
-
</Flexbox>
|
|
47
|
-
<span>{t('warp')}</span>
|
|
48
|
-
</Flexbox>
|
|
49
|
-
<SaveTopic />
|
|
50
|
-
{loading ? (
|
|
51
|
-
<Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop}>
|
|
52
|
-
{t('stop')}
|
|
53
|
-
</Button>
|
|
54
|
-
) : (
|
|
55
|
-
<Button onClick={() => onSend()} type={'primary'}>
|
|
56
|
-
{t('send')}
|
|
57
|
-
</Button>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
export default Footer;
|