@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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Support screenshot to clipboard when sharing."
6
+ ]
7
+ },
8
+ "date": "2025-03-17",
9
+ "version": "1.71.5"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.71.4",
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
- <Button
70
- block
71
- loading={loading}
72
- onClick={onDownload}
73
- size={isMobile ? undefined : 'large'}
74
- type={'primary'}
75
- >
76
- {t('shareModal.download')}
77
- </Button>
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
- let screenshotFn: any;
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;