@lobehub/chat 0.81.0 → 0.82.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 CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.82.1](https://github.com/lobehub/lobe-chat/compare/v0.82.0...v0.82.1)
6
+
7
+ <sup>Released on **2023-09-27**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix share screenshot scrollbar.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix share screenshot scrollbar ([244b3b4](https://github.com/lobehub/lobe-chat/commit/244b3b4))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 0.82.0](https://github.com/lobehub/lobe-chat/compare/v0.81.0...v0.82.0)
31
+
32
+ <sup>Released on **2023-09-27**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **share**: Add screenshot.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **share**: Add screenshot, closes [#152](https://github.com/lobehub/lobe-chat/issues/152) ([f5d21f4](https://github.com/lobehub/lobe-chat/commit/f5d21f4))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 0.81.0](https://github.com/lobehub/lobe-chat/compare/v0.80.2...v0.81.0)
6
56
 
7
57
  <sup>Released on **2023-09-27**</sup>
@@ -20,10 +20,7 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进
20
20
  - [数据统计](#数据统计)
21
21
  - [Vercel Analytics](#vercel-analytics)
22
22
  - [Mixpanel Analytics](#mixpanel-analytics)
23
- - [`NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN`](#next_public_mixpanel_project_token)
24
- - [`NEXT_PUBLIC_MIXPANEL_DEBUG`](#next_public_mixpanel_debug)
25
23
  - [Posthog Analytics](#posthog-analytics)
26
- - [`NEXT_PUBLIC_POSTHOG_DEBUG`](#next_public_posthog_debug)
27
24
  - [开发环境](#开发环境)
28
25
  - [`DEV_API_END_PORT_URL`](#dev_api_end_port_url)
29
26
 
@@ -128,14 +125,14 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进
128
125
  - 默认值: -
129
126
  - 示例:`1`
130
127
 
131
- ### `NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN`
128
+ #### `NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN`
132
129
 
133
130
  - 类型:可选
134
131
  - 描述:设置 Mixpanel 项目的识别 Token,可以在[这里][mixpanel-project-url]找到
135
132
  - 默认值: -
136
133
  - 示例:`60db2abae7fdd29961f4e8f91b074b3a`
137
134
 
138
- ### `NEXT_PUBLIC_MIXPANEL_DEBUG`
135
+ #### `NEXT_PUBLIC_MIXPANEL_DEBUG`
139
136
 
140
137
  - 类型:可选
141
138
  - 描述:开启 Mixpanel 的调试模式
@@ -165,7 +162,7 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进
165
162
  - 默认值:`https://app.posthog.com`
166
163
  - 示例:`https://example.com`
167
164
 
168
- ### `NEXT_PUBLIC_POSTHOG_DEBUG`
165
+ #### `NEXT_PUBLIC_POSTHOG_DEBUG`
169
166
 
170
167
  - 类型:可选
171
168
  - 描述:开启 PostHog 的调试模式
@@ -47,6 +47,7 @@
47
47
  "newAgent": "New Agent",
48
48
  "noDescription": "No Description",
49
49
  "ok": "OK",
50
+ "password": "Password",
50
51
  "pin": "Pin",
51
52
  "pinOff": "Unpin",
52
53
  "regenerate": "Regenerate",
@@ -60,6 +61,17 @@
60
61
  "sessionList": "Agent List",
61
62
  "setting": "Settings",
62
63
  "share": "Share",
64
+ "shareModal": {
65
+ "download": "Download Screenshot",
66
+ "imageType": "Image Type",
67
+ "screenshot": "Screenshot",
68
+ "settings": "Export Settings",
69
+ "withBackground": "Include Background Image",
70
+ "withPluginInfo": "Include Plugin Information",
71
+ "withSystemRole": "Include Agent Role Setting",
72
+ "withFooter": "Including footer",
73
+ "shareToShareGPT": "Generate ShareGPT sharing link"
74
+ },
63
75
  "stop": "Stop",
64
76
  "tab": {
65
77
  "chat": "Chat",
@@ -47,6 +47,7 @@
47
47
  "newAgent": "Новый ассистент",
48
48
  "noDescription": "Нет описания",
49
49
  "ok": "ОК",
50
+ "password": "пароль",
50
51
  "pin": "Закрепить",
51
52
  "pinOff": "Открепить",
52
53
  "regenerate": "Ре-генерация",
@@ -60,6 +61,17 @@
60
61
  "sessionList": "Список помощников",
61
62
  "setting": "Настройки",
62
63
  "share": "Поделиться",
64
+ "shareModal": {
65
+ "download": "Скачать скриншот",
66
+ "imageType": "Тип изображения",
67
+ "screenshot": "Скриншот",
68
+ "settings": "Настройки экспорта",
69
+ "withBackground": "С фоновым изображением",
70
+ "withPluginInfo": "С информацией о плагине",
71
+ "withSystemRole": "С настройками роли помощника",
72
+ "withFooter": "с подвалом",
73
+ "shareToShareGPT": "Создать ссылку для обмена ShareGPT"
74
+ },
63
75
  "stop": "Остановить",
64
76
  "tab": {
65
77
  "chat": "Чат",
@@ -47,6 +47,7 @@
47
47
  "newAgent": "新建助手",
48
48
  "noDescription": "暂无描述",
49
49
  "ok": "确定",
50
+ "password": "密码",
50
51
  "pin": "置顶",
51
52
  "pinOff": "取消置顶",
52
53
  "regenerate": "重新生成",
@@ -60,6 +61,17 @@
60
61
  "sessionList": "助手列表",
61
62
  "setting": "设置",
62
63
  "share": "分享",
64
+ "shareModal": {
65
+ "download": "下载截图",
66
+ "imageType": "图片格式",
67
+ "screenshot": "截图",
68
+ "settings": "导出设置",
69
+ "shareToShareGPT": "生成 ShareGPT 分享链接",
70
+ "withBackground": "包含背景图片",
71
+ "withFooter": "包含页脚",
72
+ "withPluginInfo": "包含插件信息",
73
+ "withSystemRole": "包含助手角色设定"
74
+ },
63
75
  "stop": "停止",
64
76
  "tab": {
65
77
  "chat": "会话",
@@ -47,6 +47,7 @@
47
47
  "newAgent": "新助理",
48
48
  "noDescription": "無描述",
49
49
  "ok": "確定",
50
+ "password": "密碼",
50
51
  "pin": "置頂",
51
52
  "pinOff": "取消置頂",
52
53
  "regenerate": "重新產生",
@@ -60,6 +61,17 @@
60
61
  "sessionList": "助理列表",
61
62
  "setting": "設定",
62
63
  "share": "分享",
64
+ "shareModal": {
65
+ "download": "下載截圖",
66
+ "imageType": "圖片格式",
67
+ "screenshot": "截圖",
68
+ "settings": "匯出設定",
69
+ "withBackground": "包含背景圖片",
70
+ "withPluginInfo": "包含插件資訊",
71
+ "withSystemRole": "包含助手角色設定",
72
+ "withFooter": "包含頁腳",
73
+ "shareToShareGPT": "生成 ShareGPT 分享連結"
74
+ },
63
75
  "stop": "停止",
64
76
  "tab": {
65
77
  "chat": "聊天",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.81.0",
3
+ "version": "0.82.1",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -78,9 +78,11 @@
78
78
  "antd-style": "^3",
79
79
  "brotli-wasm": "^1",
80
80
  "chroma-js": "^2",
81
+ "dayjs": "^1",
81
82
  "emoji-mart": "^5",
82
83
  "fast-deep-equal": "^3",
83
84
  "gpt-tokenizer": "^2",
85
+ "html-to-image": "^1",
84
86
  "i18next": "^23",
85
87
  "idb-keyval": "^6",
86
88
  "immer": "^10",
@@ -98,7 +100,9 @@
98
100
  "react-i18next": "^13",
99
101
  "react-intersection-observer": "^9",
100
102
  "react-layout-kit": "^1",
101
- "satori": "^0.10",
103
+ "remark": "^14",
104
+ "remark-gfm": "^3",
105
+ "remark-html": "^15",
102
106
  "swr": "^2",
103
107
  "systemjs": "^6",
104
108
  "ts-md5": "^1",
@@ -138,7 +142,6 @@
138
142
  "next-pwa": "^5",
139
143
  "node-fetch": "^3",
140
144
  "prettier": "^3",
141
- "remark": "^14",
142
145
  "remark-cli": "^11",
143
146
  "semantic-release": "^21",
144
147
  "stylelint": "^15",
@@ -6,26 +6,12 @@
6
6
  {
7
7
  "src": "/icons/icon-192x192.png",
8
8
  "sizes": "192x192",
9
- "type": "image/png",
10
- "purpose": "any"
11
- },
12
- {
13
- "src": "/icons/icon-maskable-192x192.png",
14
- "sizes": "192x192",
15
- "type": "image/png",
16
- "purpose": "maskable"
9
+ "type": "image/png"
17
10
  },
18
11
  {
19
12
  "src": "/icons/icon-512x512.png",
20
13
  "sizes": "512x512",
21
- "type": "image/png",
22
- "purpose": "any"
23
- },
24
- {
25
- "src": "/icons/icon-maskable-512x512.png",
26
- "sizes": "512x512",
27
- "type": "image/png",
28
- "purpose": "maskable"
14
+ "type": "image/png"
29
15
  }
30
16
  ],
31
17
  "id": "/",
@@ -12,9 +12,14 @@ import OtpInput from '../OTPInput';
12
12
  import APIKeyForm from './ApiKeyForm';
13
13
  import { ErrorActionContainer, FormAction } from './style';
14
14
 
15
+ enum Tab {
16
+ Api = 'api',
17
+ Password = 'password',
18
+ }
19
+
15
20
  const InvalidAccess = memo<{ id: string }>(({ id }) => {
16
21
  const { t } = useTranslation('error');
17
- const [mode, setMode] = useState('password');
22
+ const [mode, setMode] = useState<Tab>(Tab.Password);
18
23
  const [password, setSettings] = useGlobalStore((s) => [s.settings.password, s.setSettings]);
19
24
  const [resend, deleteMessage] = useSessionStore((s) => [s.resendMessage, s.deleteMessage]);
20
25
 
@@ -22,16 +27,20 @@ const InvalidAccess = memo<{ id: string }>(({ id }) => {
22
27
  <ErrorActionContainer>
23
28
  <Segmented
24
29
  block
25
- onChange={(value) => setMode(value as string)}
30
+ onChange={(value) => setMode(value as Tab)}
26
31
  options={[
27
- { icon: <Icon icon={KeySquare} />, label: 'OpenAI API Key', value: 'api' },
28
- { icon: <Icon icon={SquareAsterisk} />, label: '密码', value: 'password' },
32
+ { icon: <Icon icon={KeySquare} />, label: 'OpenAI API Key', value: Tab.Api },
33
+ {
34
+ icon: <Icon icon={SquareAsterisk} />,
35
+ label: t('password', { ns: 'common' }),
36
+ value: Tab.Password,
37
+ },
29
38
  ]}
30
39
  style={{ width: '100%' }}
31
40
  value={mode}
32
41
  />
33
42
  <Flexbox gap={24}>
34
- {mode === 'password' ? (
43
+ {mode === Tab.Password && (
35
44
  <>
36
45
  <FormAction
37
46
  avatar={'🗳'}
@@ -56,7 +65,8 @@ const InvalidAccess = memo<{ id: string }>(({ id }) => {
56
65
  {t('unlock.confirm')}
57
66
  </Button>
58
67
  </>
59
- ) : (
68
+ )}
69
+ {mode === Tab.Api && (
60
70
  <APIKeyForm
61
71
  onConfirm={() => {
62
72
  resend(id);
@@ -1,28 +1,123 @@
1
- import { ActionIcon, Icon } from '@lobehub/ui';
2
- import { Loader2, Share2 } from 'lucide-react';
3
- import { memo } from 'react';
1
+ import { Form, ItemGroup } from '@lobehub/ui';
2
+ import { Button, Segmented, SegmentedProps, Switch } from 'antd';
3
+ import { Share2 } from 'lucide-react';
4
+ import { memo, useMemo, useState } from 'react';
4
5
  import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
5
7
 
8
+ import { FORM_STYLE } from '@/const/layoutTokens';
9
+ import { useGlobalStore } from '@/store/global';
6
10
  import { useSessionStore } from '@/store/session';
7
11
 
8
- const ShareButton = memo(() => {
12
+ import Preview, { ImageType, imageTypeOptions } from './Preview';
13
+
14
+ enum Tab {
15
+ Screenshot = 'screenshot',
16
+ ShareGPT = 'sharegpt',
17
+ }
18
+
19
+ const Inner = memo(() => {
20
+ const [withSystemRole, setWithSystemRole] = useState(false);
21
+ const [withPluginInfo, setWithPluginInfo] = useState(false);
22
+ const [withBackground, setWithBackground] = useState(true);
23
+ const [withFooter, setWithFooter] = useState(true);
24
+ const [imageType, setImageType] = useState<ImageType>(ImageType.JPG);
25
+ const [tab, setTab] = useState<Tab>(Tab.Screenshot);
9
26
  const { t } = useTranslation('common');
10
27
 
28
+ const avatar = useGlobalStore((s) => s.settings.avatar);
11
29
  const [shareLoading, shareToShareGPT] = useSessionStore((s) => [
12
30
  s.shareLoading,
13
31
  s.shareToShareGPT,
14
32
  ]);
15
33
 
16
- return shareLoading ? (
17
- <Icon icon={Loader2} size={{ fontSize: 24 }} spin />
18
- ) : (
19
- <ActionIcon
20
- icon={Share2}
21
- onClick={shareToShareGPT}
22
- size={{ fontSize: 24 }}
23
- title={t('share')}
24
- />
34
+ const options: SegmentedProps['options'] = useMemo(
35
+ () => [
36
+ {
37
+ label: t('shareModal.screenshot'),
38
+ value: Tab.Screenshot,
39
+ },
40
+ {
41
+ label: 'ShareGPT',
42
+ value: Tab.ShareGPT,
43
+ },
44
+ ],
45
+ [],
46
+ );
47
+
48
+ const settings: ItemGroup = useMemo(
49
+ () =>
50
+ ({
51
+ children: [
52
+ {
53
+ children: <Switch checked={withSystemRole} onChange={setWithSystemRole} />,
54
+ label: t('shareModal.withSystemRole'),
55
+ minWidth: undefined,
56
+ },
57
+ tab === Tab.Screenshot && {
58
+ children: <Switch checked={withBackground} onChange={setWithBackground} />,
59
+ label: t('shareModal.withBackground'),
60
+ minWidth: undefined,
61
+ },
62
+ tab === Tab.Screenshot && {
63
+ children: <Switch checked={withFooter} onChange={setWithFooter} />,
64
+ label: t('shareModal.withFooter'),
65
+ minWidth: undefined,
66
+ },
67
+ tab === Tab.Screenshot && {
68
+ children: (
69
+ <Segmented
70
+ onChange={(value) => setImageType(value as ImageType)}
71
+ options={imageTypeOptions}
72
+ value={imageType}
73
+ />
74
+ ),
75
+ label: t('shareModal.imageType'),
76
+ minWidth: undefined,
77
+ },
78
+ tab === Tab.ShareGPT && {
79
+ children: <Switch checked={withPluginInfo} onChange={setWithPluginInfo} />,
80
+ label: t('shareModal.withPluginInfo'),
81
+ minWidth: undefined,
82
+ },
83
+ ].filter(Boolean),
84
+ icon: Share2,
85
+ title: t('shareModal.settings'),
86
+ }) as ItemGroup,
87
+ [tab, withSystemRole, withPluginInfo, withBackground, withFooter, imageType],
88
+ );
89
+
90
+ return (
91
+ <Flexbox gap={16}>
92
+ <Segmented
93
+ block
94
+ onChange={(value) => setTab(value as Tab)}
95
+ options={options}
96
+ style={{ width: '100%' }}
97
+ value={tab}
98
+ />
99
+ <Form items={[settings]} {...FORM_STYLE} />
100
+ {tab === Tab.Screenshot && (
101
+ <Preview
102
+ imageType={imageType}
103
+ withBackground={withBackground}
104
+ withFooter={withFooter}
105
+ withSystemRole={withSystemRole}
106
+ />
107
+ )}
108
+ {tab === Tab.ShareGPT && (
109
+ <Button
110
+ block
111
+ loading={shareLoading}
112
+ onClick={() => shareToShareGPT({ avatar, withPluginInfo, withSystemRole })}
113
+ size={'large'}
114
+ type={'primary'}
115
+ >
116
+ {t('shareModal.shareToShareGPT')}
117
+ </Button>
118
+ )}
119
+ </Flexbox>
25
120
  );
26
121
  });
27
122
 
28
- export default ShareButton;
123
+ export default Inner;
@@ -0,0 +1,140 @@
1
+ import { SiOpenai } from '@icons-pack/react-simple-icons';
2
+ import { Avatar, ChatHeaderTitle, Logo, Markdown, Tag } from '@lobehub/ui';
3
+ import { Button, SegmentedProps } from 'antd';
4
+ import dayjs from 'dayjs';
5
+ import { toJpeg, toPng, toSvg } from 'html-to-image';
6
+ import { memo, useCallback, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import pkg from '@/../package.json';
11
+ import ChatList from '@/app/chat/features/Conversation/ChatList';
12
+ import PluginTag from '@/app/chat/features/Header/PluginTag';
13
+ import { useSessionStore } from '@/store/session';
14
+ import { agentSelectors } from '@/store/session/slices/agentConfig';
15
+ import { sessionSelectors } from '@/store/session/slices/session/selectors';
16
+
17
+ import { useStyles } from './style';
18
+
19
+ export enum ImageType {
20
+ JPG = 'jpg',
21
+ PNG = 'png',
22
+ SVG = 'svg',
23
+ }
24
+
25
+ export const imageTypeOptions: SegmentedProps['options'] = [
26
+ {
27
+ label: 'JPG',
28
+ value: ImageType.JPG,
29
+ },
30
+ {
31
+ label: 'PNG',
32
+ value: ImageType.PNG,
33
+ },
34
+ {
35
+ label: 'SVG',
36
+ value: ImageType.SVG,
37
+ },
38
+ ];
39
+
40
+ interface PreviewProps {
41
+ imageType: ImageType;
42
+ withBackground: boolean;
43
+ withFooter: boolean;
44
+ withSystemRole: boolean;
45
+ }
46
+
47
+ const Preview = memo<PreviewProps>(({ withSystemRole, imageType, withBackground, withFooter }) => {
48
+ const [loading, setLoading] = useState(false);
49
+ const [isInbox, title, description, avatar, backgroundColor, model, plugins, systemRole] =
50
+ useSessionStore((s) => [
51
+ sessionSelectors.isInboxSession(s),
52
+ agentSelectors.currentAgentTitle(s),
53
+ agentSelectors.currentAgentDescription(s),
54
+ agentSelectors.currentAgentAvatar(s),
55
+ agentSelectors.currentAgentBackgroundColor(s),
56
+ agentSelectors.currentAgentModel(s),
57
+ agentSelectors.currentAgentPlugins(s),
58
+ agentSelectors.currentAgentSystemRole(s),
59
+ ]);
60
+ const { t } = useTranslation('common');
61
+ const { styles } = useStyles(withBackground);
62
+
63
+ const displayTitle = isInbox ? t('inbox.title') : title;
64
+ const displayDesc = isInbox ? t('inbox.desc') : description;
65
+
66
+ const handleDownload = useCallback(async () => {
67
+ setLoading(true);
68
+ try {
69
+ let screenshotFn: any;
70
+ switch (imageType) {
71
+ case ImageType.JPG: {
72
+ screenshotFn = toJpeg;
73
+ break;
74
+ }
75
+ case ImageType.PNG: {
76
+ screenshotFn = toPng;
77
+ break;
78
+ }
79
+ case ImageType.SVG: {
80
+ screenshotFn = toSvg;
81
+ break;
82
+ }
83
+ }
84
+ const dataUrl = await screenshotFn(document.querySelector('#preview') as HTMLDivElement);
85
+ const link = document.createElement('a');
86
+ link.download = `LobeChat_${title}_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
87
+ link.href = dataUrl;
88
+ link.click();
89
+ setLoading(false);
90
+ } catch {
91
+ console.error('Failed to download image');
92
+ setLoading(false);
93
+ }
94
+ }, [imageType, title]);
95
+
96
+ return (
97
+ <>
98
+ <div className={styles.preview}>
99
+ <div className={withBackground ? styles.background : undefined} id={'preview'}>
100
+ <Flexbox className={styles.container} gap={16}>
101
+ <div className={styles.header}>
102
+ <Flexbox align={'flex-start'} gap={12} horizontal>
103
+ <Avatar avatar={avatar} background={backgroundColor} size={40} title={title} />
104
+ <ChatHeaderTitle
105
+ desc={displayDesc}
106
+ tag={
107
+ <>
108
+ <Tag icon={<SiOpenai size={'1em'} />}>{model}</Tag>
109
+ {plugins?.length > 0 && <PluginTag plugins={plugins} />}
110
+ </>
111
+ }
112
+ title={displayTitle}
113
+ />
114
+ </Flexbox>
115
+ {withSystemRole && systemRole && (
116
+ <div className={styles.role}>
117
+ <Markdown>{systemRole}</Markdown>
118
+ </div>
119
+ )}
120
+ </div>
121
+ <ChatList />
122
+ {withFooter ? (
123
+ <Flexbox align={'center'} className={styles.footer} gap={4}>
124
+ <Logo extra={'chat'} type={'combine'} />
125
+ <div className={styles.url}>{pkg.homepage}</div>
126
+ </Flexbox>
127
+ ) : (
128
+ <div />
129
+ )}
130
+ </Flexbox>
131
+ </div>
132
+ </div>
133
+ <Button block loading={loading} onClick={handleDownload} size={'large'} type={'primary'}>
134
+ {t('shareModal.download')}
135
+ </Button>
136
+ </>
137
+ );
138
+ });
139
+
140
+ export default Preview;
@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useSessionStore } from '@/store/session';
7
7
 
8
+ import Inner from './Inner';
9
+
8
10
  const ShareButton = memo(() => {
9
11
  const [isModalOpen, setIsModalOpen] = useState(false);
10
12
  const { t } = useTranslation('common');
@@ -20,11 +22,14 @@ const ShareButton = memo(() => {
20
22
  title={t('share')}
21
23
  />
22
24
  <Modal
25
+ centered={false}
23
26
  footer={null}
24
27
  onCancel={() => setIsModalOpen(false)}
25
28
  open={isModalOpen}
26
29
  title={t('share')}
27
- />
30
+ >
31
+ <Inner />
32
+ </Modal>
28
33
  </>
29
34
  );
30
35
  });
@@ -0,0 +1,71 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { rgba } from 'polished';
3
+
4
+ export const useStyles = createStyles(({ css, token, stylish, cx }, withBackground: boolean) => ({
5
+ background: css`
6
+ padding: 24px;
7
+ background: url('/images/screenshot_background.webp') no-repeat;
8
+ background-size: 100% 100%;
9
+ `,
10
+ container: cx(
11
+ withBackground &&
12
+ css`
13
+ overflow: hidden;
14
+ border: 1px solid ${rgba(token.colorText, 0.15)};
15
+ border-radius: ${token.borderRadiusLG}px;
16
+ box-shadow:
17
+ 0 8px 16px -4px rgba(0, 0, 0, 75%),
18
+ 0 8px 24px -4px rgba(0, 0, 0, 75%);
19
+ `,
20
+ css`
21
+ background: ${token.colorBgLayout};
22
+ `,
23
+ ),
24
+ footer: css`
25
+ padding: 16px;
26
+ border-top: 1px solid ${token.colorBorder};
27
+ `,
28
+ header: css`
29
+ margin-bottom: -24px;
30
+ padding: 16px;
31
+ background: ${token.colorBgContainer};
32
+ border-bottom: 1px solid ${token.colorBorder};
33
+ `,
34
+ preview: cx(
35
+ stylish.noScrollbar,
36
+ css`
37
+ overflow-x: hidden;
38
+ overflow-y: scroll;
39
+
40
+ width: 100%;
41
+ max-height: 40vh;
42
+
43
+ background: ${token.colorBgLayout};
44
+ border: 1px solid ${token.colorBorder};
45
+ border-radius: ${token.borderRadiusLG}px;
46
+
47
+ * {
48
+ pointer-events: none;
49
+ overflow: hidden;
50
+
51
+ ::-webkit-scrollbar {
52
+ width: 0 !important;
53
+ height: 0 !important;
54
+ }
55
+ }
56
+ `,
57
+ ),
58
+ role: css`
59
+ margin-top: 12px;
60
+ padding-top: 12px;
61
+ opacity: 0.75;
62
+ border-top: 1px dashed ${token.colorBorderSecondary};
63
+
64
+ * {
65
+ font-size: 12px !important;
66
+ }
67
+ `,
68
+ url: css`
69
+ color: ${token.colorTextDescription};
70
+ `,
71
+ }));
@@ -4,6 +4,7 @@ import { createStyles } from 'antd-style';
4
4
  import { CSSProperties, memo } from 'react';
5
5
 
6
6
  import { useGlobalStore } from '@/store/global';
7
+ import { imageToBase64 } from '@/utils/imageToBase64';
7
8
  import { createUploadImageHandler } from '@/utils/uploadFIle';
8
9
 
9
10
  const useStyle = createStyles(
@@ -26,56 +27,33 @@ const useStyle = createStyles(
26
27
 
27
28
  interface AvatarWithUploadProps {
28
29
  compressSize?: number;
30
+ id?: string;
29
31
  size?: number;
30
32
  style?: CSSProperties;
31
33
  }
32
34
 
33
- const AvatarWithUpload = memo<AvatarWithUploadProps>(({ size = 40, compressSize = 128, style }) => {
34
- const [avatar, setSettings] = useGlobalStore((st) => [st.settings.avatar, st.setSettings]);
35
- const { styles } = useStyle();
36
-
37
- const handleUploadAvatar = createUploadImageHandler((avatar) => {
38
- const img = new Image();
39
- img.src = avatar;
40
- img.addEventListener('load', () => {
41
- const canvas = document.createElement('canvas');
42
- const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
43
- let startX = 0;
44
- let startY = 0;
45
-
46
- if (img.width > img.height) {
47
- startX = (img.width - img.height) / 2;
48
- } else {
49
- startY = (img.height - img.width) / 2;
50
- }
51
-
52
- canvas.width = compressSize;
53
- canvas.height = compressSize;
54
-
55
- ctx.drawImage(
56
- img,
57
- startX,
58
- startY,
59
- Math.min(img.width, img.height),
60
- Math.min(img.width, img.height),
61
- 0,
62
- 0,
63
- compressSize,
64
- compressSize,
65
- );
66
-
67
- const webpBase64 = canvas.toDataURL('image/webp');
68
- setSettings({ avatar: webpBase64 });
35
+ const AvatarWithUpload = memo<AvatarWithUploadProps>(
36
+ ({ size = 40, compressSize = 128, style, id }) => {
37
+ const [avatar, setSettings] = useGlobalStore((st) => [st.settings.avatar, st.setSettings]);
38
+ const { styles } = useStyle();
39
+
40
+ const handleUploadAvatar = createUploadImageHandler((avatar) => {
41
+ const img = new Image();
42
+ img.src = avatar;
43
+ img.addEventListener('load', () => {
44
+ const webpBase64 = imageToBase64({ img, size: compressSize });
45
+ setSettings({ avatar: webpBase64 });
46
+ });
69
47
  });
70
- });
71
48
 
72
- return (
73
- <div className={styles} style={{ maxHeight: size, maxWidth: size, ...style }}>
74
- <Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
75
- {avatar ? <Avatar avatar={avatar} size={size} /> : <Logo size={size} />}
76
- </Upload>
77
- </div>
78
- );
79
- });
49
+ return (
50
+ <div className={styles} id={id} style={{ maxHeight: size, maxWidth: size, ...style }}>
51
+ <Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
52
+ {avatar ? <Avatar avatar={avatar} size={size} /> : <Logo size={size} />}
53
+ </Upload>
54
+ </div>
55
+ );
56
+ },
57
+ );
80
58
 
81
59
  export default AvatarWithUpload;
@@ -12,7 +12,7 @@ export default memo(() => {
12
12
 
13
13
  return (
14
14
  <SideNav
15
- avatar={<AvatarWithUpload />}
15
+ avatar={<AvatarWithUpload id={'avatar'} />}
16
16
  bottomActions={<BottomActions setTab={setTab} tab={tab} />}
17
17
  style={{ height: '100vh' }}
18
18
  topActions={<TopActions setTab={setTab} tab={tab} />}
@@ -7,6 +7,8 @@ import type { Namespaces } from '@/types/locale';
7
7
 
8
8
  import resources from './resources';
9
9
 
10
+ const isDev = process.env.NODE_ENV === 'development';
11
+
10
12
  export const createI18nNext = (params: { namespace?: Namespaces[] | Namespaces } = {}) => {
11
13
  const { namespace } = params;
12
14
 
@@ -18,16 +20,16 @@ export const createI18nNext = (params: { namespace?: Namespaces[] | Namespaces }
18
20
  .use(LanguageDetector)
19
21
  .use(initReactI18next)
20
22
  .init({
21
- debug: process.env.NODE_ENV === 'development',
23
+ debug: isDev,
22
24
  defaultNS: ns,
23
25
  detection: {
24
26
  caches: [],
25
27
  },
26
- fallbackLng: 'en-US',
28
+ fallbackLng: isDev ? 'zh-CN' : 'en-US',
27
29
  interpolation: {
28
30
  escapeValue: false,
29
31
  },
30
- lng: 'en-US',
32
+ lng: isDev ? 'zh-CN' : 'en-US',
31
33
  ns,
32
34
  resources,
33
35
  });
@@ -49,6 +49,7 @@ export default {
49
49
  newAgent: '新建助手',
50
50
  noDescription: '暂无描述',
51
51
  ok: '确定',
52
+ password: '密码',
52
53
  pin: '置顶',
53
54
  pinOff: '取消置顶',
54
55
  regenerate: '重新生成',
@@ -62,6 +63,17 @@ export default {
62
63
  sessionList: '助手列表',
63
64
  setting: '设置',
64
65
  share: '分享',
66
+ shareModal: {
67
+ download: '下载截图',
68
+ imageType: '图片格式',
69
+ screenshot: '截图',
70
+ settings: '导出设置',
71
+ shareToShareGPT: '生成 ShareGPT 分享链接',
72
+ withBackground: '包含背景图片',
73
+ withFooter: '包含页脚',
74
+ withPluginInfo: '包含插件信息',
75
+ withSystemRole: '包含助手角色设定',
76
+ },
65
77
  stop: '停止',
66
78
  tab: {
67
79
  chat: '会话',
@@ -1,10 +1,20 @@
1
1
  import { ShareGPTConversation } from '@/types/share';
2
+ import { parseMarkdown } from '@/utils/parseMarkdown';
2
3
 
3
4
  const SHAREGPT_URL = 'https://sharegpt.com/api/conversations';
4
5
 
5
6
  export const genShareGPTUrl = async (conversation: ShareGPTConversation) => {
7
+ const items = [];
8
+
9
+ for (const item of conversation.items) {
10
+ items.push({
11
+ from: item.from,
12
+ value: item.from === 'gpt' ? await parseMarkdown(item.value) : item.value,
13
+ });
14
+ }
15
+
6
16
  const res = await fetch(SHAREGPT_URL, {
7
- body: JSON.stringify(conversation),
17
+ body: JSON.stringify({ ...conversation, items }),
8
18
  headers: {
9
19
  'Content-Type': 'application/json',
10
20
  },
@@ -1,3 +1,4 @@
1
+ import dayjs from 'dayjs';
1
2
  import { produce } from 'immer';
2
3
  import { StateCreator } from 'zustand/vanilla';
3
4
 
@@ -9,9 +10,45 @@ import { agentSelectors } from '../../agentConfig';
9
10
  import { sessionSelectors } from '../../session/selectors';
10
11
  import { chatSelectors } from '../selectors';
11
12
 
13
+ interface ShareMessage {
14
+ from: 'human' | 'gpt';
15
+ value: string;
16
+ }
17
+ const AVATAR =
18
+ 'data:image/webp;base64,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';
19
+
20
+ const Footer: ShareMessage = {
21
+ from: 'gpt',
22
+ value: `Share from [**🤯 LobeChat**](https://github.com/lobehub/lobe-chat) - ${dayjs().format(
23
+ 'YYYY-MM-DD',
24
+ )}`,
25
+ };
26
+
27
+ const PLUGIN_INFO = (plugin: {
28
+ apiName: string;
29
+ content: string;
30
+ identifier: string;
31
+ }): ShareMessage => ({
32
+ from: 'gpt',
33
+ value: [
34
+ `**🧩 Function Calling Plugin**`,
35
+ `- Identifier: \`${plugin.identifier}\``,
36
+ `- API name: \`${plugin.apiName}\``,
37
+ `- Result:`,
38
+ ``,
39
+ '```json',
40
+ plugin.content,
41
+ '```',
42
+ ].join('\n'),
43
+ });
44
+
12
45
  // const t = setNamespace('chat/share');
13
46
  export interface ShareAction {
14
- shareToShareGPT: () => void;
47
+ shareToShareGPT: (props: {
48
+ avatar?: string;
49
+ withPluginInfo?: boolean;
50
+ withSystemRole?: boolean;
51
+ }) => void;
15
52
  }
16
53
 
17
54
  export const chatShare: StateCreator<
@@ -20,72 +57,69 @@ export const chatShare: StateCreator<
20
57
  [],
21
58
  ShareAction
22
59
  > = (set, get) => ({
23
- // genShareUrl: () => {
24
- // const session = sessionSelectors.currentSession(get());
25
- // if (!session) return '';
26
- //
27
- // const agent = session.config;
28
- // return genShareMessagesUrl(session.chats, agent.systemRole);
29
- // },
30
- shareToShareGPT: async () => {
60
+ shareToShareGPT: async ({ withSystemRole, withPluginInfo, avatar }) => {
31
61
  const session = sessionSelectors.currentSession(get());
32
62
  if (!session) return;
33
63
  const messages = chatSelectors.currentChats(get());
34
64
  const config = agentSelectors.currentAgentConfig(get());
65
+ const meta = agentSelectors.currentAgentMeta(get());
35
66
 
36
67
  const defaultMsg: ShareGPTConversation['items'] = [];
37
-
68
+ const showSystemRole = withSystemRole && !!config.systemRole;
38
69
  const shareMsgs = produce(defaultMsg, (draft) => {
39
- if (!!config.systemRole) {
40
- draft.push({
41
- from: 'human',
42
- value: `-------------- ### This is a system settings | 这是一条系统设定
43
-
44
- ${config.systemRole}`,
45
- });
46
- }
70
+ draft.push({
71
+ from: 'gpt',
72
+ value: [
73
+ `${meta.avatar} **${meta.title}** - ${meta.description}`,
74
+ showSystemRole && '---',
75
+ showSystemRole && config.systemRole,
76
+ ]
77
+ .filter(Boolean)
78
+ .join('\n\n'),
79
+ });
47
80
 
48
81
  for (const i of messages) {
49
82
  switch (i.role) {
50
- case 'user':
51
- default: {
52
- draft.push({ from: 'human', value: i.content });
53
- break;
54
- }
55
-
56
83
  case 'assistant': {
57
84
  draft.push({ from: 'gpt', value: i.content });
58
85
  break;
59
86
  }
60
87
  case 'function': {
61
- draft.push({
62
- from: 'gpt',
63
- value: `-------------- ### This is a function call | 这是一次插件调用
64
- plugin name: ${i.plugin?.identifier}
65
- function call name: ${i.plugin?.apiName}
66
- result:
67
- \`\`\`json
68
- ${i.content}
69
- \`\`\``,
70
- });
88
+ if (withPluginInfo)
89
+ draft.push(
90
+ PLUGIN_INFO({
91
+ apiName: i.plugin?.apiName || 'undefined',
92
+ content: i.content,
93
+ identifier: i.plugin?.identifier || 'undefined',
94
+ }),
95
+ );
96
+ break;
97
+ }
98
+ case 'user': {
99
+ draft.push({ from: 'human', value: i.content });
71
100
  break;
72
101
  }
73
102
  }
74
103
  }
75
104
 
76
- draft.push({
77
- from: 'human',
78
- value: 'Share from [LobeChat](https://github.com/lobehub/lobe-chat)',
79
- });
105
+ draft.push(Footer);
80
106
  });
81
107
 
82
108
  set({ shareLoading: true });
109
+
83
110
  const res = await genShareGPTUrl({
84
- avatarUrl: 'https://npm.elemecdn.com/@lobehub/assets-logo/assets/logo-3d.webp',
111
+ avatarUrl: avatar || AVATAR,
85
112
  items: shareMsgs,
86
113
  });
87
114
  set({ shareLoading: false });
88
115
 
89
116
  window.open(res, '_blank');
90
117
  },
118
+ // genShareUrl: () => {
119
+ // const session = sessionSelectors.currentSession(get());
120
+ // if (!session) return '';
121
+ //
122
+ // const agent = session.config;
123
+ // return genShareMessagesUrl(session.chats, agent.systemRole);
124
+ // },
91
125
  });
@@ -0,0 +1,37 @@
1
+ export const imageToBase64 = ({
2
+ size,
3
+ img,
4
+ type = 'image/webp',
5
+ }: {
6
+ img: HTMLImageElement;
7
+ size: number;
8
+ type?: string;
9
+ }) => {
10
+ const canvas = document.createElement('canvas');
11
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
12
+ let startX = 0;
13
+ let startY = 0;
14
+
15
+ if (img.width > img.height) {
16
+ startX = (img.width - img.height) / 2;
17
+ } else {
18
+ startY = (img.height - img.width) / 2;
19
+ }
20
+
21
+ canvas.width = size;
22
+ canvas.height = size;
23
+
24
+ ctx.drawImage(
25
+ img,
26
+ startX,
27
+ startY,
28
+ Math.min(img.width, img.height),
29
+ Math.min(img.width, img.height),
30
+ 0,
31
+ 0,
32
+ size,
33
+ size,
34
+ );
35
+
36
+ return canvas.toDataURL(type);
37
+ };
@@ -0,0 +1,9 @@
1
+ import { remark } from 'remark';
2
+ import remarkGfm from 'remark-gfm';
3
+ import remarkHtml from 'remark-html';
4
+
5
+ export const parseMarkdown = async (content: string) => {
6
+ const file = await remark().use(remarkGfm).use(remarkHtml).process(content.trim());
7
+
8
+ return String(file);
9
+ };