@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 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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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://readme-wizard.lobehub.com/api/sponsor?themeMode=dark">
529
- <img src="https://readme-wizard.lobehub.com/api/sponsor">
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://readme-wizard.lobehub.com/api/sponsor?themeMode=dark">
501
- <img src="https://readme-wizard.lobehub.com/api/sponsor">
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
@@ -67,6 +67,8 @@ LobeChat is a open-source, extensible ([Function Calling][fc-url]), high-perform
67
67
 
68
68
  <!-- DOCS LIST -->
69
69
 
70
+ ---
71
+
70
72
  <details><summary><h4>📝 License</h4></summary>
71
73
 
72
74
  [![][fossa-license-shield]][fossa-license-url]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.114.2",
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 FileItem from '@/components/FileList/FileItem';
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
- <FileItem alwaysShowClose={alwaysShowClose} editable={editable} id={i} key={i} />
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 FileItem = memo<FileItemProps>(({ editable, id, alwaysShowClose }) => {
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 FileItem;
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 FileItem from './FileItem';
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
- <FileItem id={i} key={i} />
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
- <FileItem id={i} key={i} />
53
+ <ImageFileItem id={i} key={i} />
54
54
  ))}
55
55
  </FileGrid>
56
56
  )}
@@ -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',