@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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { DBModel } from '@/types/database/db';
|
|
4
|
+
import { LocalFile } from '@/types/database/files';
|
|
5
|
+
|
|
6
|
+
import { LocalDB } from '../db';
|
|
7
|
+
|
|
8
|
+
describe('LocalDB', () => {
|
|
9
|
+
let db: LocalDB;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
db = new LocalDB();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await db.delete();
|
|
17
|
+
db.close();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should be instantiated with the correct schema', () => {
|
|
21
|
+
const filesTable = db.files;
|
|
22
|
+
|
|
23
|
+
expect(filesTable).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should allow adding a file', async () => {
|
|
27
|
+
const file: DBModel<LocalFile> = {
|
|
28
|
+
id: 'file1',
|
|
29
|
+
name: 'testfile.txt',
|
|
30
|
+
data: new ArrayBuffer(3),
|
|
31
|
+
saveMode: 'local',
|
|
32
|
+
fileType: 'plain/text',
|
|
33
|
+
size: 3,
|
|
34
|
+
createdAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await db.files.add(file);
|
|
38
|
+
|
|
39
|
+
expect(await db.files.get(file.id)).toEqual(file);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should allow updating a file', async () => {
|
|
43
|
+
const file: DBModel<LocalFile> = {
|
|
44
|
+
id: 'file1',
|
|
45
|
+
name: 'testfile.txt',
|
|
46
|
+
data: new ArrayBuffer(3),
|
|
47
|
+
saveMode: 'local',
|
|
48
|
+
fileType: 'plain/text',
|
|
49
|
+
size: 3,
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
await db.files.add(file);
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
await db.files.update(file.id, { name: 'update.txt' });
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
expect(await db.files.get(file.id)).toHaveProperty('name', 'update.txt');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should allow deleting a file', async () => {
|
|
62
|
+
const file: DBModel<LocalFile> = {
|
|
63
|
+
id: 'file1',
|
|
64
|
+
name: 'testfile.txt',
|
|
65
|
+
data: new ArrayBuffer(3),
|
|
66
|
+
saveMode: 'local',
|
|
67
|
+
fileType: 'plain/text',
|
|
68
|
+
size: 3,
|
|
69
|
+
createdAt: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
await db.files.add(file);
|
|
72
|
+
|
|
73
|
+
await db.files.delete(file.id);
|
|
74
|
+
|
|
75
|
+
expect(await db.files.get(file.id)).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ZodSchema, z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { BaseModel } from '../model';
|
|
5
|
+
|
|
6
|
+
// Define a mock schema for testing
|
|
7
|
+
const mockSchema = z.object({
|
|
8
|
+
name: z.string(),
|
|
9
|
+
content: z.string(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Define a mock table name
|
|
13
|
+
const mockTableName = 'files';
|
|
14
|
+
|
|
15
|
+
describe('BaseModel', () => {
|
|
16
|
+
let baseModel: BaseModel<typeof mockTableName>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
baseModel = new BaseModel(mockTableName, mockSchema);
|
|
20
|
+
// Mock the console.error to test error logging
|
|
21
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
// console.error = originalConsoleError;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have a table property', () => {
|
|
30
|
+
expect(baseModel.table).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('add method', () => {
|
|
34
|
+
it('should add a valid record to the database', async () => {
|
|
35
|
+
const validData = {
|
|
36
|
+
name: 'testfile.txt',
|
|
37
|
+
content: 'Hello, World!',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const result = await baseModel['add'](validData);
|
|
41
|
+
|
|
42
|
+
expect(result).toHaveProperty('id');
|
|
43
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should throw an error and log to console when adding an invalid record', async () => {
|
|
47
|
+
const invalidData = {
|
|
48
|
+
name: 123, // Invalid type, should be a string
|
|
49
|
+
content: 'Hello, World!',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
await expect(baseModel['add'](invalidData)).rejects.toThrow(TypeError);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Dexie from 'dexie';
|
|
2
|
+
|
|
3
|
+
import { LobeDBSchemaMap as SchemaMap, lobeDBSchema } from '@/database/core/schema';
|
|
4
|
+
import { DBModel } from '@/types/database/db';
|
|
5
|
+
|
|
6
|
+
export type LocalDBSchema = {
|
|
7
|
+
[t in keyof SchemaMap]: {
|
|
8
|
+
model: SchemaMap[t];
|
|
9
|
+
table: Dexie.Table<DBModel<SchemaMap[t]>, string>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Define a local DB
|
|
14
|
+
export class LocalDB extends Dexie {
|
|
15
|
+
public files: LocalDBSchema['files']['table'];
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super('LOBE_CHAT_DB');
|
|
19
|
+
this.version(1).stores(lobeDBSchema);
|
|
20
|
+
|
|
21
|
+
this.files = this.table('files');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const LocalDBInstance = new LocalDB();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './model';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ZodSchema } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { DBModel } from '@/types/database/db';
|
|
4
|
+
import { LocalFile } from '@/types/database/files';
|
|
5
|
+
import { nanoid } from '@/utils/uuid';
|
|
6
|
+
|
|
7
|
+
import { LocalDB, LocalDBInstance, LocalDBSchema } from './db';
|
|
8
|
+
|
|
9
|
+
export class BaseModel<N extends keyof LocalDBSchema = any> {
|
|
10
|
+
private readonly db: LocalDB;
|
|
11
|
+
private readonly schema: ZodSchema;
|
|
12
|
+
private readonly _tableName: keyof LocalDBSchema;
|
|
13
|
+
|
|
14
|
+
constructor(table: N, schema: ZodSchema, db = LocalDBInstance) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.schema = schema;
|
|
17
|
+
this._tableName = table;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get table() {
|
|
21
|
+
return this.db[this._tableName];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* create a new record
|
|
26
|
+
* @param data
|
|
27
|
+
* @param id
|
|
28
|
+
*/
|
|
29
|
+
protected async add<T = LocalDBSchema[N]['model']>(data: T, id: string | number = nanoid()) {
|
|
30
|
+
const result = this.schema.safeParse(data);
|
|
31
|
+
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
const errorMsg = `[${this.db.name}][${this._tableName}] Failed to create new record. Error: ${result.error}`;
|
|
34
|
+
|
|
35
|
+
const newError = new TypeError(errorMsg);
|
|
36
|
+
// make this error show on console to help debug
|
|
37
|
+
console.error(newError);
|
|
38
|
+
throw newError;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tableName = this._tableName;
|
|
42
|
+
|
|
43
|
+
const record: DBModel<LocalFile> = { ...result.data, createdAt: Date.now(), id };
|
|
44
|
+
|
|
45
|
+
const newId = await this.db[tableName].add(record);
|
|
46
|
+
|
|
47
|
+
return { id: newId };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LocalFile } from '@/types/database/files';
|
|
4
|
+
|
|
5
|
+
import { LocalDB } from '../core/db';
|
|
6
|
+
import { FileModel } from './file';
|
|
7
|
+
|
|
8
|
+
// Assuming LocalDB is already mocked or using an in-memory database
|
|
9
|
+
// and LocalFileSchema has been imported correctly.
|
|
10
|
+
|
|
11
|
+
describe('_FileModel', () => {
|
|
12
|
+
let fileData: LocalFile;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Set up file data with the correct structure according to LocalFileSchema
|
|
16
|
+
fileData = {
|
|
17
|
+
data: new ArrayBuffer(10),
|
|
18
|
+
fileType: 'image/png',
|
|
19
|
+
name: 'test.png',
|
|
20
|
+
saveMode: 'local',
|
|
21
|
+
size: 10,
|
|
22
|
+
// url is optional, only needed if saveMode is 'url'
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
// Clean up the database after each test
|
|
28
|
+
const db = new LocalDB();
|
|
29
|
+
await db.files.clear();
|
|
30
|
+
db.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should create a file record', async () => {
|
|
34
|
+
// First, create a file to test the create method
|
|
35
|
+
const fileData: LocalFile = {
|
|
36
|
+
data: new ArrayBuffer(10),
|
|
37
|
+
fileType: 'image/png',
|
|
38
|
+
name: 'test.png',
|
|
39
|
+
saveMode: 'local',
|
|
40
|
+
size: 10,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const result = await FileModel.create(fileData);
|
|
44
|
+
|
|
45
|
+
expect(result).toHaveProperty('id');
|
|
46
|
+
expect(result.id).toMatch(/^file-/);
|
|
47
|
+
|
|
48
|
+
// Verify that the file has been added to the database
|
|
49
|
+
const fileInDb = await FileModel.findById(result.id);
|
|
50
|
+
|
|
51
|
+
expect(fileInDb).toEqual(expect.objectContaining(fileData));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should find a file by id', async () => {
|
|
55
|
+
// First, create a file to test the findById method
|
|
56
|
+
const createdFile = await FileModel.create(fileData);
|
|
57
|
+
const foundFile = await FileModel.findById(createdFile.id);
|
|
58
|
+
|
|
59
|
+
expect(foundFile).toEqual(expect.objectContaining(fileData));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should delete a file by id', async () => {
|
|
63
|
+
// First, create a file to test the delete method
|
|
64
|
+
const createdFile = await FileModel.create(fileData);
|
|
65
|
+
await FileModel.delete(createdFile.id);
|
|
66
|
+
|
|
67
|
+
// Verify that the file has been removed from the database
|
|
68
|
+
const fileInDb = await FileModel.findById(createdFile.id);
|
|
69
|
+
expect(fileInDb).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LocalFile, LocalFileSchema } from '@/types/database/files';
|
|
2
|
+
import { nanoid } from '@/utils/uuid';
|
|
3
|
+
|
|
4
|
+
import { BaseModel } from '../core';
|
|
5
|
+
|
|
6
|
+
class _FileModel extends BaseModel {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('files', LocalFileSchema);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async create(file: LocalFile) {
|
|
12
|
+
const id = nanoid();
|
|
13
|
+
|
|
14
|
+
return this.add(file, `file-${id}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async findById(id: string) {
|
|
18
|
+
return this.table.get(id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async delete(id: string) {
|
|
22
|
+
return this.table.delete(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const FileModel = new _FileModel();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { FileModel } from '@/database/models/file';
|
|
4
|
+
import { LocalFile } from '@/types/database/files';
|
|
5
|
+
|
|
6
|
+
import { fileService } from '../file';
|
|
7
|
+
|
|
8
|
+
// Mocks for the FileModel
|
|
9
|
+
vi.mock('@/database/models/file', () => ({
|
|
10
|
+
FileModel: {
|
|
11
|
+
create: vi.fn(),
|
|
12
|
+
delete: vi.fn(),
|
|
13
|
+
findById: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mocks for the URL and Blob objects
|
|
18
|
+
global.URL.createObjectURL = vi.fn();
|
|
19
|
+
global.Blob = vi.fn();
|
|
20
|
+
|
|
21
|
+
describe('FileService', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Reset all mocks before each test
|
|
24
|
+
vi.resetAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uploadFile should save the file to the database', async () => {
|
|
28
|
+
const localFile: LocalFile = {
|
|
29
|
+
name: 'test',
|
|
30
|
+
data: new ArrayBuffer(1),
|
|
31
|
+
fileType: 'image/png',
|
|
32
|
+
saveMode: 'local',
|
|
33
|
+
size: 1,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
(FileModel.create as Mock).mockResolvedValue(localFile);
|
|
37
|
+
|
|
38
|
+
const result = await fileService.uploadFile(localFile);
|
|
39
|
+
|
|
40
|
+
expect(FileModel.create).toHaveBeenCalledWith(localFile);
|
|
41
|
+
expect(result).toEqual(localFile);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('removeFile should delete the file from the database', async () => {
|
|
45
|
+
const fileId = '1';
|
|
46
|
+
(FileModel.delete as Mock).mockResolvedValue(true);
|
|
47
|
+
|
|
48
|
+
const result = await fileService.removeFile(fileId);
|
|
49
|
+
|
|
50
|
+
expect(FileModel.delete).toHaveBeenCalledWith(fileId);
|
|
51
|
+
expect(result).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('getFile should retrieve and convert file info to FilePreview', async () => {
|
|
55
|
+
const fileId = '1';
|
|
56
|
+
const fileData: LocalFile = {
|
|
57
|
+
name: 'test',
|
|
58
|
+
data: new ArrayBuffer(1),
|
|
59
|
+
fileType: 'image/png',
|
|
60
|
+
saveMode: 'local',
|
|
61
|
+
size: 1,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
(FileModel.findById as Mock).mockResolvedValue(fileData);
|
|
65
|
+
(global.URL.createObjectURL as Mock).mockReturnValue('blob:test');
|
|
66
|
+
(global.Blob as Mock).mockImplementation(() => ['test']);
|
|
67
|
+
|
|
68
|
+
const result = await fileService.getFile(fileId);
|
|
69
|
+
|
|
70
|
+
expect(FileModel.findById).toHaveBeenCalledWith(fileId);
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
base64Url: 'data:image/png;base64,AA==',
|
|
73
|
+
fileType: 'image/png',
|
|
74
|
+
name: 'test',
|
|
75
|
+
saveMode: 'local',
|
|
76
|
+
url: 'blob:test',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('getFile should throw an error when the file is not found', async () => {
|
|
81
|
+
const fileId = 'non-existent';
|
|
82
|
+
(FileModel.findById as Mock).mockResolvedValue(null);
|
|
83
|
+
|
|
84
|
+
const getFilePromise = fileService.getFile(fileId);
|
|
85
|
+
|
|
86
|
+
await expect(getFilePromise).rejects.toThrow('file not found');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { merge } from 'lodash-es';
|
|
2
2
|
|
|
3
|
+
import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
|
|
3
4
|
import { pluginSelectors, usePluginStore } from '@/store/plugin';
|
|
4
5
|
import { initialLobeAgentConfig } from '@/store/session/initialState';
|
|
5
|
-
import type {
|
|
6
|
+
import type { OpenAIChatStreamPayload } from '@/types/openai/chat';
|
|
6
7
|
|
|
7
8
|
import { createHeaderWithOpenAI } from './_header';
|
|
8
9
|
import { OPENAI_URLS } from './_url';
|
|
@@ -26,13 +27,17 @@ export const fetchChatModel = (
|
|
|
26
27
|
},
|
|
27
28
|
params,
|
|
28
29
|
);
|
|
29
|
-
// ============
|
|
30
|
+
// ============ preprocess tools ============ //
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const filterTools = pluginSelectors.enabledSchema(enabledPlugins)(usePluginStore.getState());
|
|
33
|
+
|
|
34
|
+
// the rule that model can use tools:
|
|
35
|
+
// 1. tools is not empty
|
|
36
|
+
// 2. model is not in vision white list, because vision model can't use tools
|
|
37
|
+
// TODO: we need to find some method to let vision model use tools
|
|
38
|
+
const shouldUseTools = filterTools.length > 0 && !VISION_MODEL_WHITE_LIST.includes(payload.model);
|
|
34
39
|
|
|
35
|
-
const functions =
|
|
40
|
+
const functions = shouldUseTools ? filterTools : undefined;
|
|
36
41
|
|
|
37
42
|
return fetch(OPENAI_URLS.chat, {
|
|
38
43
|
body: JSON.stringify({ ...payload, functions }),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FileModel } from '@/database/models/file';
|
|
2
|
+
import { LocalFile } from '@/types/database/files';
|
|
3
|
+
import { FilePreview } from '@/types/files';
|
|
4
|
+
|
|
5
|
+
class FileService {
|
|
6
|
+
async uploadFile(file: LocalFile) {
|
|
7
|
+
// save to local storage
|
|
8
|
+
// we may want to save to a remote server later
|
|
9
|
+
return FileModel.create(file);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async removeFile(id: string) {
|
|
13
|
+
return FileModel.delete(id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getFile(id: string): Promise<FilePreview> {
|
|
17
|
+
const item = await FileModel.findById(id);
|
|
18
|
+
if (!item) {
|
|
19
|
+
throw new Error('file not found');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// arrayBuffer to url
|
|
23
|
+
const url = URL.createObjectURL(new Blob([item.data]));
|
|
24
|
+
const base64 = Buffer.from(item.data).toString('base64');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
base64Url: `data:${item.fileType};base64,${base64}`,
|
|
28
|
+
fileType: item.fileType,
|
|
29
|
+
name: item.name,
|
|
30
|
+
saveMode: 'local',
|
|
31
|
+
url,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const fileService = new FileService();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { FilesStoreState } from './initialState';
|
|
4
|
+
import { filesSelectors } from './selectors';
|
|
5
|
+
|
|
6
|
+
describe('filesSelectors', () => {
|
|
7
|
+
let state: FilesStoreState;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// 创建并初始化 state 的模拟实例
|
|
11
|
+
state = {
|
|
12
|
+
imagesMap: {
|
|
13
|
+
'1': {
|
|
14
|
+
name: 'a',
|
|
15
|
+
fileType: 'image/png',
|
|
16
|
+
saveMode: 'local',
|
|
17
|
+
base64Url: 'base64string1',
|
|
18
|
+
url: 'blob:abc',
|
|
19
|
+
},
|
|
20
|
+
'2': {
|
|
21
|
+
name: 'b',
|
|
22
|
+
fileType: 'image/png',
|
|
23
|
+
saveMode: 'url',
|
|
24
|
+
base64Url: 'base64string2',
|
|
25
|
+
url: 'url2',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
// 假设 '3' 是不存在的 ID
|
|
29
|
+
inputFilesList: ['1', '2', '3'],
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('getImageDetailByList should return details for the provided list of image IDs', () => {
|
|
34
|
+
const list = ['1', '2'];
|
|
35
|
+
const details = filesSelectors.getImageDetailByList(list)(state);
|
|
36
|
+
expect(details.length).toBe(2);
|
|
37
|
+
expect(details[0].name).toBe('a');
|
|
38
|
+
expect(details[1].name).toBe('b');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('imageDetailList should return details for the images in the inputFilesList', () => {
|
|
42
|
+
const details = filesSelectors.imageDetailList(state);
|
|
43
|
+
// '3' should be filtered due to not exist in map
|
|
44
|
+
expect(details.length).toBe(2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('getImageUrlOrBase64ById should return the correct URL or Base64 based on saveMode', () => {
|
|
48
|
+
const localImage = filesSelectors.getImageUrlOrBase64ById('1')(state);
|
|
49
|
+
expect(localImage).toEqual({ id: '1', url: 'base64string1' });
|
|
50
|
+
|
|
51
|
+
const serverImage = filesSelectors.getImageUrlOrBase64ById('2')(state);
|
|
52
|
+
expect(serverImage).toEqual({ id: '2', url: 'url2' });
|
|
53
|
+
|
|
54
|
+
const nonExistentImage = filesSelectors.getImageUrlOrBase64ById('3')(state);
|
|
55
|
+
expect(nonExistentImage).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('getImageUrlOrBase64ByList should return the correct list of URLs or Base64 strings', () => {
|
|
59
|
+
const list = ['1', '2', '3'];
|
|
60
|
+
const urlsOrBase64s = filesSelectors.getImageUrlOrBase64ByList(list)(state);
|
|
61
|
+
expect(urlsOrBase64s.length).toBe(2); // '3' 应该被过滤掉,因为它不存在
|
|
62
|
+
expect(urlsOrBase64s[0].url).toBe('base64string1');
|
|
63
|
+
expect(urlsOrBase64s[1].url).toBe('url2');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('imageUrlOrBase64List should return a list of image URLs or Base64 strings for all images in inputFilesList', () => {
|
|
67
|
+
const urlsOrBase64s = filesSelectors.imageUrlOrBase64List(state);
|
|
68
|
+
expect(urlsOrBase64s.length).toBe(2); // '3' 是不存在的 ID,所以应该被过滤掉
|
|
69
|
+
|
|
70
|
+
expect(urlsOrBase64s).toEqual([
|
|
71
|
+
{ id: '1', url: 'base64string1' }, // 本地保存的图像应该使用 base64 URL
|
|
72
|
+
{ id: '2', url: 'url2' }, // 服务器保存的图像应该使用普通 URL
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FilesStoreState } from './initialState';
|
|
2
|
+
|
|
3
|
+
const getImageDetailByList = (list: string[]) => (s: FilesStoreState) =>
|
|
4
|
+
list.map((i) => s.imagesMap[i]).filter(Boolean);
|
|
5
|
+
|
|
6
|
+
const imageDetailList = (s: FilesStoreState) => getImageDetailByList(s.inputFilesList)(s);
|
|
7
|
+
|
|
8
|
+
const getImageUrlOrBase64ById =
|
|
9
|
+
(id: string) =>
|
|
10
|
+
(s: FilesStoreState): { id: string; url: string } | undefined => {
|
|
11
|
+
const preview = s.imagesMap[id];
|
|
12
|
+
|
|
13
|
+
if (!preview) return undefined;
|
|
14
|
+
|
|
15
|
+
const url = preview.saveMode === 'local' ? (preview.base64Url as string) : preview.url;
|
|
16
|
+
|
|
17
|
+
return { id, url: url };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getImageUrlOrBase64ByList = (idList: string[]) => (s: FilesStoreState) =>
|
|
21
|
+
idList.map((i) => getImageUrlOrBase64ById(i)(s)).filter(Boolean) as {
|
|
22
|
+
id: string;
|
|
23
|
+
url: string;
|
|
24
|
+
}[];
|
|
25
|
+
|
|
26
|
+
const imageUrlOrBase64List = (s: FilesStoreState) => getImageUrlOrBase64ByList(s.inputFilesList)(s);
|
|
27
|
+
|
|
28
|
+
export const filesSelectors = {
|
|
29
|
+
getImageDetailByList,
|
|
30
|
+
getImageUrlOrBase64ById,
|
|
31
|
+
getImageUrlOrBase64ByList,
|
|
32
|
+
imageDetailList,
|
|
33
|
+
imageUrlOrBase64List,
|
|
34
|
+
};
|