@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.
Files changed (68) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +2 -2
  5. package/__mocks__/zustand/traditional.ts +25 -0
  6. package/package.json +7 -3
  7. package/src/app/api/files/image/imgur.ts +72 -0
  8. package/src/app/api/files/image/route.ts +42 -0
  9. package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
  10. package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
  11. package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
  12. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
  13. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +3 -3
  14. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +38 -30
  15. package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
  16. package/src/app/chat/components/FileList/FileItem.tsx +69 -0
  17. package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
  18. package/src/app/chat/components/FileList/index.tsx +63 -0
  19. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +13 -4
  20. package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
  21. package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
  22. package/src/app/chat/{(desktop)/features → features}/ChatInput/useSend.ts +5 -1
  23. package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
  24. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
  25. package/src/config/__tests__/client.test.ts +76 -0
  26. package/src/config/__tests__/server.test.ts +52 -0
  27. package/src/config/server.ts +8 -0
  28. package/src/const/layoutTokens.ts +1 -1
  29. package/src/const/llm.ts +3 -0
  30. package/src/const/settings.ts +2 -0
  31. package/src/database/core/__tests__/db.test.ts +77 -0
  32. package/src/database/core/__tests__/model.test.ts +55 -0
  33. package/src/database/core/db.ts +25 -0
  34. package/src/database/core/index.ts +1 -0
  35. package/src/database/core/model.ts +49 -0
  36. package/src/database/core/schema.ts +9 -0
  37. package/src/database/models/file.test.ts +71 -0
  38. package/src/database/models/file.ts +26 -0
  39. package/src/locales/default/chat.ts +5 -0
  40. package/src/services/__tests__/file.test.ts +88 -0
  41. package/src/services/chatModel.ts +11 -6
  42. package/src/services/file.ts +36 -0
  43. package/src/store/files/index.ts +2 -0
  44. package/src/store/files/initialState.ts +7 -0
  45. package/src/store/files/selectors.test.ts +75 -0
  46. package/src/store/files/selectors.ts +34 -0
  47. package/src/store/files/slices/images/action.test.ts +186 -0
  48. package/src/store/files/slices/images/action.ts +83 -0
  49. package/src/store/files/slices/images/index.ts +3 -0
  50. package/src/store/files/slices/images/initialState.ts +11 -0
  51. package/src/store/files/store.ts +27 -0
  52. package/src/store/global/selectors/settings.test.ts +1 -3
  53. package/src/store/session/slices/agentConfig/selectors.ts +7 -1
  54. package/src/store/session/slices/chat/actions/message.ts +78 -17
  55. package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
  56. package/src/store/session/slices/chat/reducers/files.ts +37 -0
  57. package/src/store/session/slices/session/initialState.ts +1 -0
  58. package/src/store/session/slices/session/reducers/session.test.ts +98 -1
  59. package/src/store/session/slices/session/reducers/session.ts +26 -1
  60. package/src/types/chatMessage.ts +3 -2
  61. package/src/types/database/db.ts +4 -0
  62. package/src/types/database/files.ts +38 -0
  63. package/src/types/files.ts +8 -0
  64. package/src/types/openai/chat.ts +15 -1
  65. package/src/types/session.ts +1 -0
  66. package/tests/setup.ts +8 -0
  67. package/vitest.config.ts +2 -0
  68. 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
- test('should not change state when invalid operation type is provided', () => {
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
- * @title 更新会话聊天上下文
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;
@@ -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,4 @@
1
+ export type DBModel<T> = T & {
2
+ createdAt: number;
3
+ id: string;
4
+ };
@@ -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>;
@@ -0,0 +1,8 @@
1
+ import { LocalFile } from './database/files';
2
+
3
+ export interface FilePreview extends Pick<LocalFile, 'saveMode' | 'fileType'> {
4
+ base64Url?: string;
5
+ data?: ArrayBuffer;
6
+ name: string;
7
+ url: string;
8
+ }
@@ -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;
@@ -19,6 +19,7 @@ interface LobeSessionBase extends BaseDataModel {
19
19
  * 聊天记录
20
20
  */
21
21
  chats: ChatMessageMap;
22
+ files?: string[];
22
23
  /**
23
24
  * 置顶
24
25
  */
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;