@lobehub/chat 0.80.2 → 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 +75 -0
- package/docs/Analytics.zh-CN.md +19 -0
- package/docs/Environment-Variable.zh-CN.md +77 -1
- package/locales/en_US/common.json +12 -0
- package/locales/ru_RU/common.json +12 -0
- package/locales/zh_CN/common.json +12 -0
- package/locales/zh_TW/common.json +12 -0
- package/next.config.mjs +4 -0
- package/package.json +9 -3
- package/public/images/screenshot_background.webp +0 -0
- package/public/manifest.json +2 -16
- package/src/app/chat/features/Conversation/ChatList/Error/InvalidAccess.tsx +16 -6
- package/src/app/chat/features/Header/ShareButton/Inner.tsx +109 -14
- package/src/app/chat/features/Header/ShareButton/Preview.tsx +140 -0
- package/src/app/chat/features/Header/ShareButton/index.tsx +6 -1
- package/src/app/chat/features/Header/ShareButton/style.ts +71 -0
- package/src/components/Analytics/Mixpanel.tsx +23 -0
- package/src/components/Analytics/Plausible.tsx +18 -0
- package/src/components/Analytics/Posthog.tsx +23 -0
- package/src/components/Analytics/Vercel.tsx +12 -0
- package/src/components/Analytics/index.tsx +19 -2
- package/src/components/AppTheme/index.tsx +3 -4
- package/src/config/client.ts +46 -0
- package/src/config/server.ts +9 -6
- package/src/const/url.ts +4 -2
- package/src/features/AvatarWithUpload/index.tsx +23 -45
- package/src/features/SideBar/index.tsx +1 -1
- package/src/locales/create.ts +5 -3
- package/src/locales/default/common.ts +12 -0
- package/src/services/shareGPT.ts +11 -1
- package/src/store/session/slices/chat/actions/share.ts +73 -39
- package/src/utils/imageToBase64.ts +37 -0
- package/src/utils/parseMarkdown.ts +9 -0
- package/public/icons/icon-maskable-192x192.png +0 -0
- package/public/icons/icon-maskable-512x512.png +0 -0
|
@@ -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
|
+
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import mixpanel from 'mixpanel-browser';
|
|
4
|
+
import { memo, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
import { getClientConfig } from '@/config/client';
|
|
7
|
+
|
|
8
|
+
const { MIXPANEL_PROJECT_TOKEN, MIXPANEL_DEBUG } = getClientConfig();
|
|
9
|
+
|
|
10
|
+
const MixpanelAnalytics = memo(() => {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!MIXPANEL_PROJECT_TOKEN) return;
|
|
13
|
+
|
|
14
|
+
mixpanel.init(MIXPANEL_PROJECT_TOKEN, {
|
|
15
|
+
debug: MIXPANEL_DEBUG,
|
|
16
|
+
persistence: 'localStorage',
|
|
17
|
+
track_pageview: true,
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
return null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default MixpanelAnalytics;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Script from 'next/script';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { getClientConfig } from '@/config/client';
|
|
7
|
+
|
|
8
|
+
const { PLAUSIBLE_DOMAIN } = getClientConfig();
|
|
9
|
+
|
|
10
|
+
const PlausibleAnalytics = memo(() => {
|
|
11
|
+
return (
|
|
12
|
+
PLAUSIBLE_DOMAIN && (
|
|
13
|
+
<Script data-domain={PLAUSIBLE_DOMAIN} defer src="https://plausible.io/js/script.js" />
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export default PlausibleAnalytics;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import posthog from 'posthog-js';
|
|
4
|
+
import { FC, memo, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
import { getClientConfig } from '@/config/client';
|
|
7
|
+
|
|
8
|
+
const { POSTHOG_HOST, POSTHOG_KEY, POSTHOG_DEBUG } = getClientConfig();
|
|
9
|
+
|
|
10
|
+
const PostHog: FC = memo(() => {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!POSTHOG_KEY) return;
|
|
13
|
+
|
|
14
|
+
posthog.init(POSTHOG_KEY, {
|
|
15
|
+
api_host: POSTHOG_HOST ?? 'https://app.posthog.com',
|
|
16
|
+
debug: POSTHOG_DEBUG,
|
|
17
|
+
});
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default PostHog;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Analytics } from '@vercel/analytics/react';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { getClientConfig } from '@/config/client';
|
|
7
|
+
|
|
8
|
+
const { VERCEL_DEBUG } = getClientConfig();
|
|
9
|
+
|
|
10
|
+
const VercelAnalytics = memo(() => <Analytics debug={VERCEL_DEBUG} />);
|
|
11
|
+
|
|
12
|
+
export default VercelAnalytics;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import dynamic from 'next/dynamic';
|
|
2
|
+
|
|
3
|
+
import { getClientConfig } from '@/config/client';
|
|
4
|
+
|
|
5
|
+
const Vercel = dynamic(() => import('./Vercel'), { ssr: false });
|
|
6
|
+
const Mixpanel = dynamic(() => import('./Mixpanel'), { ssr: false });
|
|
7
|
+
const Plausible = dynamic(() => import('./Plausible'), { ssr: false });
|
|
8
|
+
const Posthog = dynamic(() => import('./Posthog'), { ssr: false });
|
|
9
|
+
|
|
10
|
+
const { ANALYTICS_VERCEL, ANALYTICS_POSTHOG, ANALYTICS_MIXPANEL, ANALYTICS_PLAUSIBLE } =
|
|
11
|
+
getClientConfig();
|
|
2
12
|
|
|
3
13
|
const Analytics = () => {
|
|
4
|
-
return
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
{ANALYTICS_VERCEL && <Vercel />}
|
|
17
|
+
{ANALYTICS_PLAUSIBLE && <Plausible />}
|
|
18
|
+
{ANALYTICS_MIXPANEL && <Mixpanel />}
|
|
19
|
+
{ANALYTICS_POSTHOG && <Posthog />}
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
5
22
|
};
|
|
6
23
|
|
|
7
24
|
export default Analytics;
|
|
@@ -20,9 +20,9 @@ export interface AppThemeProps {
|
|
|
20
20
|
|
|
21
21
|
const AppTheme = memo<AppThemeProps>(
|
|
22
22
|
({ children, defaultAppearance, defaultPrimaryColor, defaultNeutralColor }) => {
|
|
23
|
-
console.log('server:appearance', defaultAppearance);
|
|
24
|
-
console.log('server:primaryColor', defaultPrimaryColor);
|
|
25
|
-
console.log('server:neutralColor', defaultNeutralColor);
|
|
23
|
+
// console.log('server:appearance', defaultAppearance);
|
|
24
|
+
// console.log('server:primaryColor', defaultPrimaryColor);
|
|
25
|
+
// console.log('server:neutralColor', defaultNeutralColor);
|
|
26
26
|
const themeMode = useGlobalStore((s) => s.settings.themeMode);
|
|
27
27
|
|
|
28
28
|
const [primaryColor, neutralColor] = useGlobalStore((s) => [
|
|
@@ -31,7 +31,6 @@ const AppTheme = memo<AppThemeProps>(
|
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
33
|
useEffect(() => {
|
|
34
|
-
console.log(primaryColor);
|
|
35
34
|
setCookie(LOBE_THEME_PRIMARY_COLOR, primaryColor);
|
|
36
35
|
}, [primaryColor]);
|
|
37
36
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
5
|
+
namespace NodeJS {
|
|
6
|
+
interface ProcessEnv {
|
|
7
|
+
AGENTS_INDEX_URL?: string;
|
|
8
|
+
PLUGINS_INDEX_URL?: string;
|
|
9
|
+
|
|
10
|
+
NEXT_PUBLIC_ANALYTICS_VERCEL?: string;
|
|
11
|
+
NEXT_PUBLIC_VERCEL_DEBUG?: string;
|
|
12
|
+
|
|
13
|
+
NEXT_PUBLIC_ANALYTICS_MIXPANEL?: string;
|
|
14
|
+
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN?: string;
|
|
15
|
+
NEXT_PUBLIC_MIXPANEL_DEBUG?: string;
|
|
16
|
+
|
|
17
|
+
NEXT_PUBLIC_ANALYTICS_PLAUSIBLE?: string;
|
|
18
|
+
NEXT_PUBLIC_PLAUSIBLE_DOMAIN?: string;
|
|
19
|
+
|
|
20
|
+
NEXT_PUBLIC_ANALYTICS_POSTHOG: string;
|
|
21
|
+
NEXT_PUBLIC_POSTHOG_KEY: string;
|
|
22
|
+
NEXT_PUBLIC_POSTHOG_HOST: string;
|
|
23
|
+
NEXT_PUBLIC_POSTHOG_DEBUG: string;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getClientConfig = () => ({
|
|
29
|
+
AGENTS_INDEX_URL: process.env.AGENTS_INDEX_URL,
|
|
30
|
+
PLUGINS_INDEX_URL: process.env.PLUGINS_INDEX_URL,
|
|
31
|
+
|
|
32
|
+
ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL !== '0',
|
|
33
|
+
VERCEL_DEBUG: process.env.NEXT_PUBLIC_VERCEL_DEBUG === '1',
|
|
34
|
+
|
|
35
|
+
ANALYTICS_PLAUSIBLE: process.env.NEXT_PUBLIC_ANALYTICS_PLAUSIBLE === '1',
|
|
36
|
+
PLAUSIBLE_DOMAIN: process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN,
|
|
37
|
+
|
|
38
|
+
ANALYTICS_MIXPANEL: process.env.NEXT_PUBLIC_ANALYTICS_MIXPANEL === '1',
|
|
39
|
+
MIXPANEL_PROJECT_TOKEN: process.env.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN,
|
|
40
|
+
MIXPANEL_DEBUG: process.env.NEXT_PUBLIC_MIXPANEL_DEBUG === '1',
|
|
41
|
+
|
|
42
|
+
ANALYTICS_POSTHOG: process.env.NEXT_PUBLIC_ANALYTICS_POSTHOG === '1',
|
|
43
|
+
POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
|
44
|
+
POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
|
45
|
+
POSTHOG_DEBUG: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === '1',
|
|
46
|
+
});
|
package/src/config/server.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
|
|
2
|
+
|
|
1
3
|
declare global {
|
|
2
4
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
3
5
|
namespace NodeJS {
|
|
4
6
|
interface ProcessEnv {
|
|
5
7
|
ACCESS_CODE?: string;
|
|
6
|
-
|
|
7
|
-
AZURE_API_VERSION?: string;
|
|
8
|
+
|
|
8
9
|
OPENAI_API_KEY?: string;
|
|
9
10
|
OPENAI_PROXY_URL?: string;
|
|
11
|
+
|
|
12
|
+
AZURE_API_KEY?: string;
|
|
13
|
+
AZURE_API_VERSION?: string;
|
|
10
14
|
USE_AZURE_OPENAI?: string;
|
|
11
15
|
}
|
|
12
16
|
}
|
|
@@ -19,13 +23,12 @@ export const getServerConfig = () => {
|
|
|
19
23
|
|
|
20
24
|
return {
|
|
21
25
|
ACCESS_CODE: process.env.ACCESS_CODE,
|
|
22
|
-
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
23
|
-
AZURE_API_KEY: process.env.AZURE_API_KEY,
|
|
24
|
-
AZURE_API_VERSION: process.env.AZURE_API_VERSION,
|
|
25
|
-
/* eslint-enabled */
|
|
26
26
|
|
|
27
27
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
28
28
|
OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
|
|
29
|
+
|
|
30
|
+
AZURE_API_KEY: process.env.AZURE_API_KEY,
|
|
31
|
+
AZURE_API_VERSION: process.env.AZURE_API_VERSION,
|
|
29
32
|
USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1',
|
|
30
33
|
};
|
|
31
34
|
};
|
package/src/const/url.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import urlJoin from 'url-join';
|
|
2
2
|
|
|
3
|
+
import { getClientConfig } from '@/config/client';
|
|
3
4
|
import { localeOptions } from '@/locales/options';
|
|
4
5
|
import { Locales } from '@/locales/resources';
|
|
5
6
|
|
|
@@ -13,7 +14,7 @@ export const FEEDBACK = pkg.bugs.url;
|
|
|
13
14
|
export const DISCORD = 'https://discord.gg/AYFPHvv2jT';
|
|
14
15
|
|
|
15
16
|
export const PLUGINS_INDEX_URL =
|
|
16
|
-
|
|
17
|
+
getClientConfig().PLUGINS_INDEX_URL ?? 'https://chat-plugins.lobehub.com';
|
|
17
18
|
|
|
18
19
|
export const DEFAULT_LANG = 'en-US';
|
|
19
20
|
|
|
@@ -26,7 +27,8 @@ export const getPluginIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = PLUGI
|
|
|
26
27
|
return urlJoin(baseUrl, `index.${lang}.json`);
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
export const AGENTS_INDEX_URL =
|
|
30
|
+
export const AGENTS_INDEX_URL =
|
|
31
|
+
getClientConfig().AGENTS_INDEX_URL ?? 'https://chat-agents.lobehub.com';
|
|
30
32
|
|
|
31
33
|
export const getAgentIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = AGENTS_INDEX_URL) => {
|
|
32
34
|
if (checkLang(lang)) return baseUrl;
|
|
@@ -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>(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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} />}
|
package/src/locales/create.ts
CHANGED
|
@@ -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:
|
|
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: '会话',
|
package/src/services/shareGPT.ts
CHANGED
|
@@ -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
|
},
|