@lobehub/chat 1.27.3 → 1.28.1

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 (81) hide show
  1. package/CHANGELOG.md +50 -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/database/server/migrations/0010_add_accessed_at_and_clean_tables.sql +26 -0
  41. package/src/database/server/migrations/meta/0010_snapshot.json +3184 -0
  42. package/src/database/server/migrations/meta/_journal.json +7 -0
  43. package/src/database/server/models/__tests__/session.test.ts +0 -2
  44. package/src/database/server/models/__tests__/topic.test.ts +2 -0
  45. package/src/database/server/schemas/lobechat/_helpers.ts +8 -0
  46. package/src/database/server/schemas/lobechat/agent.ts +6 -7
  47. package/src/database/server/schemas/lobechat/asyncTask.ts +2 -3
  48. package/src/database/server/schemas/lobechat/file.ts +5 -5
  49. package/src/database/server/schemas/lobechat/index.ts +0 -1
  50. package/src/database/server/schemas/lobechat/message.ts +2 -3
  51. package/src/database/server/schemas/lobechat/rag.ts +4 -6
  52. package/src/database/server/schemas/lobechat/ragEvals.ts +5 -7
  53. package/src/database/server/schemas/lobechat/relations.ts +0 -33
  54. package/src/database/server/schemas/lobechat/session.ts +3 -5
  55. package/src/database/server/schemas/lobechat/topic.ts +7 -8
  56. package/src/database/server/schemas/lobechat/user.ts +3 -5
  57. package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/Preview.tsx +5 -3
  58. package/src/features/ShareModal/ShareImage/index.tsx +103 -0
  59. package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/style.ts +1 -23
  60. package/src/features/ShareModal/ShareJSON/Preview.tsx +18 -0
  61. package/src/features/ShareModal/ShareJSON/generateMessages.test.ts +135 -0
  62. package/src/features/ShareModal/ShareJSON/generateMessages.ts +35 -0
  63. package/src/features/ShareModal/ShareJSON/index.tsx +99 -0
  64. package/src/features/ShareModal/ShareJSON/type.ts +4 -0
  65. package/src/features/ShareModal/ShareText/Preview.tsx +16 -0
  66. package/src/features/ShareModal/ShareText/index.tsx +119 -0
  67. package/src/features/ShareModal/ShareText/template.test.ts +178 -0
  68. package/src/features/ShareModal/ShareText/template.ts +79 -0
  69. package/src/features/ShareModal/ShareText/type.ts +6 -0
  70. package/src/features/ShareModal/index.tsx +69 -0
  71. package/src/features/ShareModal/style.ts +30 -0
  72. package/src/locales/default/chat.ts +7 -1
  73. package/src/services/__tests__/share.test.ts +35 -105
  74. package/src/services/share.ts +0 -30
  75. package/src/store/chat/slices/share/action.test.ts +7 -198
  76. package/src/store/chat/slices/share/action.ts +9 -113
  77. package/src/utils/client/exportFile.ts +20 -0
  78. package/src/app/(main)/chat/(workspace)/features/ShareButton/ShareModal.tsx +0 -164
  79. package/src/database/server/schemas/lobechat/discover.ts +0 -84
  80. /package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/type.ts +0 -0
  81. /package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/useScreenshot.ts +0 -0
