@lobehub/chat 0.100.5 → 0.101.1

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 (83) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +50 -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/app/market/(desktop)/features/AgentDetail.tsx +14 -9
  26. package/src/app/market/(desktop)/index.tsx +2 -3
  27. package/src/app/market/(mobile)/index.tsx +2 -3
  28. package/src/app/market/features/AgentCard/AgentCardItem.tsx +4 -1
  29. package/src/app/market/features/AgentCard/index.tsx +20 -31
  30. package/src/app/market/features/AgentDetailContent/AgentInfo/Header.tsx +0 -1
  31. package/src/app/market/features/AgentDetailContent/AgentInfo/index.tsx +2 -3
  32. package/src/app/market/features/AgentSearchBar/index.tsx +9 -4
  33. package/src/app/market/features/TagList/Inner.tsx +28 -0
  34. package/src/app/market/features/TagList/index.tsx +12 -23
  35. package/src/config/__tests__/client.test.ts +76 -0
  36. package/src/config/__tests__/server.test.ts +52 -0
  37. package/src/config/server.ts +8 -0
  38. package/src/const/layoutTokens.ts +1 -1
  39. package/src/const/llm.ts +3 -0
  40. package/src/const/settings.ts +2 -0
  41. package/src/database/core/__tests__/db.test.ts +77 -0
  42. package/src/database/core/__tests__/model.test.ts +55 -0
  43. package/src/database/core/db.ts +25 -0
  44. package/src/database/core/index.ts +1 -0
  45. package/src/database/core/model.ts +49 -0
  46. package/src/database/core/schema.ts +9 -0
  47. package/src/database/models/file.test.ts +71 -0
  48. package/src/database/models/file.ts +26 -0
  49. package/src/locales/default/chat.ts +5 -0
  50. package/src/services/__tests__/file.test.ts +88 -0
  51. package/src/services/chatModel.ts +11 -6
  52. package/src/services/file.ts +36 -0
  53. package/src/store/files/index.ts +2 -0
  54. package/src/store/files/initialState.ts +7 -0
  55. package/src/store/files/selectors.test.ts +75 -0
  56. package/src/store/files/selectors.ts +34 -0
  57. package/src/store/files/slices/images/action.test.ts +186 -0
  58. package/src/store/files/slices/images/action.ts +83 -0
  59. package/src/store/files/slices/images/index.ts +3 -0
  60. package/src/store/files/slices/images/initialState.ts +11 -0
  61. package/src/store/files/store.ts +27 -0
  62. package/src/store/global/selectors/settings.test.ts +1 -3
  63. package/src/store/market/action.ts +9 -1
  64. package/src/store/market/initialState.ts +2 -0
  65. package/src/store/market/selectors.ts +10 -8
  66. package/src/store/session/slices/agentConfig/selectors.ts +7 -1
  67. package/src/store/session/slices/chat/actions/message.ts +78 -17
  68. package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
  69. package/src/store/session/slices/chat/reducers/files.ts +37 -0
  70. package/src/store/session/slices/session/initialState.ts +1 -0
  71. package/src/store/session/slices/session/reducers/session.test.ts +98 -1
  72. package/src/store/session/slices/session/reducers/session.ts +26 -1
  73. package/src/types/chatMessage.ts +3 -2
  74. package/src/types/database/db.ts +4 -0
  75. package/src/types/database/files.ts +38 -0
  76. package/src/types/files.ts +8 -0
  77. package/src/types/market.ts +1 -0
  78. package/src/types/openai/chat.ts +15 -1
  79. package/src/types/session.ts +1 -0
  80. package/tests/setup.ts +8 -0
  81. package/vitest.config.ts +2 -0
  82. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -63
  83. package/src/utils/findDuplicates.ts +0 -22
@@ -1,17 +1,19 @@
1
- import { flatten } from 'lodash-es';
2
-
3
1
  import { DEFAULT_AGENTS_MARKET_ITEM } from '@/const/market';
4
2
  import { AgentsMarketItem } from '@/types/market';
5
- import { findDuplicates } from '@/utils/findDuplicates';
6
3
 
7
4
  import type { Store } from './store';
8
5
 
