@lobehub/chat 1.27.3 → 1.28.0

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 (63) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/locales/ar/chat.json +7 -1
  3. package/locales/ar/models.json +3 -0
  4. package/locales/bg-BG/chat.json +7 -1
  5. package/locales/bg-BG/models.json +3 -0
  6. package/locales/de-DE/chat.json +7 -1
  7. package/locales/de-DE/models.json +3 -0
  8. package/locales/en-US/chat.json +7 -1
  9. package/locales/en-US/models.json +3 -0
  10. package/locales/es-ES/chat.json +7 -1
  11. package/locales/es-ES/models.json +3 -0
  12. package/locales/fa-IR/chat.json +7 -1
  13. package/locales/fa-IR/models.json +3 -0
  14. package/locales/fr-FR/chat.json +7 -1
  15. package/locales/fr-FR/models.json +3 -0
  16. package/locales/it-IT/chat.json +7 -1
  17. package/locales/it-IT/models.json +3 -0
  18. package/locales/ja-JP/chat.json +7 -1
  19. package/locales/ja-JP/models.json +3 -0
  20. package/locales/ko-KR/chat.json +7 -1
  21. package/locales/ko-KR/models.json +3 -0
  22. package/locales/nl-NL/chat.json +7 -1
  23. package/locales/nl-NL/models.json +3 -0
  24. package/locales/pl-PL/chat.json +7 -1
  25. package/locales/pl-PL/models.json +3 -0
  26. package/locales/pt-BR/chat.json +7 -1
  27. package/locales/pt-BR/models.json +3 -0
  28. package/locales/ru-RU/chat.json +7 -1
  29. package/locales/ru-RU/models.json +3 -0
  30. package/locales/tr-TR/chat.json +7 -1
  31. package/locales/tr-TR/models.json +3 -0
  32. package/locales/vi-VN/chat.json +7 -1
  33. package/locales/vi-VN/models.json +3 -0
  34. package/locales/zh-CN/chat.json +7 -1
  35. package/locales/zh-CN/models.json +3 -0
  36. package/locales/zh-TW/chat.json +7 -1
  37. package/locales/zh-TW/models.json +3 -0
  38. package/package.json +1 -1
  39. package/src/app/(main)/chat/(workspace)/features/ShareButton/index.tsx +2 -1
  40. package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/Preview.tsx +5 -3
  41. package/src/features/ShareModal/ShareImage/index.tsx +103 -0
  42. package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/style.ts +1 -23
  43. package/src/features/ShareModal/ShareJSON/Preview.tsx +18 -0
  44. package/src/features/ShareModal/ShareJSON/generateMessages.test.ts +135 -0
  45. package/src/features/ShareModal/ShareJSON/generateMessages.ts +35 -0
  46. package/src/features/ShareModal/ShareJSON/index.tsx +99 -0
  47. package/src/features/ShareModal/ShareJSON/type.ts +4 -0
  48. package/src/features/ShareModal/ShareText/Preview.tsx +16 -0
  49. package/src/features/ShareModal/ShareText/index.tsx +119 -0
  50. package/src/features/ShareModal/ShareText/template.test.ts +178 -0
  51. package/src/features/ShareModal/ShareText/template.ts +79 -0
  52. package/src/features/ShareModal/ShareText/type.ts +6 -0
  53. package/src/features/ShareModal/index.tsx +69 -0
  54. package/src/features/ShareModal/style.ts +30 -0
  55. package/src/locales/default/chat.ts +7 -1
  56. package/src/services/__tests__/share.test.ts +35 -105
  57. package/src/services/share.ts +0 -30
  58. package/src/store/chat/slices/share/action.test.ts +7 -198
  59. package/src/store/chat/slices/share/action.ts +9 -113
  60. package/src/utils/client/exportFile.ts +20 -0
  61. package/src/app/(main)/chat/(workspace)/features/ShareButton/ShareModal.tsx +0 -164
  62. /package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/type.ts +0 -0
  63. /package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/useScreenshot.ts +0 -0
