@lobehub/chat 0.114.2 → 0.114.3
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/CHANGELOG.md +17 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/Home.md +2 -0
- package/package.json +3 -2
- package/src/components/FileList/EditableFileList.tsx +2 -2
- package/src/components/FileList/{FileItem.tsx → ImageFileItem.tsx} +2 -2
- package/src/components/FileList/index.tsx +3 -3
- package/src/services/file.ts +33 -0
- package/src/utils/compressImage.test.ts +59 -0
- package/src/utils/compressImage.ts +29 -0
- package/tests/setup.ts +1 -0
- package/vitest.config.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.114.3](https://github.com/lobehub/lobe-chat/compare/v0.114.2...v0.114.3)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-12-18**</sup>
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
<details>
|
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
13
|
+
|
|
14
|
+
</details>
|
|
15
|
+
|
|
16
|
+
<div align="right">
|
|
17
|
+
|
|
18
|
+
[](#readme-top)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
5
22
|
### [Version 0.114.2](https://github.com/lobehub/lobe-chat/compare/v0.114.1...v0.114.2)
|
|
6
23
|
|
|
7
24
|
<sup>Released on **2023-12-17**</sup>
|
package/README.md
CHANGED
|
@@ -525,8 +525,8 @@ Every bit counts and your one-time donation sparkles in our galaxy of support! Y
|
|
|
525
525
|
|
|
526
526
|
<a href="https://opencollective.com/lobehub" target="_blank">
|
|
527
527
|
<picture>
|
|
528
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
529
|
-
<img src="https://
|
|
528
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/lobehub/.github/blob/main/static/sponsor-dark.png?raw=true">
|
|
529
|
+
<img src="https://github.com/lobehub/.github/blob/main/static/sponsor-light.png?raw=true">
|
|
530
530
|
</picture>
|
|
531
531
|
</a>
|
|
532
532
|
|
package/README.zh-CN.md
CHANGED
|
@@ -497,8 +497,8 @@ $ bun run dev
|
|
|
497
497
|
|
|
498
498
|
<a href="https://opencollective.com/lobehub" target="_blank">
|
|
499
499
|
<picture>
|
|
500
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
501
|
-
<img src="https://
|
|
500
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/lobehub/.github/blob/main/static/sponsor-dark.png?raw=true">
|
|
501
|
+
<img src="https://github.com/lobehub/.github/blob/main/static/sponsor-light.png?raw=true">
|
|
502
502
|
</picture>
|
|
503
503
|
</a>
|
|
504
504
|
|
package/docs/Home.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.114.
|
|
3
|
+
"version": "0.114.3",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -174,7 +174,8 @@
|
|
|
174
174
|
"typescript": "^5",
|
|
175
175
|
"unified": "^11",
|
|
176
176
|
"unist-util-visit": "^5",
|
|
177
|
-
"vitest": "0.34.6"
|
|
177
|
+
"vitest": "0.34.6",
|
|
178
|
+
"vitest-canvas-mock": "^0.3.3"
|
|
178
179
|
},
|
|
179
180
|
"publishConfig": {
|
|
180
181
|
"access": "public",
|
|
@@ -3,7 +3,7 @@ import { useResponsive } from 'antd-style';
|
|
|
3
3
|
import { memo } from 'react';
|
|
4
4
|
import { Flexbox } from 'react-layout-kit';
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import ImageFileItem from '@/components/FileList/ImageFileItem';
|
|
7
7
|
|
|
8
8
|
interface EditableFileListProps {
|
|
9
9
|
alwaysShowClose?: boolean;
|
|
@@ -24,7 +24,7 @@ const EditableFileList = memo<EditableFileListProps>(
|
|
|
24
24
|
>
|
|
25
25
|
<ImageGallery>
|
|
26
26
|
{items.map((i) => (
|
|
27
|
-
<
|
|
27
|
+
<ImageFileItem alwaysShowClose={alwaysShowClose} editable={editable} id={i} key={i} />
|
|
28
28
|
))}
|
|
29
29
|
</ImageGallery>
|
|
30
30
|
</Flexbox>
|
|
@@ -34,7 +34,7 @@ interface FileItemProps {
|
|
|
34
34
|
onClick?: () => void;
|
|
35
35
|
style?: CSSProperties;
|
|
36
36
|
}
|
|
37
|
-
const
|
|
37
|
+
const ImageFileItem = memo<FileItemProps>(({ editable, id, alwaysShowClose }) => {
|
|
38
38
|
const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
|
|
39
39
|
const IMAGE_SIZE = editable ? MIN_IMAGE_SIZE : '100%';
|
|
40
40
|
const { data, isLoading } = useFetchFile(id);
|
|
@@ -74,4 +74,4 @@ const FileItem = memo<FileItemProps>(({ editable, id, alwaysShowClose }) => {
|
|
|
74
74
|
);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
export default
|
|
77
|
+
export default ImageFileItem;
|
|
@@ -6,7 +6,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
6
6
|
import { MAX_SIZE_DESKTOP, MAX_SIZE_MOBILE } from '@/components/FileList/style';
|
|
7
7
|
|
|
8
8
|
import FileGrid from './FileGrid';
|
|
9
|
-
import
|
|
9
|
+
import ImageFileItem from './ImageFileItem';
|
|
10
10
|
|
|
11
11
|
interface FileListProps {
|
|
12
12
|
items: string[];
|
|
@@ -44,13 +44,13 @@ const FileList = memo<FileListProps>(({ items }) => {
|
|
|
44
44
|
<Flexbox gap={gap}>
|
|
45
45
|
<FileGrid col={firstRow.length} gap={gap} max={max}>
|
|
46
46
|
{firstRow.map((i) => (
|
|
47
|
-
<
|
|
47
|
+
<ImageFileItem id={i} key={i} />
|
|
48
48
|
))}
|
|
49
49
|
</FileGrid>
|
|
50
50
|
{lastRow.length > 0 && (
|
|
51
51
|
<FileGrid col={lastRow.length > 2 ? 3 : lastRow.length} gap={gap} max={max}>
|
|
52
52
|
{lastRow.map((i) => (
|
|
53
|
-
<
|
|
53
|
+
<ImageFileItem id={i} key={i} />
|
|
54
54
|
))}
|
|
55
55
|
</FileGrid>
|
|
56
56
|
)}
|
package/src/services/file.ts
CHANGED
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
import { FileModel } from '@/database/models/file';
|
|
2
2
|
import { DB_File } from '@/database/schemas/files';
|
|
3
3
|
import { FilePreview } from '@/types/files';
|
|
4
|
+
import compressImage from '@/utils/compressImage';
|
|
4
5
|
|
|
5
6
|
class FileService {
|
|
7
|
+
private isImage(fileType: string) {
|
|
8
|
+
const imageRegex = /^image\//;
|
|
9
|
+
return imageRegex.test(fileType);
|
|
10
|
+
}
|
|
6
11
|
async uploadFile(file: DB_File) {
|
|
12
|
+
// 跳过图片上传测试
|
|
13
|
+
const isTestData = file.size === 1;
|
|
14
|
+
if (this.isImage(file.fileType) && !isTestData) {
|
|
15
|
+
return this.uploadImageFile(file);
|
|
16
|
+
}
|
|
7
17
|
// save to local storage
|
|
8
18
|
// we may want to save to a remote server later
|
|
9
19
|
return FileModel.create(file);
|
|
10
20
|
}
|
|
11
21
|
|
|
22
|
+
async uploadImageFile(file: DB_File) {
|
|
23
|
+
// 加载图片
|
|
24
|
+
const url = file.url || URL.createObjectURL(new Blob([file.data]));
|
|
25
|
+
const img = new Image();
|
|
26
|
+
img.src = url;
|
|
27
|
+
await (() =>
|
|
28
|
+
new Promise((resolve) => {
|
|
29
|
+
img.addEventListener('load', resolve);
|
|
30
|
+
}))();
|
|
31
|
+
|
|
32
|
+
// 压缩图片
|
|
33
|
+
const fileType = 'image/webp';
|
|
34
|
+
const base64String = compressImage({
|
|
35
|
+
img,
|
|
36
|
+
type: fileType,
|
|
37
|
+
});
|
|
38
|
+
const binaryString = atob(base64String.split('base64,')[1]);
|
|
39
|
+
const uint8Array = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
|
|
40
|
+
file.data = uint8Array.buffer;
|
|
41
|
+
|
|
42
|
+
return FileModel.create(file);
|
|
43
|
+
}
|
|
44
|
+
|
|
12
45
|
async removeFile(id: string) {
|
|
13
46
|
return FileModel.delete(id);
|
|
14
47
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import compressImage from './compressImage';
|
|
2
|
+
|
|
3
|
+
const getContextSpy = vi.spyOn(global.HTMLCanvasElement.prototype, 'getContext');
|
|
4
|
+
const drawImageSpy = vi.spyOn(CanvasRenderingContext2D.prototype, 'drawImage');
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
getContextSpy.mockClear();
|
|
8
|
+
drawImageSpy.mockClear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('compressImage', () => {
|
|
12
|
+
it('should compress image with maxWidth', () => {
|
|
13
|
+
const img = document.createElement('img');
|
|
14
|
+
img.width = 3000;
|
|
15
|
+
img.height = 2000;
|
|
16
|
+
|
|
17
|
+
const r = compressImage({ img });
|
|
18
|
+
|
|
19
|
+
expect(r).toMatch(/^data:image\/webp;base64,/);
|
|
20
|
+
|
|
21
|
+
expect(getContextSpy).toBeCalledTimes(1);
|
|
22
|
+
expect(getContextSpy).toBeCalledWith('2d');
|
|
23
|
+
|
|
24
|
+
expect(drawImageSpy).toBeCalledTimes(1);
|
|
25
|
+
expect(drawImageSpy).toBeCalledWith(img, 0, 0, 3000, 2000, 0, 0, 2160, 1440);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should compress image with maxHeight', () => {
|
|
29
|
+
const img = document.createElement('img');
|
|
30
|
+
img.width = 2000;
|
|
31
|
+
img.height = 3000;
|
|
32
|
+
|
|
33
|
+
const r = compressImage({ img });
|
|
34
|
+
|
|
35
|
+
expect(r).toMatch(/^data:image\/webp;base64,/);
|
|
36
|
+
|
|
37
|
+
expect(getContextSpy).toBeCalledTimes(1);
|
|
38
|
+
expect(getContextSpy).toBeCalledWith('2d');
|
|
39
|
+
|
|
40
|
+
expect(drawImageSpy).toBeCalledTimes(1);
|
|
41
|
+
expect(drawImageSpy).toBeCalledWith(img, 0, 0, 2000, 3000, 0, 0, 1440, 2160);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should not compress image', () => {
|
|
45
|
+
const img = document.createElement('img');
|
|
46
|
+
img.width = 2000;
|
|
47
|
+
img.height = 2000;
|
|
48
|
+
|
|
49
|
+
const r = compressImage({ img });
|
|
50
|
+
|
|
51
|
+
expect(r).toMatch(/^data:image\/webp;base64,/);
|
|
52
|
+
|
|
53
|
+
expect(getContextSpy).toBeCalledTimes(1);
|
|
54
|
+
expect(getContextSpy).toBeCalledWith('2d');
|
|
55
|
+
|
|
56
|
+
expect(drawImageSpy).toBeCalledTimes(1);
|
|
57
|
+
expect(drawImageSpy).toBeCalledWith(img, 0, 0, 2000, 2000, 0, 0, 2000, 2000);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const compressImage = ({ img, type = 'image/webp' }: { img: HTMLImageElement; type?: string }) => {
|
|
2
|
+
// 设置最大宽高
|
|
3
|
+
const maxWidth = 2160;
|
|
4
|
+
const maxHeight = 2160;
|
|
5
|
+
let width = img.width;
|
|
6
|
+
let height = img.height;
|
|
7
|
+
|
|
8
|
+
if (width > height && width > maxWidth) {
|
|
9
|
+
// 如果图片宽度大于高度且大于最大宽度限制
|
|
10
|
+
width = maxWidth;
|
|
11
|
+
height = Math.round((maxWidth / img.width) * img.height);
|
|
12
|
+
} else if (height > width && height > maxHeight) {
|
|
13
|
+
// 如果图片高度大于宽度且大于最大高度限制
|
|
14
|
+
height = maxHeight;
|
|
15
|
+
width = Math.round((maxHeight / img.height) * img.width);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
20
|
+
|
|
21
|
+
canvas.width = width;
|
|
22
|
+
canvas.height = height;
|
|
23
|
+
|
|
24
|
+
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, width, height);
|
|
25
|
+
|
|
26
|
+
return canvas.toDataURL(type);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default compressImage;
|
package/tests/setup.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { theme } from 'antd';
|
|
|
5
5
|
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
|
|
6
6
|
import 'fake-indexeddb/auto';
|
|
7
7
|
import React from 'react';
|
|
8
|
+
import 'vitest-canvas-mock';
|
|
8
9
|
|
|
9
10
|
// remove antd hash on test
|
|
10
11
|
theme.defaultConfig.hashed = false;
|
package/vitest.config.ts
CHANGED
|
@@ -16,6 +16,10 @@ export default defineConfig({
|
|
|
16
16
|
provider: 'v8',
|
|
17
17
|
reporter: ['text', 'json', 'lcov', 'text-summary'],
|
|
18
18
|
},
|
|
19
|
+
deps: {
|
|
20
|
+
inline: ['vitest-canvas-mock'],
|
|
21
|
+
},
|
|
22
|
+
// threads: false,
|
|
19
23
|
environment: 'jsdom',
|
|
20
24
|
globals: true,
|
|
21
25
|
setupFiles: './tests/setup.ts',
|