9
- const getAgentList = (s: Store) => s.agentList;
10
- const getAgentTagList = (s: Store) => {
11
- const agentList = s.agentList;
12
- const rawAgentTagList = flatten(agentList.map((item) => item.meta.tags)) as string[];
13
- return findDuplicates(rawAgentTagList);
6
+ const getAgentList = (s: Store) => {
7
+ const { searchKeywords, agentList } = s;
8
+ if (!searchKeywords) return agentList;
9
+ return agentList.filter(({ meta }) => {
10
+ const checkMeta: string = [meta.tags, meta.title, meta.description, meta.avatar]
11
+ .filter(Boolean)
12
+ .join('');
13
+ return checkMeta.toLowerCase().includes(searchKeywords.toLowerCase());
14
+ });
14
15
  };
16
+ const getAgentTagList = (s: Store) => s.tagList;
15
17
 
16
18
  const getAgentItemById = (d: string) => (s: Store) => s.agentMap[d];
17
19
 
@@ -1,6 +1,6 @@
1
1
  import { t } from 'i18next';
2
2
 
3
- import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm';
3
+ import { DEFAULT_OPENAI_MODEL_LIST, VISION_MODEL_WHITE_LIST } from '@/const/llm';
4
4
  import { DEFAULT_AVATAR, DEFAULT_BACKGROUND_COLOR } from '@/const/meta';
5
5
  import { SessionStore } from '@/store/session';
6
6
  import { LanguageModel } from '@/types/llm';
@@ -49,6 +49,11 @@ const currentAgentModel = (s: SessionStore): LanguageModel | string => {
49
49
  return config?.model || LanguageModel.GPT3_5;
50
50
  };
51
51
 
52
+ const modelHasVisionAbility = (s: SessionStore): boolean => {
53
+ const model = currentAgentModel(s);
54
+ return VISION_MODEL_WHITE_LIST.includes(model);
55
+ };
56
+
52
57
  const currentAgentPlugins = (s: SessionStore) => {
53
58
  const config = currentAgentConfig(s);
54
59
 
@@ -87,5 +92,6 @@ export const agentSelectors = {
87
92
  getDescription,
88
93
  getTitle,
89
94
  hasSystemRole,
95
+ modelHasVisionAbility,
90
96
  showTokenTag,
91
97
  };
@@ -1,10 +1,14 @@
1
1
  import { template } from 'lodash-es';
2
2
  import { StateCreator } from 'zustand/vanilla';
3
3
 
4
+ import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
4
5
  import { LOADING_FLAT } from '@/const/message';
6
+ import { VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/settings';
5
7
  import { fetchChatModel } from '@/services/chatModel';
8
+ import { filesSelectors, useFileStore } from '@/store/files';
6
9
  import { SessionStore } from '@/store/session';
7
10
  import { ChatMessage } from '@/types/chatMessage';
11
+ import { OpenAIChatMessage, UserMessageContentPart } from '@/types/openai/chat';
8
12
  import { fetchSSE } from '@/utils/fetch';
9
13
  import { isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/utils/message';
10
14
  import { setNamespace } from '@/utils/storeDebug';
@@ -12,6 +16,7 @@ import { nanoid } from '@/utils/uuid';
12
16
 
13
17
  import { agentSelectors } from '../../agentConfig/selectors';
14
18
  import { sessionSelectors } from '../../session/selectors';
19
+ import { FileDispatch, filesReducer } from '../reducers/files';
15
20
  import { MessageDispatch, messagesReducer } from '../reducers/message';
16
21
  import { chatSelectors } from '../selectors';
17
22
  import { getSlicedMessagesWithConfig } from '../utils';
@@ -37,6 +42,10 @@ export interface ChatMessageAction {
37
42
  * @param id - 消息 ID
38
43
  */
39
44
  deleteMessage: (id: string) => void;
45
+ /**
46
+ * agent files dispatch method
47
+ */
48
+ dispatchAgentFile: (payload: FileDispatch) => void;
40
49
  /**
41
50
  * 分发消息
42
51
  * @param payload - 消息分发参数
@@ -66,7 +75,7 @@ export interface ChatMessageAction {
66
75
  * 发送消息
67
76
  * @param text - 消息文本
68
77
  */
69
- sendMessage: (text: string) => Promise<void>;
78
+ sendMessage: (text: string, images?: { id: string; url: string }[]) => Promise<void>;
70
79
  stopGenerateMessage: () => void;
71
80
  toggleChatLoading: (
72
81
  loading: boolean,
@@ -164,6 +173,15 @@ export const chatMessage: StateCreator<
164
173
  get().dispatchMessage({ id, type: 'deleteMessage' });
165
174
  },
166
175
 
176
+ dispatchAgentFile: (payload) => {
177
+ const { activeId } = get();
178
+ const session = sessionSelectors.currentSession(get());
179
+ if (!activeId || !session) return;
180
+
181
+ const files = filesReducer(session.files || [], payload);
182
+
183
+ get().dispatchSession({ files, id: activeId, type: 'updateSessionFiles' });
184
+ },
167
185
  dispatchMessage: (payload) => {
168
186
  const { activeId } = get();
169
187
  const session = sessionSelectors.currentSession(get());
@@ -173,6 +191,7 @@ export const chatMessage: StateCreator<
173
191
 
174
192
  get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' });
175
193
  },
194
+
176
195
  fetchAIChatMessage: async (messages, assistantId) => {
177
196
  const { dispatchMessage, toggleChatLoading } = get();
178
197
 
@@ -186,17 +205,17 @@ export const chatMessage: StateCreator<
186
205
 
187
206
  const compiler = template(config.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
188
207
 
189
- // ========================== //
190
- // 对 messages 做统一预处理 //
191
- // ========================== //
208
+ // ================================== //
209
+ // messages uniformly preprocess //
210
+ // ================================== //
192
211
 
193
- // 1. 按参数设定截断长度
194
- const slicedMessages = getSlicedMessagesWithConfig(messages, config);
212
+ // 1. slice messages with config
213
+ let preprocessMsgs = getSlicedMessagesWithConfig(messages, config);
195
214
 
196
- // 2. 替换 inputMessage 模板
197
- const postMessages = !config.inputTemplate
198
- ? slicedMessages
199
- : slicedMessages.map((m) => {
215
+ // 2. replace inputMessage template
216
+ preprocessMsgs = !config.inputTemplate
217
+ ? preprocessMsgs
218
+ : preprocessMsgs.map((m) => {
200
219
  if (m.role === 'user') {
201
220
  try {
202
221
  return { ...m, content: compiler({ text: m.content }) };
@@ -206,12 +225,41 @@ export const chatMessage: StateCreator<
206
225
  return m;
207
226
  }
208
227
  }
228
+
209
229
  return m;
210
230
  });
211
231
 
212
- // 3. 添加 systemRole
232
+ // 3. add systemRole
213
233
  if (config.systemRole) {
214
- postMessages.unshift({ content: config.systemRole, role: 'system' } as ChatMessage);
234
+ preprocessMsgs.unshift({ content: config.systemRole, role: 'system' } as ChatMessage);
235
+ }
236
+
237
+ let postMessages: OpenAIChatMessage[] = preprocessMsgs;
238
+
239
+ // 4. handle content type for vision model
240
+ // for the models with visual ability, add image url to content
241
+ // refs: https://platform.openai.com/docs/guides/vision/quick-start
242
+ if (VISION_MODEL_WHITE_LIST.includes(config.model)) {
243
+ postMessages = preprocessMsgs.map((m) => {
244
+ if (!m.files) return m;
245
+
246
+ const imageList = filesSelectors.getImageUrlOrBase64ByList(m.files)(
247
+ useFileStore.getState(),
248
+ );
249
+
250
+ if (imageList.length === 0) return m;
251
+
252
+ const content: UserMessageContentPart[] = [
253
+ { text: m.content, type: 'text' },
254
+ ...imageList.map(
255
+ (i) => ({ image_url: { detail: 'auto', url: i.url }, type: 'image_url' }) as const,
256
+ ),
257
+ ];
258
+ return { ...m, content };
259
+ });
260
+
261
+ // due to vision model's default max_tokens is very small, we need to set the max_tokens a larger one.
262
+ if (!config.params.max_tokens) config.params.max_tokens = VISION_MODEL_DEFAULT_MAX_TOKENS;
215
263
  }
216
264
 
217
265
  const fetcher = () =>
@@ -248,7 +296,7 @@ export const chatMessage: StateCreator<
248
296
 
249
297
  toggleChatLoading(false, undefined, t('generateMessage(end)') as string);
250
298
 
251
- // also exist message like this: 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
299
+ // also exist message like this: 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
252
300
  if (!isFunctionCall) {
253
301
  const { content, valid } = testFunctionMessageAtEnd(output);
254
302
 
@@ -305,13 +353,27 @@ export const chatMessage: StateCreator<
305
353
  await coreProcessMessage(contextMessages, latestMsg.id);
306
354
  },
307
355
 
308
- sendMessage: async (message) => {
309
- const { dispatchMessage, coreProcessMessage, activeTopicId } = get();
356
+ sendMessage: async (message, files) => {
357
+ const { dispatchMessage, dispatchAgentFile, coreProcessMessage, activeTopicId } = get();
310
358
  const session = sessionSelectors.currentSession(get());
311
359
  if (!session || !message) return;
312
360
 
313
361
  const userId = nanoid();
314
- dispatchMessage({ id: userId, message, role: 'user', type: 'addMessage' });
362
+
363
+ dispatchMessage({
364
+ id: userId,
365
+ message: message,
366
+ role: 'user',
367
+ type: 'addMessage',
368
+ });
369
+
370
+ // if message has attached with files, then add files to message and the agent
371
+ if (files && files.length > 0) {
372
+ const fileIdList = files.map((f) => f.id);
373
+ dispatchMessage({ id: userId, key: 'files', type: 'updateMessage', value: fileIdList });
374
+
375
+ dispatchAgentFile({ files: fileIdList, type: 'addFiles' });
376
+ }
315
377
 
316
378
  // if there is activeTopicId,then add topicId to message
317
379
  if (activeTopicId) {
@@ -341,7 +403,6 @@ export const chatMessage: StateCreator<
341
403
 
342
404
  toggleChatLoading(false);
343
405
  },
344
-
345
406
  toggleChatLoading: (loading, id, action) => {
346
407
  if (loading) {
347
408
  const abortController = new AbortController();
@@ -0,0 +1,38 @@
1
+ import { FileDispatch, FilesState, filesReducer } from './files';
2
+
3
+ describe('filesReducer', () => {
4
+ it('should add a file to the state', () => {
5
+ const initialState: FilesState = ['file1', 'file2'];
6
+ const action: FileDispatch = { type: 'addFile', file: 'file3' };
7
+ const newState = filesReducer(initialState, action);
8
+ expect(newState).toEqual(['file1', 'file2', 'file3']);
9
+ });
10
+
11
+ it('should delete a file from the state by ID', () => {
12
+ const initialState: FilesState = ['file1', 'file2', 'file3'];
13
+ const action: FileDispatch = { type: 'deleteFile', id: 'file2' };
14
+ const newState = filesReducer(initialState, action);
15
+ expect(newState).toEqual(['file1', 'file3']);
16
+ });
17
+
18
+ it('should return the state unchanged if file ID does not exist for deletion', () => {
19
+ const initialState: FilesState = ['file1', 'file2', 'file3'];
20
+ const action: FileDispatch = { type: 'deleteFile', id: 'file4' };
21
+ const newState = filesReducer(initialState, action);
22
+ expect(newState).toEqual(['file1', 'file2', 'file3']);
23
+ });
24
+
25
+ it('should add multiple files to the state', () => {
26
+ const initialState: FilesState = ['file1', 'file2'];
27
+ const action: FileDispatch = { type: 'addFiles', files: ['file3', 'file4'] };
28
+ const newState = filesReducer(initialState, action);
29
+ expect(newState).toEqual(['file1', 'file2', 'file3', 'file4']);
30
+ });
31
+
32
+ it('should return the initial state if the action type is unknown', () => {
33
+ const initialState: FilesState = ['file1', 'file2'];
34
+ const action = { type: 'unknown', id: 'file1' };
35
+ const newState = filesReducer(initialState, action as FileDispatch);
36
+ expect(newState).toEqual(['file1', 'file2']);
37
+ });
38
+ });
@@ -0,0 +1,37 @@
1
+ import { produce } from 'immer';
2
+
3
+ export type FilesState = string[];
4
+
5
+ export type FileDispatch =
6
+ | { file: string; type: 'addFile' }
7
+ | { id: string; type: 'deleteFile' }
8
+ | { files: string[]; type: 'addFiles' };
9
+
10
+ export const filesReducer = (state: FilesState, payload: FileDispatch): FilesState => {
11
+ switch (payload.type) {
12
+ case 'addFile': {
13
+ return produce(state, (draftState) => {
14
+ draftState.push(payload.file);
15
+ });
16
+ }
17
+
18
+ case 'deleteFile': {
19
+ return produce(state, (draftState) => {
20
+ const index = draftState.indexOf(payload.id);
21
+ if (index !== -1) {
22
+ draftState.splice(index, 1);
23
+ }
24
+ });
25
+ }
26
+
27
+ case 'addFiles': {
28
+ return produce(state, (draftState) => {
29
+ draftState.push(...payload.files);
30
+ });
31
+ }
32
+
33
+ default: {
34
+ return state;
35
+ }
36
+ }
37
+ };
@@ -25,6 +25,7 @@ export const initLobeSession: LobeAgentSession = {
25
25
  chats: {},
26
26
  config: initialLobeAgentConfig,
27
27
  createAt: Date.now(),
28
+ files: [],
28
29
  id: '',
29
30
  meta: DEFAULT_AGENT_META,
30
31
  type: LobeSessionType.Agent,
@@ -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
+ }
@@ -16,4 +16,5 @@ export type AgentsMarketItem = AgentsMarketIndexItem & LobeAgentSettings;
16
16
  export interface LobeChatAgentsMarketIndex {
17
17
  agents: AgentsMarketIndexItem[];
18
18
  schemaVersion: 1;
19
+ tags: string[];
19
20
  }
@@ -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
  });