@lobehub/chat 1.88.4 → 1.88.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.88.5](https://github.com/lobehub/lobe-chat/compare/v1.88.4...v1.88.5)
6
+
7
+ <sup>Released on **2025-05-25**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Support share single message.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Support share single message, closes [#7967](https://github.com/lobehub/lobe-chat/issues/7967) ([660a5ad](https://github.com/lobehub/lobe-chat/commit/660a5ad))
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.88.4](https://github.com/lobehub/lobe-chat/compare/v1.88.3...v1.88.4)
6
31
 
7
32
  <sup>Released on **2025-05-25**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Support share single message."
6
+ ]
7
+ },
8
+ "date": "2025-05-25",
9
+ "version": "1.88.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.88.4",
3
+ "version": "1.88.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",
@@ -17,8 +17,17 @@ export const AssistantActionsBar: RenderAction = memo(({ onActionClick, error, t
17
17
  threadSelectors.hasThreadBySourceMsgId(id)(s),
18
18
  ]);
19
19
 
20
- const { regenerate, edit, delAndRegenerate, copy, divider, del, branching } =
21
- useChatListActionsBar({ hasThread });
20
+ const {
21
+ regenerate,
22
+ edit,
23
+ delAndRegenerate,
24
+ copy,
25
+ divider,
26
+ del,
27
+ branching,
28
+ // export: exportPDF,
29
+ share,
30
+ } = useChatListActionsBar({ hasThread });
22
31
 
23
32
  const { translate, tts } = useCustomActions();
24
33
  const hasTools = !!tools;
@@ -38,7 +47,20 @@ export const AssistantActionsBar: RenderAction = memo(({ onActionClick, error, t
38
47
  <ActionIconGroup
39
48
  items={items}
40
49
  menu={{
41
- items: [edit, copy, divider, tts, translate, divider, regenerate, delAndRegenerate, del],
50
+ items: [
51
+ edit,
52
+ copy,
53
+ divider,
54
+ tts,
55
+ translate,
56
+ divider,
57
+ share,
58
+ // exportPDF,
59
+ divider,
60
+ regenerate,
61
+ delAndRegenerate,
62
+ del,
63
+ ],
42
64
  }}
43
65
  onActionClick={onActionClick}
44
66
  />
@@ -1,7 +1,7 @@
1
1
  import { ActionIconGroup, type ActionIconGroupEvent, type ActionIconGroupProps } from '@lobehub/ui';
2
2
  import { App } from 'antd';
3
3
  import isEqual from 'fast-deep-equal';
4
- import { memo, use, useCallback } from 'react';
4
+ import { memo, use, useCallback, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { VirtuosoContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
@@ -11,6 +11,7 @@ import { MessageRoleType } from '@/types/message';
11
11
 
12
12
  import { renderActions } from '../../Actions';
13
13
  import { useChatListActionsBar } from '../../hooks/useChatListActionsBar';
14
+ import ShareMessageModal from './ShareMessageModal';
14
15
 
15
16
  export type ActionsBarProps = ActionIconGroupProps;
16
17
 
@@ -63,6 +64,8 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
63
64
  const { message } = App.useApp();
64
65
  const virtuosoRef = use(VirtuosoContext);
65
66
 
67
+ const [showShareModal, setShareModal] = useState(false);
68
+
66
69
  const handleActionClick = useCallback(
67
70
  async (action: ActionIconGroupEvent) => {
68
71
  switch (action.key) {
@@ -113,6 +116,16 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
113
116
  ttsMessage(id);
114
117
  break;
115
118
  }
119
+
120
+ // case 'export': {
121
+ // setModal(true);
122
+ // break;
123
+ // }
124
+
125
+ case 'share': {
126
+ setShareModal(true);
127
+ break;
128
+ }
116
129
  }
117
130
 
118
131
  if (action.keyPath.at(-1) === 'translate') {
@@ -128,7 +141,23 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
128
141
 
129
142
  const RenderFunction = renderActions[(item?.role || '') as MessageRoleType] ?? ActionsBar;
130
143
 
131
- return <RenderFunction {...item!} onActionClick={handleActionClick} />;
144
+ if (!item) return null;
145
+
146
+ return (
147
+ <>
148
+ <RenderFunction {...item} onActionClick={handleActionClick} />
149
+ {/*{showModal && (*/}
150
+ {/* <ExportPreview content={item.content} onClose={() => setModal(false)} open={showModal} />*/}
151
+ {/*)}*/}
152
+ <ShareMessageModal
153
+ message={item}
154
+ onCancel={() => {
155
+ setShareModal(false);
156
+ }}
157
+ open={showShareModal}
158
+ />
159
+ </>
160
+ );
132
161
  });
133
162
 
134
163
  export default Actions;
@@ -0,0 +1,83 @@
1
+ import { ModelTag } from '@lobehub/icons';
2
+ import { Avatar } from '@lobehub/ui';
3
+ import { ChatHeaderTitle } from '@lobehub/ui/chat';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import pkg from '@/../package.json';
9
+ import { ProductLogo } from '@/components/Branding';
10
+ import { ChatItem } from '@/features/Conversation';
11
+ import PluginTag from '@/features/PluginTag';
12
+ import { useAgentStore } from '@/store/agent';
13
+ import { agentSelectors } from '@/store/agent/selectors';
14
+ import { useSessionStore } from '@/store/session';
15
+ import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
16
+ import { ChatMessage } from '@/types/message';
17
+
18
+ import { useContainerStyles } from '../style';
19
+ import { useStyles } from './style';
20
+ import { FieldType } from './type';
21
+
22
+ interface PreviewProps extends FieldType {
23
+ message: ChatMessage;
24
+ title?: string;
25
+ }
26
+
27
+ const Preview = memo<PreviewProps>(({ title, withBackground, withFooter, message }) => {
28
+ const [model, plugins] = useAgentStore((s) => [
29
+ agentSelectors.currentAgentModel(s),
30
+ agentSelectors.currentAgentPlugins(s),
31
+ ]);
32
+
33
+ const [isInbox, description, avatar, backgroundColor] = useSessionStore((s) => [
34
+ sessionSelectors.isInboxSession(s),
35
+ sessionMetaSelectors.currentAgentDescription(s),
36
+ sessionMetaSelectors.currentAgentAvatar(s),
37
+ sessionMetaSelectors.currentAgentBackgroundColor(s),
38
+ ]);
39
+
40
+ const { t } = useTranslation('chat');
41
+ const { styles } = useStyles(withBackground);
42
+ const { styles: containerStyles } = useContainerStyles();
43
+
44
+ const displayTitle = isInbox ? t('inbox.title') : title;
45
+ const displayDesc = isInbox ? t('inbox.desc') : description;
46
+
47
+ return (
48
+ <div className={containerStyles.preview}>
49
+ <div className={withBackground ? styles.background : undefined} id={'preview'}>
50
+ <Flexbox className={styles.container} gap={16}>
51
+ <div className={styles.header}>
52
+ <Flexbox align={'flex-start'} gap={12} horizontal>
53
+ <Avatar avatar={avatar} background={backgroundColor} size={40} title={title} />
54
+ <ChatHeaderTitle
55
+ desc={displayDesc}
56
+ tag={
57
+ <Flexbox gap={4} horizontal>
58
+ <ModelTag model={model} />
59
+ {plugins?.length > 0 && <PluginTag plugins={plugins} />}
60
+ </Flexbox>
61
+ }
62
+ title={displayTitle}
63
+ />
64
+ </Flexbox>
65
+ </div>
66
+ <Flexbox height={'100%'} style={{ paddingTop: 24, position: 'relative' }} width={'100%'}>
67
+ <ChatItem id={message.id} index={0} />
68
+ </Flexbox>
69
+ {withFooter ? (
70
+ <Flexbox align={'center'} className={styles.footer} gap={4}>
71
+ <ProductLogo type={'combine'} />
72
+ <div className={styles.url}>{pkg.homepage}</div>
73
+ </Flexbox>
74
+ ) : (
75
+ <div />
76
+ )}
77
+ </Flexbox>
78
+ </div>
79
+ </div>
80
+ );
81
+ });
82
+
83
+ export default Preview;
@@ -0,0 +1,106 @@
1
+ import { Button, Form, type FormItemProps, Segmented } from '@lobehub/ui';
2
+ import { Switch } from 'antd';
3
+ import { CopyIcon } from 'lucide-react';
4
+ import { memo, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import { FORM_STYLE } from '@/const/layoutTokens';
9
+ import { useImgToClipboard } from '@/hooks/useImgToClipboard';
10
+ import { useIsMobile } from '@/hooks/useIsMobile';
11
+ import { ImageType, imageTypeOptions, useScreenshot } from '@/hooks/useScreenshot';
12
+ import { useSessionStore } from '@/store/session';
13
+ import { sessionMetaSelectors } from '@/store/session/selectors';
14
+ import { ChatMessage } from '@/types/message';
15
+
16
+ import { useStyles } from '../style';
17
+ import Preview from './Preview';
18
+ import { FieldType } from './type';
19
+
20
+ const DEFAULT_FIELD_VALUE: FieldType = {
21
+ imageType: ImageType.JPG,
22
+ withBackground: true,
23
+ withFooter: true,
24
+ };
25
+
26
+ const ShareImage = memo<{ message: ChatMessage; mobile?: boolean }>(({ message }) => {
27
+ const currentAgentTitle = useSessionStore(sessionMetaSelectors.currentAgentTitle);
28
+ const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
29
+ const { t } = useTranslation(['chat', 'common']);
30
+ const { styles } = useStyles();
31
+ const { loading, onDownload, title } = useScreenshot({
32
+ imageType: fieldValue.imageType,
33
+ title: currentAgentTitle,
34
+ });
35
+ const { loading: copyLoading, onCopy } = useImgToClipboard();
36
+ const settings: FormItemProps[] = [
37
+ {
38
+ children: <Switch />,
39
+ label: t('shareModal.withBackground'),
40
+ layout: 'horizontal',
41
+ minWidth: undefined,
42
+ name: 'withBackground',
43
+ valuePropName: 'checked',
44
+ },
45
+ {
46
+ children: <Switch />,
47
+ label: t('shareModal.withFooter'),
48
+ layout: 'horizontal',
49
+ minWidth: undefined,
50
+ name: 'withFooter',
51
+ valuePropName: 'checked',
52
+ },
53
+ {
54
+ children: <Segmented options={imageTypeOptions} />,
55
+ label: t('shareModal.imageType'),
56
+ layout: 'horizontal',
57
+ minWidth: undefined,
58
+ name: 'imageType',
59
+ },
60
+ ];
61
+
62
+ const isMobile = useIsMobile();
63
+
64
+ const button = (
65
+ <>
66
+ <Button
67
+ block
68
+ icon={CopyIcon}
69
+ loading={copyLoading}
70
+ onClick={() => onCopy()}
71
+ size={isMobile ? undefined : 'large'}
72
+ type={'primary'}
73
+ >
74
+ {t('copy', { ns: 'common' })}
75
+ </Button>
76
+ <Button block loading={loading} onClick={onDownload} size={isMobile ? undefined : 'large'}>
77
+ {t('shareModal.download')}
78
+ </Button>
79
+ </>
80
+ );
81
+
82
+ return (
83
+ <>
84
+ <Flexbox className={styles.body} gap={16} horizontal={!isMobile}>
85
+ <Preview title={title} {...fieldValue} message={message} />
86
+ <Flexbox className={styles.sidebar} gap={12}>
87
+ <Form
88
+ initialValues={DEFAULT_FIELD_VALUE}
89
+ items={settings}
90
+ itemsType={'flat'}
91
+ onValuesChange={(_, v) => setFieldValue(v)}
92
+ {...FORM_STYLE}
93
+ />
94
+ {!isMobile && button}
95
+ </Flexbox>
96
+ </Flexbox>
97
+ {isMobile && (
98
+ <Flexbox className={styles.footer} gap={8} horizontal>
99
+ {button}
100
+ </Flexbox>
101
+ )}
102
+ </>
103
+ );
104
+ });
105
+
106
+ export default ShareImage;
@@ -0,0 +1,49 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ import { imageUrl } from '@/const/url';
4
+
5
+ export const useStyles = createStyles(({ css, token, cx }, withBackground: boolean) => ({
6
+ background: css`
7
+ padding: 24px;
8
+
9
+ background-color: ${token.colorBgLayout};
10
+ background-image: url(${imageUrl('screenshot_background.webp')});
11
+ background-position: center;
12
+ background-size: 120% 120%;
13
+ `,
14
+ container: cx(
15
+ withBackground &&
16
+ css`
17
+ overflow: hidden;
18
+ border: 2px solid ${token.colorBorder};
19
+ border-radius: ${token.borderRadiusLG}px;
20
+ `,
21
+
22
+ css`
23
+ background: ${token.colorBgLayout};
24
+ `,
25
+ ),
26
+ footer: css`
27
+ padding: 16px;
28
+ border-block-start: 1px solid ${token.colorBorder};
29
+ `,
30
+ header: css`
31
+ margin-block-end: -24px;
32
+ padding: 16px;
33
+ border-block-end: 1px solid ${token.colorBorder};
34
+ background: ${token.colorBgContainer};
35
+ `,
36
+ role: css`
37
+ margin-block-start: 12px;
38
+ padding-block-start: 12px;
39
+ border-block-start: 1px dashed ${token.colorBorderSecondary};
40
+ opacity: 0.75;
41
+
42
+ * {
43
+ font-size: 12px !important;
44
+ }
45
+ `,
46
+ url: css`
47
+ color: ${token.colorTextDescription};
48
+ `,
49
+ }));
@@ -0,0 +1,7 @@
1
+ import { ImageType } from '@/hooks/useScreenshot';
2
+
3
+ export type FieldType = {
4
+ imageType: ImageType;
5
+ withBackground: boolean;
6
+ withFooter: boolean;
7
+ };
@@ -0,0 +1,19 @@
1
+ import { Markdown } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+
4
+ import { useIsMobile } from '@/hooks/useIsMobile';
5
+
6
+ import { useContainerStyles } from '../style';
7
+
8
+ const Preview = memo<{ content: string }>(({ content }) => {
9
+ const { styles } = useContainerStyles();
10
+ const isMobile = useIsMobile();
11
+
12
+ return (
13
+ <div className={styles.preview} style={{ padding: 12 }}>
14
+ <Markdown variant={isMobile ? 'chat' : undefined}>{content}</Markdown>
15
+ </div>
16
+ );
17
+ });
18
+
19
+ export default Preview;
@@ -0,0 +1,81 @@
1
+ import { Button, copyToClipboard } from '@lobehub/ui';
2
+ import { App } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { CopyIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useIsMobile } from '@/hooks/useIsMobile';
10
+ import { useChatStore } from '@/store/chat';
11
+ import { topicSelectors } from '@/store/chat/selectors';
12
+ import { ChatMessage } from '@/types/message';
13
+ import { exportFile } from '@/utils/client/exportFile';
14
+
15
+ import { useStyles } from '../style';
16
+ import Preview from './Preview';
17
+ import { generateMarkdown } from './template';
18
+
19
+ interface ShareTextProps {
20
+ item: ChatMessage;
21
+ }
22
+
23
+ const ShareText = memo<ShareTextProps>(({ item }) => {
24
+ const { t } = useTranslation(['chat', 'common']);
25
+ const { styles } = useStyles();
26
+ const { message } = App.useApp();
27
+
28
+ const messages = [item];
29
+ const topic = useChatStore(topicSelectors.currentActiveTopic, isEqual);
30
+
31
+ const title = topic?.title || t('shareModal.exportTitle');
32
+ const content = generateMarkdown({
33
+ messages,
34
+ }).replaceAll('\n\n\n', '\n');
35
+
36
+ const isMobile = useIsMobile();
37
+
38
+ const button = (
39
+ <>
40
+ <Button
41
+ block
42
+ icon={CopyIcon}
43
+ onClick={async () => {
44
+ await copyToClipboard(content);
45
+ message.success(t('copySuccess', { defaultValue: 'Copy Success', ns: 'common' }));
46
+ }}
47
+ size={isMobile ? undefined : 'large'}
48
+ type={'primary'}
49
+ >
50
+ {t('copy', { ns: 'common' })}
51
+ </Button>
52
+ <Button
53
+ block
54
+ onClick={() => {
55
+ exportFile(content, `${title}.md`);
56
+ }}
57
+ size={isMobile ? undefined : 'large'}
58
+ >
59
+ {t('shareModal.downloadFile')}
60
+ </Button>
61
+ </>
62
+ );
63
+
64
+ return (
65
+ <>
66
+ <Flexbox className={styles.body} gap={16} horizontal={!isMobile}>
67
+ <Preview content={content} />
68
+ <Flexbox className={styles.sidebar} gap={12}>
69
+ {!isMobile && button}
70
+ </Flexbox>
71
+ </Flexbox>
72
+ {isMobile && (
73
+ <Flexbox className={styles.footer} gap={8} horizontal>
74
+ {button}
75
+ </Flexbox>
76
+ )}
77
+ </>
78
+ );
79
+ });
80
+
81
+ export default ShareText;
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { LOADING_FLAT } from '@/const/message';
4
+ import { ChatMessage } from '@/types/message';
5
+
6
+ import { generateMarkdown } from './template';
7
+
8
+ describe('generateMarkdown', () => {
9
+ // 创建测试用的消息数据
10
+ const mockMessages = [
11
+ {
12
+ id: '1',
13
+ content: 'Hello',
14
+ role: 'user',
15
+ createdAt: Date.now(),
16
+ },
17
+ {
18
+ id: '2',
19
+ content: 'Hi there',
20
+ role: 'assistant',
21
+ createdAt: Date.now(),
22
+ },
23
+ {
24
+ id: '3',
25
+ content: LOADING_FLAT,
26
+ role: 'assistant',
27
+ createdAt: Date.now(),
28
+ },
29
+ {
30
+ id: '4',
31
+ content: '{"result": "tool data"}',
32
+ role: 'tool',
33
+ createdAt: Date.now(),
34
+ tool_call_id: 'tool1',
35
+ },
36
+ {
37
+ id: '5',
38
+ content: 'Message with tools',
39
+ role: 'assistant',
40
+ createdAt: Date.now(),
41
+ tools: [{ name: 'calculator', result: '42' }],
42
+ },
43
+ ] as ChatMessage[];
44
+
45
+ const defaultParams = {
46
+ messages: mockMessages,
47
+ title: 'Chat Title',
48
+ includeTool: false,
49
+ includeUser: true,
50
+ withSystemRole: false,
51
+ withRole: false,
52
+ systemRole: '',
53
+ };
54
+
55
+ it('should filter out loading messages', () => {
56
+ const result = generateMarkdown(defaultParams);
57
+
58
+ expect(result).not.toContain(LOADING_FLAT);
59
+ });
60
+
61
+ it('should handle messages with special characters', () => {
62
+ const messagesWithSpecialChars = [
63
+ {
64
+ id: '1',
65
+ content: '**Bold** *Italic* `Code`',
66
+ role: 'user',
67
+ createdAt: Date.now(),
68
+ },
69
+ ] as ChatMessage[];
70
+
71
+ const result = generateMarkdown({
72
+ ...defaultParams,
73
+ messages: messagesWithSpecialChars,
74
+ });
75
+
76
+ expect(result).toContain('**Bold** *Italic* `Code`');
77
+ });
78
+ });
@@ -0,0 +1,26 @@
1
+ import { template } from 'lodash-es';
2
+
3
+ import { LOADING_FLAT } from '@/const/message';
4
+ import { ChatMessage } from '@/types/message';
5
+
6
+ const markdownTemplate = template(
7
+ `<% messages.forEach(function(chat) { %>
8
+
9
+ {{chat.content}}
10
+
11
+ <% }); %>
12
+ `,
13
+ {
14
+ evaluate: /<%([\S\s]+?)%>/g,
15
+ interpolate: /{{([\S\s]+?)}}/g,
16
+ },
17
+ );
18
+
19
+ interface MarkdownParams {
20
+ messages: ChatMessage[];
21
+ }
22
+
23
+ export const generateMarkdown = ({ messages }: MarkdownParams) =>
24
+ markdownTemplate({
25
+ messages: messages.filter((m) => m.content !== LOADING_FLAT),
26
+ });
@@ -0,0 +1,3 @@
1
+ export type FieldType = {
2
+ includeTool: boolean;
3
+ };
@@ -0,0 +1,68 @@
1
+ import { Modal, Segmented, type SegmentedProps } from '@lobehub/ui';
2
+ import { memo, useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { useIsMobile } from '@/hooks/useIsMobile';
7
+ import { ChatMessage } from '@/types/message';
8
+
9
+ import ShareImage from './ShareImage';
10
+ import ShareText from './ShareText';
11
+
12
+ enum Tab {
13
+ Screenshot = 'screenshot',
14
+ Text = 'text',
15
+ }
16
+
17
+ interface ShareModalProps {
18
+ message: ChatMessage;
19
+ onCancel: () => void;
20
+ open: boolean;
21
+ }
22
+
23
+ const ShareModal = memo<ShareModalProps>(({ onCancel, open, message }) => {
24
+ const [tab, setTab] = useState<Tab>(Tab.Screenshot);
25
+ const { t } = useTranslation('chat');
26
+
27
+ const options: SegmentedProps['options'] = useMemo(
28
+ () => [
29
+ {
30
+ label: t('shareModal.screenshot'),
31
+ value: Tab.Screenshot,
32
+ },
33
+ {
34
+ label: t('shareModal.text'),
35
+ value: Tab.Text,
36
+ },
37
+ ],
38
+ [],
39
+ );
40
+
41
+ const isMobile = useIsMobile();
42
+ return (
43
+ <Modal
44
+ allowFullscreen
45
+ centered={false}
46
+ footer={null}
47
+ onCancel={onCancel}
48
+ open={open}
49
+ title={t('share', { ns: 'common' })}
50
+ width={1440}
51
+ >
52
+ <Flexbox gap={isMobile ? 8 : 24}>
53
+ <Segmented
54
+ block
55
+ onChange={(value) => setTab(value as Tab)}
56
+ options={options}
57
+ style={{ width: '100%' }}
58
+ value={tab}
59
+ variant={'filled'}
60
+ />
61
+ {tab === Tab.Screenshot && <ShareImage message={message} mobile={isMobile} />}
62
+ {tab === Tab.Text && <ShareText item={message} />}
63
+ </Flexbox>
64
+ </Modal>
65
+ );
66
+ });
67
+
68
+ export default ShareModal;
@@ -0,0 +1,60 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ export const useContainerStyles = createStyles(({ css, token, stylish, cx, responsive }) => ({
4
+ preview: cx(
5
+ stylish.noScrollbar,
6
+ css`
7
+ overflow: hidden scroll;
8
+
9
+ width: 100%;
10
+ max-height: 70dvh;
11
+ border: 1px solid ${token.colorBorder};
12
+ border-radius: ${token.borderRadiusLG}px;
13
+
14
+ background: ${token.colorBgLayout};
15
+
16
+ * {
17
+ pointer-events: none;
18
+
19
+ ::-webkit-scrollbar {
20
+ width: 0 !important;
21
+ height: 0 !important;
22
+ }
23
+ }
24
+
25
+ ${responsive.mobile} {
26
+ max-height: 40dvh;
27
+ }
28
+ `,
29
+ ),
30
+ }));
31
+
32
+ export const useStyles = createStyles(({ responsive, token, css }) => ({
33
+ body: css`
34
+ ${responsive.mobile} {
35
+ padding-block-end: 68px;
36
+ }
37
+ `,
38
+ footer: css`
39
+ ${responsive.mobile} {
40
+ position: absolute;
41
+ inset-block-end: 0;
42
+ inset-inline: 0;
43
+
44
+ width: 100%;
45
+ margin: 0;
46
+ padding: 16px;
47
+
48
+ background: ${token.colorBgContainer};
49
+ }
50
+ `,
51
+ sidebar: css`
52
+ flex: none;
53
+ width: max(240px, 25%);
54
+ ${responsive.mobile} {
55
+ flex: 1;
56
+ width: unset;
57
+ margin-inline: -16px;
58
+ }
59
+ `,
60
+ }));
@@ -1,5 +1,14 @@
1
1
  import type { ActionIconGroupItemType } from '@lobehub/ui';
2
- import { Copy, Edit, ListRestart, RotateCcw, Split, Trash } from 'lucide-react';
2
+ import {
3
+ Copy,
4
+ DownloadIcon,
5
+ Edit,
6
+ ListRestart,
7
+ RotateCcw,
8
+ Share2,
9
+ Split,
10
+ Trash,
11
+ } from 'lucide-react';
3
12
  import { useMemo } from 'react';
4
13
  import { useTranslation } from 'react-i18next';
5
14
 
@@ -12,7 +21,9 @@ interface ChatListActionsBar {
12
21
  delAndRegenerate: ActionIconGroupItemType;
13
22
  divider: { type: 'divider' };
14
23
  edit: ActionIconGroupItemType;
24
+ export: ActionIconGroupItemType;
15
25
  regenerate: ActionIconGroupItemType;
26
+ share: ActionIconGroupItemType;
16
27
  }
17
28
 
18
29
  export const useChatListActionsBar = ({
@@ -59,11 +70,21 @@ export const useChatListActionsBar = ({
59
70
  key: 'edit',
60
71
  label: t('edit', { defaultValue: 'Edit' }),
61
72
  },
73
+ export: {
74
+ icon: DownloadIcon,
75
+ key: 'export',
76
+ label: '导出为 PDF',
77
+ },
62
78
  regenerate: {
63
79
  icon: RotateCcw,
64
80
  key: 'regenerate',
65
81
  label: t('regenerate', { defaultValue: 'Regenerate' }),
66
82
  },
83
+ share: {
84
+ icon: Share2,
85
+ key: 'share',
86
+ label: t('share', { defaultValue: 'Share' }),
87
+ },
67
88
  }),
68
89
  [hasThread],
69
90
  );