@lobehub/chat 0.100.4 → 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 +50 -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 +69 -0
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +60 -19
- package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +79 -0
- package/src/app/chat/(mobile)/features/{ChatInput.tsx → ChatInput/index.tsx} +19 -3
- package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +19 -0
- 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 +48 -0
- package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
- package/src/app/chat/features/ChatInput/ActionBar/History.tsx +64 -0
- package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +42 -0
- package/src/app/chat/features/ChatInput/ActionBar/Temperature.tsx +47 -0
- package/src/app/chat/features/{ChatInputContent/ActionBar/Token.tsx → ChatInput/ActionBar/Token/TokenTag.tsx} +4 -4
- package/src/app/chat/features/ChatInput/ActionBar/Token/index.tsx +21 -0
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +21 -0
- package/src/app/chat/features/ChatInput/ActionBar/index.tsx +35 -0
- package/src/app/chat/features/ChatInput/useSend.ts +21 -0
- 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 +83 -18
- package/src/store/session/slices/chat/initialState.ts +4 -1
- 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/chat/selectors/chat.ts +3 -2
- 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 -35
- package/src/app/chat/features/ChatInputContent/ActionBar/ActionLeft.tsx +0 -117
- package/src/app/chat/features/ChatInputContent/ActionBar/ActionRight.tsx +0 -48
- package/src/app/chat/features/ChatInputContent/index.tsx +0 -71
- /package/src/app/chat/features/{ChatInputContent → ChatInput}/Topic/index.tsx +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { devtools } from 'zustand/middleware';
|
|
2
|
+
import { shallow } from 'zustand/shallow';
|
|
3
|
+
import { createWithEqualityFn } from 'zustand/traditional';
|
|
4
|
+
import { StateCreator } from 'zustand/vanilla';
|
|
5
|
+
|
|
6
|
+
import { isDev } from '@/utils/env';
|
|
7
|
+
|
|
8
|
+
import { FilesStoreState, initialState } from './initialState';
|
|
9
|
+
import { FileAction, createFileSlice } from './slices/images';
|
|
10
|
+
|
|
11
|
+
// =============== 聚合 createStoreFn ============ //
|
|
12
|
+
|
|
13
|
+
export type FileStore = FilesStoreState & FileAction;
|
|
14
|
+
|
|
15
|
+
const createStore: StateCreator<FileStore, [['zustand/devtools', never]]> = (...parameters) => ({
|
|
16
|
+
...initialState,
|
|
17
|
+
...createFileSlice(...parameters),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// =============== 实装 useStore ============ //
|
|
21
|
+
|
|
22
|
+
export const useFileStore = createWithEqualityFn<FileStore>()(
|
|
23
|
+
devtools(createStore, {
|
|
24
|
+
name: 'LobeChat_File' + (isDev ? '_DEV' : ''),
|
|
25
|
+
}),
|
|
26
|
+
shallow,
|
|
27
|
+
);
|
|
@@ -75,7 +75,7 @@ describe('settingsSelectors', () => {
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
// describe('defaultAgent', () => {
|
|
80
80
|
// it('should merge DEFAULT_AGENT and s.settings.defaultAgent correctly', () => {
|
|
81
81
|
// const s: GlobalStore = {
|
|
@@ -95,8 +95,6 @@ describe('settingsSelectors', () => {
|
|
|
95
95
|
//
|
|
96
96
|
// const result = settingsSelectors.defaultAgent(s);
|
|
97
97
|
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
98
|
// expect(result).toEqual(expected);
|
|
101
99
|
// });
|
|
102
100
|
// });
|
|
@@ -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,14 +75,15 @@ 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
|
-
|
|
72
80
|
toggleChatLoading: (
|
|
73
81
|
loading: boolean,
|
|
74
82
|
id?: string,
|
|
75
83
|
action?: string,
|
|
76
84
|
) => AbortController | undefined;
|
|
85
|
+
|
|
86
|
+
updateInputMessage: (message: string) => void;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
export const chatMessage: StateCreator<
|
|
@@ -94,7 +104,6 @@ export const chatMessage: StateCreator<
|
|
|
94
104
|
// after remove topic , go back to default topic
|
|
95
105
|
toggleTopic();
|
|
96
106
|
},
|
|
97
|
-
|
|
98
107
|
coreProcessMessage: async (messages, userMessageId) => {
|
|
99
108
|
const { dispatchMessage, fetchAIChatMessage, triggerFunctionCall, activeTopicId } = get();
|
|
100
109
|
|
|
@@ -163,6 +172,16 @@ export const chatMessage: StateCreator<
|
|
|
163
172
|
deleteMessage: (id) => {
|
|
164
173
|
get().dispatchMessage({ id, type: 'deleteMessage' });
|
|
165
174
|
},
|
|
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
|
+
},
|
|
166
185
|
dispatchMessage: (payload) => {
|
|
167
186
|
const { activeId } = get();
|
|
168
187
|
const session = sessionSelectors.currentSession(get());
|
|
@@ -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
|
-
//
|
|
191
|
-
//
|
|
208
|
+
// ================================== //
|
|
209
|
+
// messages uniformly preprocess //
|
|
210
|
+
// ================================== //
|
|
192
211
|
|
|
193
|
-
// 1.
|
|
194
|
-
|
|
212
|
+
// 1. slice messages with config
|
|
213
|
+
let preprocessMsgs = getSlicedMessagesWithConfig(messages, config);
|
|
195
214
|
|
|
196
|
-
// 2.
|
|
197
|
-
|
|
198
|
-
?
|
|
199
|
-
:
|
|
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.
|
|
232
|
+
// 3. add systemRole
|
|
213
233
|
if (config.systemRole) {
|
|
214
|
-
|
|
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:
|
|
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
|
-
|
|
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) {
|
|
@@ -350,4 +412,7 @@ export const chatMessage: StateCreator<
|
|
|
350
412
|
set({ abortController: undefined, chatLoadingId: undefined }, false, action);
|
|
351
413
|
}
|
|
352
414
|
},
|
|
415
|
+
updateInputMessage: (message) => {
|
|
416
|
+
set({ inputMessage: message }, false, t('updateInputMessage'));
|
|
417
|
+
},
|
|
353
418
|
});
|
|
@@ -2,9 +2,12 @@ export interface ChatState {
|
|
|
2
2
|
abortController?: AbortController;
|
|
3
3
|
activeTopicId?: string;
|
|
4
4
|
chatLoadingId?: string;
|
|
5
|
+
inputMessage: string;
|
|
5
6
|
renameTopicId?: string;
|
|
6
7
|
shareLoading?: boolean;
|
|
7
8
|
topicLoadingId?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export const initialChatState: ChatState = {
|
|
11
|
+
export const initialChatState: ChatState = {
|
|
12
|
+
inputMessage: '',
|
|
13
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -41,6 +41,7 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
|
|
|
41
41
|
return getChatsById(s.activeId)(s);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
const initTime = Date.now();
|
|
44
45
|
// 针对新助手添加初始化时的自定义消息
|
|
45
46
|
export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] => {
|
|
46
47
|
const data = currentChats(s);
|
|
@@ -67,14 +68,14 @@ export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] =>
|
|
|
67
68
|
|
|
68
69
|
const emptyInboxGuideMessage = {
|
|
69
70
|
content: isInbox ? inboxMsg : !!meta.description ? agentSystemRoleMsg : agentMsg,
|
|
70
|
-
createAt:
|
|
71
|
+
createAt: initTime,
|
|
71
72
|
extra: {},
|
|
72
73
|
id: 'default',
|
|
73
74
|
meta: meta || {
|
|
74
75
|
avatar: DEFAULT_INBOX_AVATAR,
|
|
75
76
|
},
|
|
76
77
|
role: 'assistant',
|
|
77
|
-
updateAt:
|
|
78
|
+
updateAt: initTime,
|
|
78
79
|
} as ChatMessage;
|
|
79
80
|
|
|
80
81
|
return [emptyInboxGuideMessage];
|
|
@@ -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;
|