@lobehub/chat 1.88.3 → 1.88.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/TopicListContent/ThreadList/index.tsx +22 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem/index.tsx +15 -9
- package/src/features/Conversation/Actions/Assistant.tsx +25 -3
- package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +31 -2
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/Preview.tsx +83 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/index.tsx +106 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/style.ts +49 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/type.ts +7 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/Preview.tsx +19 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx +81 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/template.test.ts +78 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/template.ts +26 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/type.ts +3 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/index.tsx +68 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/style.ts +60 -0
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +22 -1
- package/src/hooks/useFetchTopics.ts +1 -3
- package/src/store/chat/slices/thread/action.ts +0 -2
- /package/docs/usage/providers/{gemini.mdx → google.mdx} +0 -0
- /package/docs/usage/providers/{gemini.zh-CN.mdx → google.zh-CN.mdx} +0 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,64 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.88.5](https://github.com/lobehub/lobe-chat/compare/v1.88.4...v1.88.5)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-25**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Support share single message.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Support share single message, closes [#7967](https://github.com/lobehub/lobe-chat/issues/7967) ([660a5ad](https://github.com/lobehub/lobe-chat/commit/660a5ad))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.88.4](https://github.com/lobehub/lobe-chat/compare/v1.88.3...v1.88.4)
|
31
|
+
|
32
|
+
<sup>Released on **2025-05-25**</sup>
|
33
|
+
|
34
|
+
#### 🐛 Bug Fixes
|
35
|
+
|
36
|
+
- **docs**: Rename and update Google Gemini documentation.
|
37
|
+
|
38
|
+
#### 💄 Styles
|
39
|
+
|
40
|
+
- **misc**: Improve thread flicker when first-time loading.
|
41
|
+
|
42
|
+
<br/>
|
43
|
+
|
44
|
+
<details>
|
45
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
46
|
+
|
47
|
+
#### What's fixed
|
48
|
+
|
49
|
+
- **docs**: Rename and update Google Gemini documentation, closes [#7957](https://github.com/lobehub/lobe-chat/issues/7957) ([432c28d](https://github.com/lobehub/lobe-chat/commit/432c28d))
|
50
|
+
|
51
|
+
#### Styles
|
52
|
+
|
53
|
+
- **misc**: Improve thread flicker when first-time loading, closes [#7963](https://github.com/lobehub/lobe-chat/issues/7963) ([4cacacd](https://github.com/lobehub/lobe-chat/commit/4cacacd))
|
54
|
+
|
55
|
+
</details>
|
56
|
+
|
57
|
+
<div align="right">
|
58
|
+
|
59
|
+
[](#readme-top)
|
60
|
+
|
61
|
+
</div>
|
62
|
+
|
5
63
|
### [Version 1.88.3](https://github.com/lobehub/lobe-chat/compare/v1.88.2...v1.88.3)
|
6
64
|
|
7
65
|
<sup>Released on **2025-05-25**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Support share single message."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-25",
|
9
|
+
"version": "1.88.5"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"improvements": [
|
14
|
+
"Improve thread flicker when first-time loading."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-05-25",
|
18
|
+
"version": "1.88.4"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"fixes": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.88.
|
3
|
+
"version": "1.88.5",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { memo } from 'react';
|
2
|
+
|
3
|
+
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
4
|
+
import { useChatStore } from '@/store/chat';
|
5
|
+
import { threadSelectors } from '@/store/chat/selectors';
|
6
|
+
|
7
|
+
import ThreadItem from '../ThreadItem';
|
8
|
+
|
9
|
+
const ThreadList = memo(() => {
|
10
|
+
const [id] = useChatStore((s) => [s.activeTopicId]);
|
11
|
+
const threads = useChatStore(threadSelectors.getThreadsByTopic(id));
|
12
|
+
|
13
|
+
useFetchThreads(id);
|
14
|
+
|
15
|
+
return threads?.map((item, index) => (
|
16
|
+
<ThreadItem id={item.id} index={index} key={item.id} title={item.title} />
|
17
|
+
));
|
18
|
+
});
|
19
|
+
|
20
|
+
ThreadList.displayName = 'ThreadList';
|
21
|
+
|
22
|
+
export default ThreadList;
|
@@ -1,12 +1,12 @@
|
|
1
|
+
import { Skeleton } from 'antd';
|
1
2
|
import { createStyles } from 'antd-style';
|
2
|
-
import { memo, useState } from 'react';
|
3
|
+
import { Suspense, memo, useState } from 'react';
|
3
4
|
import { Flexbox } from 'react-layout-kit';
|
4
5
|
|
5
6
|
import { useChatStore } from '@/store/chat';
|
6
|
-
import { threadSelectors } from '@/store/chat/selectors';
|
7
7
|
import { useGlobalStore } from '@/store/global';
|
8
8
|
|
9
|
-
import
|
9
|
+
import ThreadList from '../ThreadList';
|
10
10
|
import DefaultContent from './DefaultContent';
|
11
11
|
import TopicContent from './TopicContent';
|
12
12
|
|
@@ -54,8 +54,6 @@ const TopicItem = memo<ConfigCellProps>(({ title, active, id, fav, threadId }) =
|
|
54
54
|
const [toggleTopic] = useChatStore((s) => [s.switchTopic]);
|
55
55
|
const [isHover, setHovering] = useState(false);
|
56
56
|
|
57
|
-
const threads = useChatStore(threadSelectors.getThreadsByTopic(id));
|
58
|
-
|
59
57
|
return (
|
60
58
|
<Flexbox style={{ position: 'relative' }}>
|
61
59
|
<Flexbox
|
@@ -80,10 +78,18 @@ const TopicItem = memo<ConfigCellProps>(({ title, active, id, fav, threadId }) =
|
|
80
78
|
<TopicContent fav={fav} id={id} showMore={isHover} title={title} />
|
81
79
|
)}
|
82
80
|
</Flexbox>
|
83
|
-
{active &&
|
84
|
-
|
85
|
-
|
86
|
-
|
81
|
+
{active && (
|
82
|
+
<Suspense
|
83
|
+
fallback={
|
84
|
+
<Flexbox gap={8} paddingBlock={8} paddingInline={24} width={'100%'}>
|
85
|
+
<Skeleton.Button active size={'small'} style={{ height: 18, width: '100%' }} />
|
86
|
+
<Skeleton.Button active size={'small'} style={{ height: 18, width: '100%' }} />
|
87
|
+
</Flexbox>
|
88
|
+
}
|
89
|
+
>
|
90
|
+
<ThreadList />
|
91
|
+
</Suspense>
|
92
|
+
)}
|
87
93
|
</Flexbox>
|
88
94
|
);
|
89
95
|
});
|
@@ -17,8 +17,17 @@ export const AssistantActionsBar: RenderAction = memo(({ onActionClick, error, t
|
|
17
17
|
threadSelectors.hasThreadBySourceMsgId(id)(s),
|
18
18
|
]);
|
19
19
|
|
20
|
-
const {
|
21
|
-
|
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: [
|
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
|
-
|
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;
|
package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareImage/Preview.tsx
ADDED
@@ -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,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;
|
package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/template.test.ts
ADDED
@@ -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,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 {
|
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
|
);
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
2
1
|
import { useChatStore } from '@/store/chat';
|
3
2
|
import { useGlobalStore } from '@/store/global';
|
4
3
|
import { systemStatusSelectors } from '@/store/global/selectors';
|
@@ -9,9 +8,8 @@ import { useSessionStore } from '@/store/session';
|
|
9
8
|
*/
|
10
9
|
export const useFetchTopics = () => {
|
11
10
|
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
12
|
-
const
|
11
|
+
const useFetchTopics = useChatStore((s) => s.useFetchTopics);
|
13
12
|
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
14
13
|
|
15
14
|
useFetchTopics(isDBInited, sessionId);
|
16
|
-
useFetchThreads(activeTopicId);
|
17
15
|
};
|
@@ -215,8 +215,6 @@ export const chatThreadMessage: StateCreator<
|
|
215
215
|
enable && !!topicId && !isDeprecatedEdition ? [SWR_USE_FETCH_THREADS, topicId] : null,
|
216
216
|
async ([, topicId]: [string, string]) => threadService.getThreads(topicId),
|
217
217
|
{
|
218
|
-
suspense: true,
|
219
|
-
fallbackData: [],
|
220
218
|
onSuccess: (threads) => {
|
221
219
|
const nextMap = { ...get().threadMaps, [topicId!]: threads };
|
222
220
|
|
File without changes
|
File without changes
|