@lobehub/chat 1.71.4 → 1.71.5
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,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.71.5](https://github.com/lobehub/lobe-chat/compare/v1.71.4...v1.71.5)
|
6
|
+
|
7
|
+
<sup>Released on **2025-03-17**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Support screenshot to clipboard when sharing.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Support screenshot to clipboard when sharing, closes [#6275](https://github.com/lobehub/lobe-chat/issues/6275) ([45663c3](https://github.com/lobehub/lobe-chat/commit/45663c3))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.71.4](https://github.com/lobehub/lobe-chat/compare/v1.71.3...v1.71.4)
|
6
31
|
|
7
32
|
<sup>Released on **2025-03-17**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.71.
|
3
|
+
"version": "1.71.5",
|
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",
|
@@ -1,10 +1,12 @@
|
|
1
|
-
import { Form, type FormItemProps } from '@lobehub/ui';
|
1
|
+
import { Form, type FormItemProps, Icon } from '@lobehub/ui';
|
2
2
|
import { Button, Segmented, Switch } from 'antd';
|
3
|
+
import { CopyIcon } from 'lucide-react';
|
3
4
|
import { memo, useState } from 'react';
|
4
5
|
import { useTranslation } from 'react-i18next';
|
5
6
|
import { Flexbox } from 'react-layout-kit';
|
6
7
|
|
7
8
|
import { FORM_STYLE } from '@/const/layoutTokens';
|
9
|
+
import { useImgToClipboard } from '@/hooks/useImgToClipboard';
|
8
10
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
9
11
|
import { ImageType, imageTypeOptions, useScreenshot } from '@/hooks/useScreenshot';
|
10
12
|
import { useSessionStore } from '@/store/session';
|
@@ -32,7 +34,9 @@ const ShareImage = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
32
34
|
title: currentAgentTitle,
|
33
35
|
width: mobile ? 720 : undefined,
|
34
36
|
});
|
35
|
-
|
37
|
+
const { loading: copyLoading, onCopy } = useImgToClipboard({
|
38
|
+
width: mobile ? 720 : undefined,
|
39
|
+
});
|
36
40
|
const settings: FormItemProps[] = [
|
37
41
|
{
|
38
42
|
children: <Switch />,
|
@@ -66,15 +70,27 @@ const ShareImage = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
66
70
|
const isMobile = useIsMobile();
|
67
71
|
|
68
72
|
const button = (
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
<>
|
74
|
+
<Button
|
75
|
+
block
|
76
|
+
icon={<Icon icon={CopyIcon} />}
|
77
|
+
loading={copyLoading}
|
78
|
+
onClick={() => onCopy()}
|
79
|
+
size={isMobile ? undefined : 'large'}
|
80
|
+
type={'primary'}
|
81
|
+
>
|
82
|
+
{t('copy', { ns: 'common' })}
|
83
|
+
</Button>
|
84
|
+
<Button
|
85
|
+
block
|
86
|
+
loading={loading}
|
87
|
+
onClick={onDownload}
|
88
|
+
size={isMobile ? undefined : 'large'}
|
89
|
+
variant={'filled'}
|
90
|
+
>
|
91
|
+
{t('shareModal.download')}
|
92
|
+
</Button>
|
93
|
+
</>
|
78
94
|
);
|
79
95
|
|
80
96
|
return (
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { App } from 'antd';
|
2
|
+
import { t } from 'i18next';
|
3
|
+
import { useState } from 'react';
|
4
|
+
|
5
|
+
import { ImageType, getImageUrl } from './useScreenshot';
|
6
|
+
|
7
|
+
export const useImgToClipboard = ({ id = '#preview', width }: { id?: string; width?: number }) => {
|
8
|
+
const [loading, setLoading] = useState(false);
|
9
|
+
const { message } = App.useApp();
|
10
|
+
|
11
|
+
const handleCopy = async () => {
|
12
|
+
setLoading(true);
|
13
|
+
try {
|
14
|
+
const dataUrl = await getImageUrl({ id, imageType: ImageType.PNG, width });
|
15
|
+
const blob = await fetch(dataUrl).then((res) => res.blob());
|
16
|
+
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
17
|
+
setLoading(false);
|
18
|
+
message.success(t('copySuccess', { defaultValue: 'Copy Success', ns: 'common' }));
|
19
|
+
} catch (error) {
|
20
|
+
console.error('Failed to copy image', error);
|
21
|
+
setLoading(false);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
|
25
|
+
return {
|
26
|
+
loading,
|
27
|
+
onCopy: handleCopy,
|
28
|
+
};
|
29
|
+
};
|
@@ -31,6 +31,58 @@ export const imageTypeOptions: SegmentedProps['options'] = [
|
|
31
31
|
},
|
32
32
|
];
|
33
33
|
|
34
|
+
export const getImageUrl = async ({
|
35
|
+
imageType,
|
36
|
+
id = '#preview',
|
37
|
+
width,
|
38
|
+
}: {
|
39
|
+
id?: string;
|
40
|
+
imageType: ImageType;
|
41
|
+
width?: number;
|
42
|
+
}) => {
|
43
|
+
let screenshotFn: any;
|
44
|
+
switch (imageType) {
|
45
|
+
case ImageType.JPG: {
|
46
|
+
screenshotFn = domToJpeg;
|
47
|
+
break;
|
48
|
+
}
|
49
|
+
case ImageType.PNG: {
|
50
|
+
screenshotFn = domToPng;
|
51
|
+
break;
|
52
|
+
}
|
53
|
+
case ImageType.SVG: {
|
54
|
+
screenshotFn = domToSvg;
|
55
|
+
break;
|
56
|
+
}
|
57
|
+
case ImageType.WEBP: {
|
58
|
+
screenshotFn = domToWebp;
|
59
|
+
break;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
|
64
|
+
let copy: HTMLDivElement = dom;
|
65
|
+
|
66
|
+
if (width) {
|
67
|
+
copy = dom.cloneNode(true) as HTMLDivElement;
|
68
|
+
copy.style.width = `${width}px`;
|
69
|
+
document.body.append(copy);
|
70
|
+
}
|
71
|
+
|
72
|
+
const dataUrl = await screenshotFn(width ? copy : dom, {
|
73
|
+
features: {
|
74
|
+
// 不启用移除控制符,否则会导致 safari emoji 报错
|
75
|
+
removeControlCharacter: false,
|
76
|
+
},
|
77
|
+
scale: 2,
|
78
|
+
width,
|
79
|
+
});
|
80
|
+
|
81
|
+
if (width && copy) copy?.remove();
|
82
|
+
|
83
|
+
return dataUrl;
|
84
|
+
};
|
85
|
+
|
34
86
|
export const useScreenshot = ({
|
35
87
|
imageType,
|
36
88
|
title = 'share',
|
@@ -47,46 +99,7 @@ export const useScreenshot = ({
|
|
47
99
|
const handleDownload = useCallback(async () => {
|
48
100
|
setLoading(true);
|
49
101
|
try {
|
50
|
-
|
51
|
-
switch (imageType) {
|
52
|
-
case ImageType.JPG: {
|
53
|
-
screenshotFn = domToJpeg;
|
54
|
-
break;
|
55
|
-
}
|
56
|
-
case ImageType.PNG: {
|
57
|
-
screenshotFn = domToPng;
|
58
|
-
break;
|
59
|
-
}
|
60
|
-
case ImageType.SVG: {
|
61
|
-
screenshotFn = domToSvg;
|
62
|
-
break;
|
63
|
-
}
|
64
|
-
case ImageType.WEBP: {
|
65
|
-
screenshotFn = domToWebp;
|
66
|
-
break;
|
67
|
-
}
|
68
|
-
}
|
69
|
-
|
70
|
-
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
|
71
|
-
let copy: HTMLDivElement = dom;
|
72
|
-
|
73
|
-
if (width) {
|
74
|
-
copy = dom.cloneNode(true) as HTMLDivElement;
|
75
|
-
copy.style.width = `${width}px`;
|
76
|
-
document.body.append(copy);
|
77
|
-
}
|
78
|
-
|
79
|
-
const dataUrl = await screenshotFn(width ? copy : dom, {
|
80
|
-
features: {
|
81
|
-
// 不启用移除控制符,否则会导致 safari emoji 报错
|
82
|
-
removeControlCharacter: false,
|
83
|
-
},
|
84
|
-
scale: 2,
|
85
|
-
width,
|
86
|
-
});
|
87
|
-
|
88
|
-
if (width && copy) copy?.remove();
|
89
|
-
|
102
|
+
const dataUrl = await getImageUrl({ id, imageType, width });
|
90
103
|
const link = document.createElement('a');
|
91
104
|
link.download = `${BRANDING_NAME}_${title}_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
|
92
105
|
link.href = dataUrl;
|