@@ -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;
@@ -0,0 +1,178 @@
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 generate basic markdown with title', () => {
56
+ const result = generateMarkdown(defaultParams);
57
+
58
+ expect(result).toContain('# Chat Title');
59
+ expect(result).toContain('Hello');
60
+ expect(result).toContain('Hi there');
61
+ });
62
+
63
+ it('should include system role when withSystemRole is true', () => {
64
+ const systemRole = 'I am a helpful assistant';
65
+ const result = generateMarkdown({
66
+ ...defaultParams,
67
+ withSystemRole: true,
68
+ systemRole,
69
+ });
70
+
71
+ expect(result).toContain('````md\nI am a helpful assistant\n````');
72
+ });
73
+
74
+ it('should not include system role when withSystemRole is false', () => {
75
+ const systemRole = 'I am a helpful assistant';
76
+ const result = generateMarkdown({
77
+ ...defaultParams,
78
+ withSystemRole: false,
79
+ systemRole,
80
+ });
81
+
82
+ expect(result).not.toContain('```\nI am a helpful assistant\n```');
83
+ });
84
+
85
+ it('should add role labels when withRole is true', () => {
86
+ const result = generateMarkdown({
87
+ ...defaultParams,
88
+ withRole: true,
89
+ });
90
+
91
+ expect(result).toContain('##### User:');
92
+ expect(result).toContain('##### Assistant:');
93
+ });
94
+
95
+ it('should not add role labels when withRole is false', () => {
96
+ const result = generateMarkdown({
97
+ ...defaultParams,
98
+ withRole: false,
99
+ });
100
+
101
+ expect(result).not.toContain('##### User:');
102
+ expect(result).not.toContain('##### Assistant:');
103
+ });
104
+
105
+ it('should include tool messages when includeTool is true', () => {
106
+ const result = generateMarkdown({
107
+ ...defaultParams,
108
+ includeTool: true,
109
+ withRole: true,
110
+ });
111
+
112
+ expect(result).toContain('##### Tools Calling:');
113
+ expect(result).toContain('```json\n{"result": "tool data"}\n```');
114
+ });
115
+
116
+ it('should exclude tool messages when includeTool is false', () => {
117
+ const result = generateMarkdown({
118
+ ...defaultParams,
119
+ includeTool: false,
120
+ });
121
+
122
+ expect(result).not.toContain('{"result": "tool data"}');
123
+ });
124
+
125
+ it('should exclude user messages when includeUser is false', () => {
126
+ const result = generateMarkdown({
127
+ ...defaultParams,
128
+ includeUser: false,
129
+ });
130
+
131
+ expect(result).not.toContain('Hello');
132
+ expect(result).toContain('Hi there');
133
+ });
134
+
135
+ it('should filter out loading messages', () => {
136
+ const result = generateMarkdown(defaultParams);
137
+
138
+ expect(result).not.toContain(LOADING_FLAT);
139
+ });
140
+
141
+ it('should include tools data when includeTool is true', () => {
142
+ const result = generateMarkdown({
143
+ ...defaultParams,
144
+ includeTool: true,
145
+ });
146
+
147
+ expect(result).toContain('"name": "calculator"');
148
+ expect(result).toContain('"result": "42"');
149
+ });
150
+
151
+ it('should handle empty messages array', () => {
152
+ const result = generateMarkdown({
153
+ ...defaultParams,
154
+ messages: [],
155
+ });
156
+
157
+ expect(result).toContain('# Chat Title');
158
+ // Should not throw error and should contain at least the title
159
+ });
160
+
161
+ it('should handle messages with special characters', () => {
162
+ const messagesWithSpecialChars = [
163
+ {
164
+ id: '1',
165
+ content: '**Bold** *Italic* `Code`',
166
+ role: 'user',
167
+ createdAt: Date.now(),
168
+ },
169
+ ] as ChatMessage[];
170
+
171
+ const result = generateMarkdown({
172
+ ...defaultParams,
173
+ messages: messagesWithSpecialChars,
174
+ });
175
+
176
+ expect(result).toContain('**Bold** *Italic* `Code`');
177
+ });
178
+ });
@@ -0,0 +1,79 @@
1
+ import { template } from 'lodash-es';
2
+
3
+ import { LOADING_FLAT } from '@/const/message';
4
+ import { FieldType } from '@/features/ShareModal/ShareText/type';
5
+ import { ChatMessage } from '@/types/message';
6
+
7
+ const markdownTemplate = template(
8
+ `# {{title}}
9
+
10
+ <% if (systemRole) { %>
11
+ \`\`\`\`md
12
+ {{systemRole}}
13
+ \`\`\`\`
14
+ <% } %>
15
+
16
+ <% messages.forEach(function(chat) { %>
17
+
18
+ <% if (withRole) { %>
19
+
20
+ <% if (chat.role === 'user') { %>
21
+ ##### User:
22
+ <% } else if (chat.role === 'assistant') { %>
23
+ ##### Assistant:
24
+ <% } else if (chat.role === 'tool') { %>
25
+ ##### Tools Calling:
26
+ <% } %>
27
+
28
+ <% } %>
29
+
30
+ <% if (chat.role === 'tool') { %>
31
+ \`\`\`json
32
+ {{chat.content}}
33
+ \`\`\`
34
+ <% } else { %>
35
+
36
+ {{chat.content}}
37
+
38
+ <% if (includeTool && chat.tools) { %>
39
+
40
+ \`\`\`json
41
+ {{JSON.stringify(chat.tools, null, 2)}}
42
+ \`\`\`
43
+
44
+ <% } %>
45
+ <% } %>
46
+
47
+ <% }); %>
48
+ `,
49
+ {
50
+ evaluate: /<%([\S\s]+?)%>/g,
51
+ interpolate: /{{([\S\s]+?)}}/g,
52
+ },
53
+ );
54
+
55
+ interface MarkdownParams extends FieldType {
56
+ messages: ChatMessage[];
57
+ systemRole: string;
58
+ title: string;
59
+ }
60
+
61
+ export const generateMarkdown = ({
62
+ messages,
63
+ title,
64
+ includeTool,
65
+ includeUser,
66
+ withSystemRole,
67
+ withRole,
68
+ systemRole,
69
+ }: MarkdownParams) =>
70
+ markdownTemplate({
71
+ includeTool,
72
+ messages: messages
73
+ .filter((m) => m.content !== LOADING_FLAT)
74
+ .filter((m) => (!includeUser ? m.role !== 'user' : true))
75
+ .filter((m) => (!includeTool ? m.role !== 'tool' : true)),
76
+ systemRole: withSystemRole ? systemRole : undefined,
77
+ title,
78
+ withRole,
79
+ });
@@ -0,0 +1,6 @@
1
+ export type FieldType = {
2
+ includeTool: boolean;
3
+ includeUser: boolean;
4
+ withRole: boolean;
5
+ withSystemRole: boolean;
6
+ };