@@ -320,6 +320,9 @@
320
320
  "claude-2.1": {
321
321
  "description": "Claude 2 为企业提供了关键能力的进步,包括业界领先的 200K token 上下文、大幅降低模型幻觉的发生率、系统提示以及一个新的测试功能:工具调用。"
322
322
  },
323
+ "claude-3-5-haiku-20241022": {
324
+ "description": "Claude 3.5 Haiku 是 Anthropic 最快的下一代模型。与 Claude 3 Haiku 相比,Claude 3.5 Haiku 在各项技能上都有所提升,并在许多智力基准测试中超越了上一代最大的模型 Claude 3 Opus。"
325
+ },
323
326
  "claude-3-5-sonnet-20240620": {
324
327
  "description": "Claude 3.5 Sonnet 提供了超越 Opus 的能力和比 Sonnet 更快的速度,同时保持与 Sonnet 相同的价格。Sonnet 特别擅长编程、数据科学、视觉处理、代理任务。"
325
328
  },
@@ -100,14 +100,20 @@
100
100
  "tooLong": "分組名稱長度需在 1-20 之內"
101
101
  },
102
102
  "shareModal": {
103
+ "copy": "複製",
103
104
  "download": "下載截圖",
105
+ "downloadFile": "下載檔案",
106
+ "exportTitle": "預設標題",
104
107
  "imageType": "圖片格式",
108
+ "includeTool": "包含插件訊息",
109
+ "includeUser": "包含使用者訊息",
105
110
  "screenshot": "截圖",
106
111
  "settings": "導出設置",
107
- "shareToShareGPT": "生成 ShareGPT 分享鏈接",
112
+ "text": "文本",
108
113
  "withBackground": "包含背景圖片",
109
114
  "withFooter": "包含頁腳",
110
115
  "withPluginInfo": "包含插件信息",
116
+ "withRole": "包含訊息角色",
111
117
  "withSystemRole": "包含助手角色設定"
112
118
  },
