@lobehub/chat 1.88.4 → 1.88.6

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.
Files changed (19) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/locales/en-US/components.json +5 -5
  4. package/package.json +1 -1
  5. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/index.tsx +1 -14
  6. package/src/features/Conversation/Actions/Assistant.tsx +25 -3
  7. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +31 -2
  8. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/Preview.tsx +83 -0
  9. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/index.tsx +106 -0
  10. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/style.ts +49 -0
  11. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/type.ts +7 -0
  12. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/Preview.tsx +19 -0
  13. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx +81 -0
  14. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/template.test.ts +78 -0
  15. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/template.ts +26 -0
  16. package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/type.ts +3 -0
  17. package/src/features/Conversation/components/ChatItem/ShareMessageModal/index.tsx +68 -0
  18. package/src/features/Conversation/components/ChatItem/ShareMessageModal/style.ts +60 -0
  19. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +22 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.88.6](https://github.com/lobehub/lobe-chat/compare/v1.88.5...v1.88.6)
6
+
7
+ <sup>Released on **2025-05-25**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix draggable issue with agent header.
12
+
13
+ #### 💄 Styles
14
+
15
+ - **misc**: Fix a few typos in the model tooltips.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### What's fixed
23
+
24
+ - **misc**: Fix draggable issue with agent header, closes [#7968](https://github.com/lobehub/lobe-chat/issues/7968) ([cd84241](https://github.com/lobehub/lobe-chat/commit/cd84241))
25
+
26
+ #### Styles
27
+
28
+ - **misc**: Fix a few typos in the model tooltips, closes [#7952](https://github.com/lobehub/lobe-chat/issues/7952) ([8416fec](https://github.com/lobehub/lobe-chat/commit/8416fec))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ### [Version 1.88.5](https://github.com/lobehub/lobe-chat/compare/v1.88.4...v1.88.5)
39
+
40
+ <sup>Released on **2025-05-25**</sup>
41
+
42
+ #### 💄 Styles
43
+
44
+ - **misc**: Support share single message.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### Styles
52
+
53
+ - **misc**: Support share single message, closes [#7967](https://github.com/lobehub/lobe-chat/issues/7967) ([660a5ad](https://github.com/lobehub/lobe-chat/commit/660a5ad))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ### [Version 1.88.4](https://github.com/lobehub/lobe-chat/compare/v1.88.3...v1.88.4)
6
64
 
7
65
  <sup>Released on **2025-05-25**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,25 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix draggable issue with agent header."
6
+ ],
7
+ "improvements": [
8
+ "Fix a few typos in the model tooltips."
9
+ ]
10
+ },
11
+ "date": "2025-05-25",
12
+ "version": "1.88.6"
13
+ },
14
+ {
15
+ "children": {
16
+ "improvements": [
17
+ "Support share single message."
18
+ ]
19
+ },
20
+ "date": "2025-05-25",
21
+ "version": "1.88.5"
22
+ },
2
23
  {
3
24
  "children": {
4
25
  "improvements": [
@@ -73,12 +73,12 @@
73
73
  },
74
74
  "ModelSelect": {
75
75
  "featureTag": {
76
- "custom": "Custom model, by default, supports both function call and visual recognition. Please verify the availability of the above capabilities based on actual situations.",
76
+ "custom": "Custom model, by default, supports both function calls and visual recognition. Please verify the availability of the above capabilities based on actual situations.",
77
77
  "file": "This model supports file upload for reading and recognition.",
78
- "functionCall": "This model supports function call.",
79
- "imageOutput": "This model supports image generation",
80
- "reasoning": "This model supports deep thinking",
81
- "search": "This model supports online search",
78
+ "functionCall": "This model supports function calls.",
79
+ "imageOutput": "This model supports image generation.",
80
+ "reasoning": "This model supports deep thinking.",
81
+ "search": "This model supports online search.",
82
82
  "tokens": "This model supports up to {{tokens}} tokens in a single session.",
83
83
  "vision": "This model supports visual recognition."
84
84
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.88.4",
3
+ "version": "1.88.6",
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",
@@ -4,24 +4,11 @@ import { ChatHeader } from '@lobehub/ui/chat';
4
4
 
5
5
  import { useGlobalStore } from '@/store/global';
6
6
  import { systemStatusSelectors } from '@/store/global/selectors';
7
- import { electronStylish } from '@/styles/electron';
8
-
9
- import HeaderAction from './HeaderAction';
10
- import Main from './Main';
11
7
 
12
8
  const Header = () => {
13
9
  const showHeader = useGlobalStore(systemStatusSelectors.showChatHeader);
14
10
 
15
- return (
16
- showHeader && (
17
- <ChatHeader
18
- className={electronStylish.draggable}
19
- left={<Main className={electronStylish.nodrag} />}
20
- right={<HeaderAction className={electronStylish.nodrag} />}
21
- style={{ paddingInline: 8, position: 'initial', zIndex: 11 }}
22
- />
23
- )
24
- );
11
+ return showHeader && <ChatHeader style={{ paddingInline: 8, position: 'initial', zIndex: 11 }} />;
25
12
  };
26
13
 
27
14
  export default Header;
@@ -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
  );