@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.
- package/CHANGELOG.md +50 -0
- package/locales/ar/chat.json +7 -1
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/chat.json +7 -1
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/chat.json +7 -1
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/chat.json +7 -1
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/chat.json +7 -1
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/chat.json +7 -1
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/chat.json +7 -1
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/chat.json +7 -1
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/chat.json +7 -1
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/chat.json +7 -1
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/chat.json +7 -1
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/chat.json +7 -1
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/chat.json +7 -1
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/chat.json +7 -1
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/chat.json +7 -1
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/chat.json +7 -1
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/chat.json +7 -1
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/chat.json +7 -1
- package/locales/zh-TW/models.json +3 -0
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/features/ShareButton/index.tsx +2 -1
- package/src/database/server/migrations/0010_add_accessed_at_and_clean_tables.sql +26 -0
- package/src/database/server/migrations/meta/0010_snapshot.json +3184 -0
- package/src/database/server/migrations/meta/_journal.json +7 -0
- package/src/database/server/models/__tests__/session.test.ts +0 -2
- package/src/database/server/models/__tests__/topic.test.ts +2 -0
- package/src/database/server/schemas/lobechat/_helpers.ts +8 -0
- package/src/database/server/schemas/lobechat/agent.ts +6 -7
- package/src/database/server/schemas/lobechat/asyncTask.ts +2 -3
- package/src/database/server/schemas/lobechat/file.ts +5 -5
- package/src/database/server/schemas/lobechat/index.ts +0 -1
- package/src/database/server/schemas/lobechat/message.ts +2 -3
- package/src/database/server/schemas/lobechat/rag.ts +4 -6
- package/src/database/server/schemas/lobechat/ragEvals.ts +5 -7
- package/src/database/server/schemas/lobechat/relations.ts +0 -33
- package/src/database/server/schemas/lobechat/session.ts +3 -5
- package/src/database/server/schemas/lobechat/topic.ts +7 -8
- package/src/database/server/schemas/lobechat/user.ts +3 -5
- package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/Preview.tsx +5 -3
- package/src/features/ShareModal/ShareImage/index.tsx +103 -0
- package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/style.ts +1 -23
- package/src/features/ShareModal/ShareJSON/Preview.tsx +18 -0
- package/src/features/ShareModal/ShareJSON/generateMessages.test.ts +135 -0
- package/src/features/ShareModal/ShareJSON/generateMessages.ts +35 -0
- package/src/features/ShareModal/ShareJSON/index.tsx +99 -0
- package/src/features/ShareModal/ShareJSON/type.ts +4 -0
- package/src/features/ShareModal/ShareText/Preview.tsx +16 -0
- package/src/features/ShareModal/ShareText/index.tsx +119 -0
- package/src/features/ShareModal/ShareText/template.test.ts +178 -0
- package/src/features/ShareModal/ShareText/template.ts +79 -0
- package/src/features/ShareModal/ShareText/type.ts +6 -0
- package/src/features/ShareModal/index.tsx +69 -0
- package/src/features/ShareModal/style.ts +30 -0
- package/src/locales/default/chat.ts +7 -1
- package/src/services/__tests__/share.test.ts +35 -105
- package/src/services/share.ts +0 -30
- package/src/store/chat/slices/share/action.test.ts +7 -198
- package/src/store/chat/slices/share/action.ts +9 -113
- package/src/utils/client/exportFile.ts +20 -0
- package/src/app/(main)/chat/(workspace)/features/ShareButton/ShareModal.tsx +0 -164
- package/src/database/server/schemas/lobechat/discover.ts +0 -84
- /package/src/{app/(main)/chat/(workspace)/features/ShareButton → features/ShareModal/ShareImage}/type.ts +0 -0
- /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,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
|
+
});
|