113
119
  "stt": {
@@ -320,6 +320,9 @@
320
320
  "claude-2.1": {
321
321
  "description": "Claude 2 為企業提供了關鍵能力的進步,包括業界領先的 200K token 上下文、大幅降低模型幻覺的發生率、系統提示以及一個新的測試功能:工具調用。"
322
322
  },
323
+ "claude-3-5-haiku-20241022": {
324
+ "description": "Claude 3.5 Haiku 是 Anthropic 最快的下一代模型。與 Claude 3 Haiku 相比,Claude 3.5 Haiku 在各項技能上都有所提升,並在許多智力基準測試中超越了上一代最大的模型 Claude 3 Opus。"
325
+ },
323
326
  "claude-3-5-sonnet-20240620": {
324
327
  "description": "Claude 3.5 Sonnet 提供了超越 Opus 的能力和比 Sonnet 更快的速度,同時保持與 Sonnet 相同的價格。Sonnet 特別擅長編程、數據科學、視覺處理、代理任務。"
325
328
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.27.3",
3
+ "version": "1.28.0",
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",
@@ -11,7 +11,8 @@ import { useChatStore } from '@/store/chat';
11
11
 
12
12
  import { useWorkspaceModal } from '../useWorkspaceModal';
13
13
 
14
- const ShareModal = dynamic(() => import('./ShareModal'));
14
+ const ShareModal = dynamic(() => import('@/features/ShareModal'));
15
+
15
16
  interface ShareButtonProps {
16
17
  mobile?: boolean;
17
18
  open?: boolean;
@@ -4,7 +4,7 @@ import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
- import pkg from '@/../package.json';
7
+ import PluginTag from '@/app/(main)/chat/(workspace)/features/PluginTag';
8
8
  import { ProductLogo } from '@/components/Branding';
9
9
  import ChatList from '@/features/Conversation/components/ChatList';
10
10
  import { useAgentStore } from '@/store/agent';
@@ -12,7 +12,8 @@ import { agentSelectors } from '@/store/agent/selectors';
12
12
  import { useSessionStore } from '@/store/session';
13
13
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
14
14
 
15
- import PluginTag from '../PluginTag';
15
+ import pkg from '../../../../package.json';
16
+ import { useContainerStyles } from '../style';
16
17
  import { useStyles } from './style';
17
18
  import { FieldType } from './type';
18
19
 
@@ -32,12 +33,13 @@ const Preview = memo<FieldType & { title?: string }>(
32
33
 
33
34
  const { t } = useTranslation('chat');
34
35
  const { styles } = useStyles(withBackground);
36
+ const { styles: containerStyles } = useContainerStyles();
35
37
 
36
38
  const displayTitle = isInbox ? t('inbox.title') : title;
37
39
  const displayDesc = isInbox ? t('inbox.desc') : description;
38
40
 
39
41
  return (
40
- <div className={styles.preview}>
42
+ <div className={containerStyles.preview}>
41
43
  <div className={withBackground ? styles.background : undefined} id={'preview'}>
42
44
  <Flexbox className={styles.container} gap={16}>
43
45
  <div className={styles.header}>
@@ -0,0 +1,103 @@
1
+ import { Form, type FormItemProps } from '@lobehub/ui';
2
+ import { Button, Segmented, SegmentedProps, Switch } from 'antd';
3
+ import { memo, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ import { FORM_STYLE } from '@/const/layoutTokens';
8
+ import { useIsMobile } from '@/hooks/useIsMobile';
9
+
10
+ import Preview from './Preview';
11
+ import { FieldType, ImageType } from './type';
12
+ import { useScreenshot } from './useScreenshot';
13
+
14
+ export const imageTypeOptions: SegmentedProps['options'] = [
15
+ {
16
+ label: 'JPG',
17
+ value: ImageType.JPG,
18
+ },
19
+ {
20
+ label: 'PNG',
21
+ value: ImageType.PNG,
22
+ },
23
+ {
24
+ label: 'SVG',
25
+ value: ImageType.SVG,
26
+ },
27
+ {
28
+ label: 'WEBP',
29
+ value: ImageType.WEBP,
30
+ },
31
+ ];
32
+
33
+ const DEFAULT_FIELD_VALUE: FieldType = {
34
+ imageType: ImageType.JPG,
35
+ withBackground: true,
36
+ withFooter: true,
37
+ withPluginInfo: false,
38
+ withSystemRole: false,
39
+ };
40
+
41
+ const ShareImage = memo(() => {
42
+ const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
43
+ const { t } = useTranslation('chat');
44
+ const { loading, onDownload, title } = useScreenshot(fieldValue.imageType);
45
+
46
+ const settings: FormItemProps[] = [
47
+ {
48
+ children: <Switch />,
49
+ label: t('shareModal.withSystemRole'),
50
+ minWidth: undefined,
51
+ name: 'withSystemRole',
52
+ valuePropName: 'checked',
53
+ },
54
+ {
55
+ children: <Switch />,
56
+ label: t('shareModal.withBackground'),
57
+ minWidth: undefined,
58
+ name: 'withBackground',
59
+ valuePropName: 'checked',
60
+ },
61
+ {
62
+ children: <Switch />,
63
+ label: t('shareModal.withFooter'),
64
+ minWidth: undefined,
65
+ name: 'withFooter',
66
+ valuePropName: 'checked',
67
+ },
68
+ {
69
+ children: <Segmented options={imageTypeOptions} />,
70
+ label: t('shareModal.imageType'),
71
+ minWidth: undefined,
72
+ name: 'imageType',
73
+ },
74
+ ];
75
+
76
+ const isMobile = useIsMobile();
77
+ return (
78
+ <Flexbox gap={16} horizontal={!isMobile}>
79
+ <Preview title={title} {...fieldValue} />
80
+ <Flexbox gap={16}>
81
+ <Form
82
+ initialValues={DEFAULT_FIELD_VALUE}
83
+ items={settings}
84
+ itemsType={'flat'}
85
+ onValuesChange={(_, v) => setFieldValue(v)}
86
+ {...FORM_STYLE}
87
+ />
88
+ <Button
89
+ block
90
+ loading={loading}
91
+ onClick={onDownload}
92
+ size={'large'}
93
+ style={isMobile ? { bottom: 0, position: 'sticky' } : undefined}
94
+ type={'primary'}
95
+ >
96
+ {t('shareModal.download')}
97
+ </Button>
98
+ </Flexbox>
99
+ </Flexbox>
100
+ );
101
+ });
102
+
103
+ export default ShareImage;
@@ -2,7 +2,7 @@ import { createStyles } from 'antd-style';
2
2
 
3
3
  import { imageUrl } from '@/const/url';
4
4
 
5
- export const useStyles = createStyles(({ css, token, stylish, cx }, withBackground: boolean) => ({
5
+ export const useStyles = createStyles(({ css, token, cx }, withBackground: boolean) => ({
6
6
  background: css`
7
7
  padding: 24px;
8
8
 
@@ -33,28 +33,6 @@ export const useStyles = createStyles(({ css, token, stylish, cx }, withBackgrou
33
33
  background: ${token.colorBgContainer};
34
34
  border-block-end: 1px solid ${token.colorBorder};
35
35
  `,
36
- preview: cx(
37
- stylish.noScrollbar,
38
- css`
39
- overflow: hidden scroll;
40
-
41
- width: 100%;
42
- max-height: 40dvh;
43
-
44
- background: ${token.colorBgLayout};
45
- border: 1px solid ${token.colorBorder};
46
- border-radius: ${token.borderRadiusLG}px;
47
-
48
- * {
49
- pointer-events: none;
50
-
51
- ::-webkit-scrollbar {
52
- width: 0 !important;
53
- height: 0 !important;
54
- }
55
- }
56
- `,
57
- ),
58
36
  role: css`
59
37
  margin-block-start: 12px;
60
38
  padding-block-start: 12px;
@@ -0,0 +1,18 @@
1
+ import { Highlighter } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+
4
+ import { useContainerStyles } from '../style';
5
+
6
+ const Preview = memo<{ content: string }>(({ content }) => {
7
+ const { styles } = useContainerStyles();
8
+
9
+ return (
10
+ <div className={styles.preview}>
11
+ <Highlighter language={'json'} wrap>
12
+ {content}
13
+ </Highlighter>
14
+ </div>
15
+ );
16
+ });
17
+
18
+ export default Preview;
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { LOADING_FLAT } from '@/const/message';
4
+ import { ChatMessage } from '@/types/message';
5
+
6
+ import { generateMessages } from './generateMessages';
7
+
8
+ describe('generateMessages', () => {
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: 'Tool response',
32
+ role: 'tool',
33
+ createdAt: Date.now(),
34
+ tool_call_id: 'tool1',
35
+ },
36
+ ] as ChatMessage[];
37
+
38
+ it('should filter out loading messages', () => {
39
+ const result = generateMessages({
40
+ messages: mockMessages,
41
+ withSystemRole: false,
42
+ includeTool: false,
43
+ systemRole: '',
44
+ });
45
+
46
+ expect(result).toHaveLength(2);
47
+ expect(result.some((m) => m.content === LOADING_FLAT)).toBeFalsy();
48
+ });
49
+
50
+ it('should include system role when withSystemRole is true and systemRole is provided', () => {
51
+ const systemRole = 'I am a helpful assistant';
52
+ const result = generateMessages({
53
+ messages: mockMessages,
54
+ withSystemRole: true,
55
+ includeTool: false,
56
+ systemRole,
57
+ });
58
+
59
+ expect(result[0]).toEqual({
60
+ content: systemRole,
61
+ role: 'system',
62
+ });
63
+ expect(result).toHaveLength(3); // system role + 2 messages
64
+ });
65
+
66
+ it('should not include system role when withSystemRole is false', () => {
67
+ const systemRole = 'I am a helpful assistant';
68
+ const result = generateMessages({
69
+ messages: mockMessages,
70
+ withSystemRole: false,
71
+ includeTool: false,
72
+ systemRole,
73
+ });
74
+
75
+ expect(result[0].role).not.toBe('system');
76
+ expect(result).toHaveLength(2);
77
+ });
78
+
79
+ it('should include tool messages when includeTool is true', () => {
80
+ const result = generateMessages({
81
+ messages: mockMessages,
82
+ withSystemRole: false,
83
+ includeTool: true,
84
+ systemRole: '',
85
+ });
86
+
87
+ expect(result).toHaveLength(3);
88
+ expect(result.some((m) => m.role === 'tool')).toBeTruthy();
89
+ expect((result.find((m) => m.role === 'tool')! as any).tool_call_id).toBeDefined();
90
+ });
91
+
92
+ it('should exclude tool messages when includeTool is false', () => {
93
+ const result = generateMessages({
94
+ messages: mockMessages,
95
+ withSystemRole: false,
96
+ includeTool: false,
97
+ systemRole: '',
98
+ });
99
+
100
+ expect(result).toHaveLength(2);
101
+ expect(result.some((m) => m.role === 'tool')).toBeFalsy();
102
+ });
103
+
104
+ it('should trim message content', () => {
105
+ const messagesWithSpaces = [
106
+ {
107
+ id: '1',
108
+ content: ' Hello ',
109
+ role: 'user',
110
+ createdAt: Date.now(),
111
+ },
112
+ ] as ChatMessage[];
113
+
114
+ const result = generateMessages({
115
+ messages: messagesWithSpaces,
116
+ withSystemRole: false,
117
+ includeTool: false,
118
+ systemRole: '',
119
+ });
120
+
121
+ expect(result[0].content).toBe('Hello');
122
+ });
123
+
124
+ it('should not include system role when systemRole is empty', () => {
125
+ const result = generateMessages({
126
+ messages: mockMessages,
127
+ withSystemRole: true,
128
+ includeTool: false,
129
+ systemRole: '',
130
+ });
131
+
132
+ expect(result).toHaveLength(2);
133
+ expect(result[0].role).not.toBe('system');
134
+ });
135
+ });
@@ -0,0 +1,35 @@
1
+ import { LOADING_FLAT } from '@/const/message';
2
+ import { ChatMessage } from '@/types/message';
3
+
4
+ import { FieldType } from './type';
5
+
6
+ interface JSONParams extends FieldType {
7
+ messages: ChatMessage[];
8
+ systemRole: string;
9
+ }
10
+ export const generateMessages = ({
11
+ messages,
12
+ withSystemRole,
13
+ includeTool,
14
+ systemRole,
15
+ }: JSONParams) => {
16
+ const defaultMessages = messages
17
+ .filter((m) => m.content !== LOADING_FLAT)
18
+ .filter((m) => (!includeTool ? m.role !== 'tool' : true))
19
+ .map((m) => ({
20
+ content: m.content.trim(),
21
+ role: m.role,
22
+ tool_call_id: includeTool && m.tool_call_id ? m.tool_call_id : undefined,
23
+ tools: includeTool && m.tools ? m.tools : undefined,
24
+ }));
25
+
26
+ return withSystemRole && !!systemRole
27
+ ? [
28
+ {
29
+ content: systemRole,
30
+ role: 'system',
31
+ },
32
+ ...defaultMessages,
33
+ ]
34
+ : defaultMessages;
35
+ };
@@ -0,0 +1,99 @@
1
+ import { Form, type FormItemProps, Icon, copyToClipboard } from '@lobehub/ui';
2
+ import { App, Button, Switch } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { CopyIcon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { FORM_STYLE } from '@/const/layoutTokens';
10
+ import { useIsMobile } from '@/hooks/useIsMobile';
11
+ import { useAgentStore } from '@/store/agent';
12
+ import { agentSelectors } from '@/store/agent/selectors';
13
+ import { useChatStore } from '@/store/chat';
14
+ import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
15
+ import { exportFile } from '@/utils/client/exportFile';
16
+
17
+ import Preview from './Preview';
18
+ import { generateMessages } from './generateMessages';
19
+ import { FieldType } from './type';
20
+
21
+ const DEFAULT_FIELD_VALUE: FieldType = {
22
+ includeTool: true,
23
+ withSystemRole: true,
24
+ };
25
+
26
+ const ShareImage = memo(() => {
27
+ const [fieldValue, setFieldValue] = useState(DEFAULT_FIELD_VALUE);
28
+ const { t } = useTranslation(['chat', 'common']);
29
+ const { message } = App.useApp();
30
+
31
+ const settings: FormItemProps[] = [
32
+ {
33
+ children: <Switch />,
34
+ label: t('shareModal.withSystemRole'),
35
+ minWidth: undefined,
36
+ name: 'withSystemRole',
37
+ valuePropName: 'checked',
38
+ },
39
+ {
40
+ children: <Switch />,
41
+ label: t('shareModal.includeTool'),
42
+ minWidth: undefined,
43
+ name: 'includeTool',
44
+ valuePropName: 'checked',
45
+ },
46
+ ];
47
+
48
+ const systemRole = useAgentStore(agentSelectors.currentAgentSystemRole);
49
+ const messages = useChatStore(chatSelectors.currentChats, isEqual);
50
+ const data = generateMessages({ ...fieldValue, messages, systemRole });
51
+ const content = JSON.stringify(data, null, 2);
52
+
53
+ const topic = useChatStore(topicSelectors.currentActiveTopic, isEqual);
54
+ const title = topic?.title || t('shareModal.exportTitle');
55
+
56
+ const isMobile = useIsMobile();
57
+ return (
58
+ <Flexbox gap={16} horizontal={!isMobile}>
59
+ <Preview content={content} />
60
+ <Flexbox gap={16}>
61
+ <Form
62
+ initialValues={DEFAULT_FIELD_VALUE}
63
+ items={settings}
64
+ itemsType={'flat'}
65
+ onValuesChange={(_, v) => setFieldValue(v)}
66
+ {...FORM_STYLE}
67
+ itemMinWidth={320}
68
+ />
69
+ <Button
70
+ block
71
+ icon={<Icon icon={CopyIcon} />}
72
+ onClick={async () => {
73
+ await copyToClipboard(content);
74
+ message.success(t('copySuccess', { defaultValue: 'Copy Success', ns: 'common' }));
75
+ }}
76
+ size={'large'}
77
+ style={isMobile ? { bottom: 0, position: 'sticky' } : undefined}
78
+ type={'primary'}
79
+ >
80
+ {t('copy', { ns: 'common' })}
81
+ </Button>
82
+ {!isMobile && (
83
+ <Button
84
+ block
85
+ onClick={() => {
86
+ exportFile(content, `${title}.json`);
87
+ }}
88
+ size={'large'}
89
+ variant={'filled'}
90
+ >
91
+ {t('shareModal.downloadFile')}
92
+ </Button>
93
+ )}
94
+ </Flexbox>
95
+ </Flexbox>
96
+ );
97
+ });
98
+
99
+ export default ShareImage;
@@ -0,0 +1,4 @@
1
+ export interface FieldType {
2
+ includeTool: boolean;
3
+ withSystemRole: boolean;
4
+ }
@@ -0,0 +1,16 @@
1
+ import { Markdown } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+
4
+ import { useContainerStyles } from '../style';
5
+
6
+ const Preview = memo<{ content: string }>(({ content }) => {
7
+ const { styles } = useContainerStyles();
8
+
9
+ return (
10
+ <div className={styles.preview} style={{ padding: 12 }}>
11
+ <Markdown>{content}</Markdown>
12
+ </div>
13
+ );
14
+ });
15
+
16
+ export default Preview;
@@ -0,0 +1,119 @@
1
+ import { Form, type FormItemProps, Icon, copyToClipboard } from '@lobehub/ui';
2
+ import { App, Button, Switch } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { CopyIcon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { FORM_STYLE } from '@/const/layoutTokens';
10
+ import { useIsMobile } from '@/hooks/useIsMobile';
11
+ import { useAgentStore } from '@/store/agent';
12
+ import { agentSelectors } from '@/store/agent/selectors';
13
+ import { useChatStore } from '@/store/chat';
14
+ import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
15
+ import { exportFile } from '@/utils/client/exportFile';
16
+
17
+ import Preview from './Preview';
18
+ import { generateMarkdown } from './template';
19
+ import { FieldType } from './type';
20
+
21
+ const DEFAULT_FIELD_VALUE: FieldType = {
22
+ includeTool: true,
23
+ includeUser: true,
24
+ withRole: true,
25
+ withSystemRole: false,
26
+ };
27
+
28
+ const ShareText = memo(() => {
29
+ const [fieldValue, setFieldValue] = useState(DEFAULT_FIELD_VALUE);
30
+ const { t } = useTranslation(['chat', 'common']);
31
+
32
+ const { message } = App.useApp();
33
+ const settings: FormItemProps[] = [
34
+ {
35
+ children: <Switch />,
36
+ label: t('shareModal.withSystemRole'),
37
+ minWidth: undefined,
38
+ name: 'withSystemRole',
39
+ valuePropName: 'checked',
40
+ },
41
+ {
42
+ children: <Switch />,
43
+ label: t('shareModal.withRole'),
44
+ minWidth: undefined,
45
+ name: 'withRole',
46
+ valuePropName: 'checked',
47
+ },
48
+ {
49
+ children: <Switch />,
50
+ label: t('shareModal.includeUser'),
51
+ minWidth: undefined,
52
+ name: 'includeUser',
53
+ valuePropName: 'checked',
54
+ },
55
+ {
56
+ children: <Switch />,
57
+ label: t('shareModal.includeTool'),
58
+ minWidth: undefined,
59
+ name: 'includeTool',
60
+ valuePropName: 'checked',
61
+ },
62
+ ];
63
+
64
+ const [systemRole] = useAgentStore((s) => [agentSelectors.currentAgentSystemRole(s)]);
65
+ const messages = useChatStore(chatSelectors.currentChats, isEqual);
66
+ const topic = useChatStore(topicSelectors.currentActiveTopic, isEqual);
67
+
68
+ const title = topic?.title || t('shareModal.exportTitle');
69
+ const content = generateMarkdown({
70
+ ...fieldValue,
71
+ messages,
72
+ systemRole,
73
+ title,
74
+ }).replaceAll('\n\n\n', '\n');
75
+
76
+ const isMobile = useIsMobile();
77
+ return (
78
+ <Flexbox gap={16} horizontal={!isMobile}>
79
+ <Preview content={content} />
80
+ <Flexbox gap={16}>
81
+ <Form
82
+ initialValues={DEFAULT_FIELD_VALUE}
83
+ items={settings}
84
+ itemsType={'flat'}
85
+ onValuesChange={(_, v) => setFieldValue(v)}
86
+ {...FORM_STYLE}
87
+ itemMinWidth={320}
88
+ />
89
+ <Button
90
+ block
91
+ icon={<Icon icon={CopyIcon} />}
92
+ onClick={async () => {
93
+ await copyToClipboard(content);
94
+ message.success(t('copySuccess', { defaultValue: 'Copy Success', ns: 'common' }));
95
+ }}
96
+ size={'large'}
97
+ style={isMobile ? { bottom: 0, position: 'sticky' } : undefined}
98
+ type={'primary'}
99
+ >
100
+ {t('copy', { ns: 'common' })}
101
+ </Button>
102
+ {!isMobile && (
103
+ <Button
104
+ block
105
+ onClick={() => {
106
+ exportFile(content, `${title}.md`);
107
+ }}
108
+ size={'large'}
109
+ variant={'filled'}
110
+ >
111
+ {t('shareModal.downloadFile')}
112
+ </Button>
113
+ )}
114
+ </Flexbox>
115
+ </Flexbox>
116
+ );
117
+ });
118
+
119
+ export default ShareText;