@lobehub/chat 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/docs/self-hosting/advanced/model-list.mdx +1 -1
  3. package/docs/self-hosting/advanced/model-list.zh-CN.mdx +1 -1
  4. package/locales/ar/chat.json +2 -0
  5. package/locales/ar/common.json +9 -0
  6. package/locales/bg-BG/chat.json +2 -0
  7. package/locales/bg-BG/common.json +9 -0
  8. package/locales/de-DE/chat.json +2 -0
  9. package/locales/de-DE/common.json +9 -0
  10. package/locales/en-US/chat.json +2 -0
  11. package/locales/en-US/common.json +11 -2
  12. package/locales/es-ES/chat.json +2 -0
  13. package/locales/es-ES/common.json +9 -0
  14. package/locales/fr-FR/chat.json +2 -0
  15. package/locales/fr-FR/common.json +9 -0
  16. package/locales/it-IT/chat.json +2 -0
  17. package/locales/it-IT/common.json +9 -0
  18. package/locales/ja-JP/chat.json +2 -0
  19. package/locales/ja-JP/common.json +9 -0
  20. package/locales/ko-KR/chat.json +2 -0
  21. package/locales/ko-KR/common.json +9 -0
  22. package/locales/nl-NL/chat.json +2 -0
  23. package/locales/nl-NL/common.json +9 -0
  24. package/locales/pl-PL/chat.json +2 -0
  25. package/locales/pl-PL/common.json +9 -0
  26. package/locales/pt-BR/chat.json +2 -0
  27. package/locales/pt-BR/common.json +9 -0
  28. package/locales/ru-RU/chat.json +2 -0
  29. package/locales/ru-RU/common.json +9 -0
  30. package/locales/tr-TR/chat.json +2 -0
  31. package/locales/tr-TR/common.json +9 -0
  32. package/locales/vi-VN/chat.json +2 -0
  33. package/locales/vi-VN/common.json +9 -0
  34. package/locales/vi-VN/setting.json +6 -10
  35. package/locales/zh-CN/chat.json +2 -0
  36. package/locales/zh-CN/common.json +9 -0
  37. package/locales/zh-TW/chat.json +2 -0
  38. package/locales/zh-TW/common.json +9 -0
  39. package/next-sitemap.config.mjs +2 -2
  40. package/package.json +2 -1
  41. package/src/app/(main)/_layout/Desktop.tsx +19 -12
  42. package/src/app/(main)/_layout/Mobile.tsx +5 -0
  43. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicContent.tsx +4 -1
  44. package/src/config/featureFlags/schema.ts +6 -0
  45. package/src/const/guide.ts +8 -4
  46. package/src/const/url.ts +2 -1
  47. package/src/features/AlertBanner/CloudBanner.tsx +91 -0
  48. package/src/features/ChatInput/ActionBar/Clear.tsx +15 -6
  49. package/src/features/ChatInput/Topic/index.tsx +44 -9
  50. package/src/features/User/UserPanel/useMenu.tsx +21 -2
  51. package/src/libs/agent-runtime/google/index.test.ts +3 -33
  52. package/src/libs/agent-runtime/google/index.ts +1 -16
  53. package/src/locales/default/chat.ts +2 -0
  54. package/src/locales/default/common.ts +10 -0
  55. package/src/store/serverConfig/selectors.test.ts +1 -0
  56. package/src/types/user/index.ts +3 -1
  57. package/src/utils/parseModels.test.ts +3 -3
@@ -361,8 +361,14 @@
361
361
  },
362
362
  "desc": "Truyền thông dữ liệu thời gian thực, điểm-điểm, cần thiết bị cùng online mới có thể đồng bộ",
363
363
  "enabled": {
364
+ "invalid": "Vui lòng nhập địa chỉ máy chủ tín hiệu và tên kênh đồng bộ trước khi bật",
364
365
  "title": "Bật đồng bộ"
365
366
  },
367
+ "signaling": {
368
+ "desc": "WebRTC sẽ sử dụng địa chỉ này để đồng bộ",
369
+ "placeholder": "Vui lòng nhập địa chỉ máy chủ tín hiệu",
370
+ "title": "Máy chủ tín hiệu"
371
+ },
366
372
  "title": "WebRTC Đồng bộ"
367
373
  }
368
374
  },
@@ -406,15 +412,5 @@
406
412
  "store": "Cửa hàng tiện ích"
407
413
  },
408
414
  "title": "Công cụ mở rộng"
409
- },
410
- "webrtc": {
411
- "enabled": {
412
- "invalid": "请填写信令服务器和同步频道名称后再开启"
413
- },
414
- "signaling": {
415
- "desc": "WebRTC sẽ sử dụng địa chỉ này để đồng bộ",
416
- "placeholder": "Vui lòng nhập địa chỉ máy chủ tín hiệu",
417
- "title": "Máy chủ tín hiệu"
418
- }
419
415
  }
420
416
  }
@@ -99,6 +99,7 @@
99
99
  "duplicate": "创建副本",
100
100
  "export": "导出话题"
101
101
  },
102
+ "checkOpenNewTopic": "是否开启新话题?",
102
103
  "confirmRemoveAll": "即将删除全部话题,删除后将不可恢复,请谨慎操作。",
103
104
  "confirmRemoveTopic": "即将删除该话题,删除后将不可恢复,请谨慎操作。",
104
105
  "confirmRemoveUnstarred": "即将删除未收藏话题,删除后将不可恢复,请谨慎操作。",
@@ -112,6 +113,7 @@
112
113
  "openNewTopic": "开启新话题",
113
114
  "removeAll": "删除全部话题",
114
115
  "removeUnstarred": "删除未收藏话题",
116
+ "checkSaveCurrentMessages": "是否保存当前会话为话题?",
115
117
  "saveCurrentMessages": "将当前会话保存为话题",
116
118
  "searchPlaceholder": "搜索话题...",
117
119
  "title": "话题"
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "about": "关于",
3
3
  "advanceSettings": "高级设置",
4
+ "alert": {
5
+ "cloud": {
6
+ "action": "立即体验",
7
+ "desc": "我们为所有注册用户提供了免费的 {{credit}} 额度计算积分,无需复杂配置开箱即用, 支持全局云同步与进阶联网查询,更多高级特性等你探索。",
8
+ "descOnMobile": "我们为所有注册用户提供了免费的 {{credit}} 额度计算积分,无需复杂配置开箱即用。",
9
+ "title": "{{name}} 开始公测"
10
+ }
11
+ },
4
12
  "appInitializing": "应用启动中,请耐心等待...",
5
13
  "autoGenerate": "自动补全",
6
14
  "autoGenerateTooltip": "基于提示词自动补全助手描述",
@@ -206,6 +214,7 @@
206
214
  "userPanel": {
207
215
  "anonymousNickName": "匿名用户",
208
216
  "billing": "账单管理",
217
+ "cloud": "体验 {{name}}",
209
218
  "data": "数据存储",
210
219
  "defaultNickname": "社区版用户",
211
220
  "discord": "社区支持",
@@ -99,6 +99,8 @@
99
99
  "duplicate": "建立副本",
100
100
  "export": "匯出主題"
101
101
  },
102
+ "checkOpenNewTopic": "是否開啟新主題?",
103
+ "checkSaveCurrentMessages": "是否將當前對話保存為話題?",
102
104
  "confirmRemoveAll": "即將刪除全部話題,刪除後將不可恢復,請謹慎操作。",
103
105
  "confirmRemoveTopic": "即將刪除該話題,刪除後將不可恢復,請謹慎操作。",
104
106
  "confirmRemoveUnstarred": "即將刪除未收藏話題,刪除後將不可恢復,請謹慎操作。",
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "about": "關於",
3
3
  "advanceSettings": "進階設定",
4
+ "alert": {
5
+ "cloud": {
6
+ "action": "免費體驗",
7
+ "desc": "我們為所有註冊用戶提供了 {{credit}} 免費的計算積分,無需複雜配置開箱即用,支持無限對話歷史記錄與全局雲同步,更多高級特性等你一起探索。",
8
+ "descOnMobile": "我們為所有註冊用戶提供了 {{credit}} 免費的計算積分,無需複雜配置即可使用。",
9
+ "title": "歡迎體驗 {{name}}"
10
+ }
11
+ },
4
12
  "appInitializing": "應用程式初始化中,請耐心等候...",
5
13
  "autoGenerate": "自動生成",
6
14
  "autoGenerateTooltip": "基於提示詞自動生成助手描述",
@@ -206,6 +214,7 @@
206
214
  "userPanel": {
207
215
  "anonymousNickName": "匿名使用者",
208
216
  "billing": "帳單管理",
217
+ "cloud": "體驗 {{name}}",
209
218
  "data": "資料儲存",
210
219
  "defaultNickname": "社群版使用者",
211
220
  "discord": "社區支援",
@@ -4,7 +4,7 @@ const isVercelPreview = process.env.VERCEL === '1' && process.env.VERCEL_ENV !==
4
4
 
5
5
  const vercelPreviewUrl = `https://${process.env.VERCEL_URL}`;
6
6
 
7
- const siteUrl = isVercelPreview ? vercelPreviewUrl : 'https://chat-preview.lobehub.com';
7
+ const siteUrl = isVercelPreview ? vercelPreviewUrl : 'https://lobechat.com';
8
8
 
9
9
  /** @type {import('next-sitemap').IConfig} */
10
10
  const config = {
@@ -46,8 +46,8 @@ const config = {
46
46
 
47
47
  return paths;
48
48
  },
49
- siteUrl,
50
49
  generateRobotsTxt: true,
50
+ siteUrl,
51
51
  };
52
52
 
53
53
  export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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",
@@ -170,6 +170,7 @@
170
170
  "random-words": "^2.0.1",
171
171
  "react": "^18.3.1",
172
172
  "react-dom": "^18.3.1",
173
+ "react-fast-marquee": "^1.6.5",
173
174
  "react-hotkeys-hook": "^4.5.0",
174
175
  "react-i18next": "14.0.2",
175
176
  "react-layout-kit": "^1.9.0",
@@ -4,7 +4,9 @@ import { useTheme } from 'antd-style';
4
4
  import { memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
+ import CloudBanner, { BANNER_HEIGHT } from '@/features/AlertBanner/CloudBanner';
7
8
  import { usePlatform } from '@/hooks/usePlatform';
9
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
8
10
 
9
11
  import { LayoutProps } from './type';
10
12
 
@@ -12,19 +14,24 @@ const Layout = memo<LayoutProps>(({ children, nav }) => {
12
14
  const { isPWA } = usePlatform();
13
15
  const theme = useTheme();
14
16
 
17
+ const { showCloudPromotion } = useServerConfigStore(featureFlagsSelectors);
18
+
15
19
  return (
16
- <Flexbox
17
- height={'100%'}
18
- horizontal
19
- style={{
20
- borderTop: isPWA ? `1px solid ${theme.colorBorder}` : undefined,
21
- position: 'relative',
22
- }}
23
- width={'100%'}
24
- >
25
- {nav}
26
- {children}
27
- </Flexbox>
20
+ <>
21
+ {showCloudPromotion && <CloudBanner />}
22
+ <Flexbox
23
+ height={showCloudPromotion ? `calc(100% - ${BANNER_HEIGHT}px)` : '100%'}
24
+ horizontal
25
+ style={{
26
+ borderTop: isPWA ? `1px solid ${theme.colorBorder}` : undefined,
27
+ position: 'relative',
28
+ }}
29
+ width={'100%'}
30
+ >
31
+ {nav}
32
+ {children}
33
+ </Flexbox>
34
+ </>
28
35
  );
29
36
  });
30
37
 
@@ -4,7 +4,9 @@ import { usePathname } from 'next/navigation';
4
4
  import qs from 'query-string';
5
5
  import { memo } from 'react';
6
6
 
7
+ import CloudBanner from '@/features/AlertBanner/CloudBanner';
7
8
  import { useQuery } from '@/hooks/useQuery';
9
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
8
10
 
9
11
  import { LayoutProps } from './type';
10
12
 
@@ -16,8 +18,11 @@ const Layout = memo(({ children, nav }: LayoutProps) => {
16
18
  const { url } = qs.parseUrl(pathname);
17
19
  const showNav = !showMobileWorkspace && MOBILE_NAV_ROUTES.has(url);
18
20
 
21
+ const { showCloudPromotion } = useServerConfigStore(featureFlagsSelectors);
22
+
19
23
  return (
20
24
  <>
25
+ {showCloudPromotion && <CloudBanner mobile />}
21
26
  {children}
22
27
  {showNav && nav}
23
28
  </>
@@ -14,6 +14,7 @@ import { memo, useMemo } from 'react';
14
14
  import { useTranslation } from 'react-i18next';
15
15
  import { Flexbox } from 'react-layout-kit';
16
16
 
17
+ import { useIsMobile } from '@/hooks/useIsMobile';
17
18
  import { useChatStore } from '@/store/chat';
18
19
 
19
20
  const useStyles = createStyles(({ css }) => ({
@@ -41,6 +42,8 @@ interface TopicContentProps {
41
42
  const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
42
43
  const { t } = useTranslation('common');
43
44
 
45
+ const mobile = useIsMobile();
46
+
44
47
  const [
45
48
  editing,
46
49
  favoriteTopic,
@@ -183,7 +186,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
183
186
  value={title}
184
187
  />
185
188
  )}
186
- {showMore && !editing && (
189
+ {(showMore || mobile) && !editing && (
187
190
  <Dropdown
188
191
  arrow={false}
189
192
  menu={{
@@ -18,6 +18,8 @@ export const FeatureFlagsSchema = z.object({
18
18
  welcome_suggest: z.boolean().optional(),
19
19
 
20
20
  clerk_sign_up: z.boolean().optional(),
21
+
22
+ cloud_promotion: z.boolean().optional(),
21
23
  });
22
24
 
23
25
  // TypeScript 类型,从 Zod schema 生成
@@ -40,6 +42,8 @@ export const DEFAULT_FEATURE_FLAGS: IFeatureFlags = {
40
42
  welcome_suggest: true,
41
43
 
42
44
  clerk_sign_up: true,
45
+
46
+ cloud_promotion: false,
43
47
  };
44
48
 
45
49
  export const mapFeatureFlagsEnvToState = (config: IFeatureFlags) => {
@@ -59,5 +63,7 @@ export const mapFeatureFlagsEnvToState = (config: IFeatureFlags) => {
59
63
  showWelcomeSuggest: config.welcome_suggest,
60
64
 
61
65
  enableClerkSignUp: config.clerk_sign_up,
66
+
67
+ showCloudPromotion: config.cloud_promotion,
62
68
  };
63
69
  };
@@ -6,6 +6,7 @@ import {
6
6
  EMAIL_BUSINESS,
7
7
  EMAIL_SUPPORT,
8
8
  GITHUB,
9
+ OFFICIAL_PREVIEW_URL,
9
10
  OFFICIAL_SITE,
10
11
  OFFICIAL_URL,
11
12
  SELF_HOSTING_DOCUMENTS,
@@ -39,10 +40,12 @@ and offers a one-click FREE deployment for a private ChatGPT chat application, m
39
40
  - [Plugin System (Function Calling)](${urlJoin(USAGE_DOCUMENTS, '/features/plugin-system')})
40
41
  - [Agent Market (GPTs)](${urlJoin(USAGE_DOCUMENTS, '/features/agent-market')})
41
42
 
42
- ### CE and Cloud Version
43
+ ### Community Edition and Cloud Version
43
44
 
44
- LobeChat is currently available as a community preview version, completely open-source and free of charge. The Cloud paid version is under development.
45
- Those interested can visit the [official website](${OFFICIAL_SITE}) to join the wishlist. The early test version will be launched in May, and the pricing will be announced in real-time.
45
+ LobeChat is currently available as a community preview version, completely open-source and free of charge.
46
+
47
+ In the LobeChat Cloud version, we provide 500,000 free computing credits to all registered users. It is ready to use without complex configurations.
48
+ If you require more usage, you can subscribe to the Basic, Advanced, or Professional versions for a fee.
46
49
 
47
50
  ### Self Hosting
48
51
 
@@ -60,7 +63,8 @@ Learn more about [Build your own LobeChat](${SELF_HOSTING_DOCUMENTS}) by checkin
60
63
  In the response, please try to pick and include the relevant links below, and if a relevant answer cannot be provided, also offer the user these related links:
61
64
 
62
65
  - Official Website: ${OFFICIAL_SITE}
63
- - Community Preview: ${OFFICIAL_URL}
66
+ - Cloud Version: ${OFFICIAL_URL}
67
+ - Community Edition: ${OFFICIAL_PREVIEW_URL}
64
68
  - GitHub Repository: ${GITHUB}
65
69
  - Latest News: ${BLOG}
66
70
  - Usage Documentation: ${USAGE_DOCUMENTS}
package/src/const/url.ts CHANGED
@@ -6,7 +6,8 @@ import { withBasePath } from '@/utils/basePath';
6
6
  import pkg from '../../package.json';
7
7
  import { INBOX_SESSION_ID } from './session';
8
8
 
9
- export const OFFICIAL_URL = 'https://chat-preview.lobehub.com/';
9
+ export const OFFICIAL_URL = 'https://lobechat.com/';
10
+ export const OFFICIAL_PREVIEW_URL = 'https://chat-preview.lobehub.com/';
10
11
  export const OFFICIAL_SITE = 'https://lobehub.com/';
11
12
 
12
13
  export const getCanonicalUrl = (path: string) => urlJoin(OFFICIAL_URL, path);
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import { Icon } from '@lobehub/ui';
4
+ import { useSize } from 'ahooks';
5
+ import { Button } from 'antd';
6
+ import { createStyles } from 'antd-style';
7
+ import { ArrowRightIcon } from 'lucide-react';
8
+ import Link from 'next/link';
9
+ import { memo, useEffect, useRef, useState } from 'react';
10
+ import Marquee from 'react-fast-marquee';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { Center, Flexbox } from 'react-layout-kit';
13
+
14
+ import { OFFICIAL_URL } from '@/const/url';
15
+ import { isOnServerSide } from '@/utils/env';
16
+
17
+ export const BANNER_HEIGHT = 40;
18
+
19
+ const useStyles = createStyles(({ css, token, stylish, cx, isDarkMode }) => ({
20
+ background: cx(
21
+ stylish.gradientAnimation,
22
+ css`
23
+ position: absolute;
24
+
25
+ width: max(64%, 1280px);
26
+ height: 100%;
27
+
28
+ opacity: 0.8;
29
+ filter: blur(60px);
30
+ `,
31
+ ),
32
+ container: css`
33
+ position: relative;
34
+ overflow: hidden;
35
+ background-color: ${isDarkMode ? token.colorFill : token.colorFillSecondary};
36
+ `,
37
+ wrapper: css`
38
+ z-index: 1;
39
+ overflow: hidden;
40
+ max-width: 100%;
41
+ `,
42
+ }));
43
+
44
+ const CloudBanner = memo<{ mobile?: boolean }>(({ mobile }) => {
45
+ const ref = useRef(null);
46
+ const contentRef = useRef(null);
47
+ const size = useSize(ref);
48
+ const contentSize = useSize(contentRef);
49
+ const { styles } = useStyles();
50
+ const { t } = useTranslation('common');
51
+ const [isTruncated, setIsTruncated] = useState(mobile);
52
+
53
+ useEffect(() => {
54
+ if (mobile || isOnServerSide || !size || !contentSize) return;
55
+ setIsTruncated(contentSize.width > size.width - 120);
56
+ }, [size, contentSize, mobile]);
57
+
58
+ const content = (
59
+ <Flexbox align={'center'} flex={'none'} gap={8} horizontal ref={contentRef}>
60
+ <b>{t('alert.cloud.title', { name: 'LobeChat Cloud' })}:</b>
61
+ <span>
62
+ {t(mobile ? 'alert.cloud.descOnMobile' : 'alert.cloud.desc', {
63
+ credit: new Intl.NumberFormat('en-US').format(500_000),
64
+ name: 'LobeChat Cloud',
65
+ })}
66
+ </span>
67
+ </Flexbox>
68
+ );
69
+ return (
70
+ <Center
71
+ className={styles.container}
72
+ flex={'none'}
73
+ height={BANNER_HEIGHT}
74
+ paddingInline={16}
75
+ ref={ref}
76
+ width={'100%'}
77
+ >
78
+ <div className={styles.background} />
79
+ <Center className={styles.wrapper} gap={16} horizontal width={'100%'}>
80
+ {isTruncated ? <Marquee pauseOnHover>{content}</Marquee> : content}
81
+ <Link href={OFFICIAL_URL} target={'_blank'}>
82
+ <Button size={'small'} type="primary">
83
+ {t('alert.cloud.action')} <Icon icon={ArrowRightIcon} />
84
+ </Button>
85
+ </Link>
86
+ </Center>
87
+ </Center>
88
+ );
89
+ });
90
+
91
+ export default CloudBanner;
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import HotKeys from '@/components/HotKeys';
8
8
  import { ALT_KEY, CLEAN_MESSAGE_KEY, META_KEY } from '@/const/hotkeys';
9
+ import { useIsMobile } from '@/hooks/useIsMobile';
9
10
  import { useChatStore } from '@/store/chat';
10
11
  import { useFileStore } from '@/store/file';
11
12
 
@@ -15,6 +16,7 @@ const Clear = memo(() => {
15
16
  const [clearImageList] = useFileStore((s) => [s.clearImageList]);
16
17
  const hotkeys = [META_KEY, ALT_KEY, CLEAN_MESSAGE_KEY].join('+');
17
18
  const [confirmOpened, updateConfirmOpened] = useState(false);
19
+ const mobile = useIsMobile();
18
20
 
19
21
  const resetConversation = useCallback(async () => {
20
22
  await clearMessage();
@@ -27,6 +29,8 @@ const Clear = memo(() => {
27
29
  <HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} inverseTheme keys={hotkeys} />
28
30
  );
29
31
 
32
+ const popconfirmPlacement = mobile ? 'top' : 'topRight';
33
+
30
34
  return (
31
35
  <Popconfirm
32
36
  arrow={false}
@@ -34,14 +38,19 @@ const Clear = memo(() => {
34
38
  onConfirm={resetConversation}
35
39
  onOpenChange={updateConfirmOpened}
36
40
  open={confirmOpened}
37
- placement={'topRight'}
38
- title={t('confirmClearCurrentMessages', { ns: 'chat' })}
41
+ placement={popconfirmPlacement}
42
+ title={
43
+ <div style={{ marginBottom: '8px', whiteSpace: 'pre-line', wordBreak: 'break-word' }}>
44
+ {t('confirmClearCurrentMessages', { ns: 'chat' })}
45
+ </div>
46
+ }
39
47
  >
40
- <ActionIcon
41
- icon={Eraser}
48
+ <ActionIcon
49
+ icon={Eraser}
42
50
  overlayStyle={{ maxWidth: 'none' }}
43
- placement={'bottom'}
44
- title={actionTitle} />
51
+ placement={'bottom'}
52
+ title={actionTitle}
53
+ />
45
54
  </Popconfirm>
46
55
  );
47
56
  });
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon, Icon, Tooltip } from '@lobehub/ui';
2
- import { Button } from 'antd';
2
+ import { Button, Popconfirm } from 'antd';
3
3
  import { LucideGalleryVerticalEnd, LucideMessageSquarePlus } from 'lucide-react';
4
- import { memo } from 'react';
4
+ import { memo, useState } from 'react';
5
5
  import { useHotkeys } from 'react-hotkeys-hook';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
@@ -19,9 +19,9 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
19
19
 
20
20
  const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
21
21
 
22
+ const [confirmOpened, setConfirmOpened] = useState(false);
23
+
22
24
  const icon = hasTopic ? LucideMessageSquarePlus : LucideGalleryVerticalEnd;
23
- const Render = mobile ? ActionIcon : Button;
24
- const iconRender: any = mobile ? icon : <Icon icon={icon} />;
25
25
  const desc = t(hasTopic ? 'topic.openNewTopic' : 'topic.saveCurrentMessages');
26
26
 
27
27
  const hotkeys = [ALT_KEY, SAVE_TOPIC_KEY].join('+');
@@ -31,11 +31,46 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
31
31
  preventDefault: true,
32
32
  });
33
33
 
34
- return (
35
- <Tooltip title={<HotKeys desc={desc} inverseTheme keys={hotkeys} />}>
36
- <Render aria-label={desc} icon={iconRender} loading={isValidating} onClick={() => mutate()} />
37
- </Tooltip>
38
- );
34
+ if (mobile) {
35
+ return (
36
+ <Popconfirm
37
+ arrow={false}
38
+ okButtonProps={{ danger: false, type: 'primary' }}
39
+ onConfirm={() => mutate()}
40
+ onOpenChange={setConfirmOpened}
41
+ open={confirmOpened}
42
+ placement={'top'}
43
+ title={
44
+ <div style={{ alignItems: 'center', display: 'flex', marginBottom: '8px' }}>
45
+ <div style={{ marginRight: '16px', whiteSpace: 'pre-line', wordBreak: 'break-word' }}>
46
+ {t(hasTopic ? 'topic.checkOpenNewTopic' : 'topic.checkSaveCurrentMessages')}
47
+ </div>
48
+ <HotKeys inverseTheme={false} keys={hotkeys} />
49
+ </div>
50
+ }
51
+ >
52
+ <Tooltip>
53
+ <ActionIcon
54
+ aria-label={desc}
55
+ icon={icon}
56
+ loading={isValidating}
57
+ onClick={() => setConfirmOpened(true)}
58
+ />
59
+ </Tooltip>
60
+ </Popconfirm>
61
+ );
62
+ } else {
63
+ return (
64
+ <Tooltip title={<HotKeys desc={desc} inverseTheme keys={hotkeys} />}>
65
+ <Button
66
+ aria-label={desc}
67
+ icon={<Icon icon={icon} />}
68
+ loading={isValidating}
69
+ onClick={() => mutate()}
70
+ />
71
+ </Tooltip>
72
+ );
73
+ }
39
74
  });
40
75
 
41
76
  export default SaveTopic;
@@ -4,6 +4,7 @@ import { ItemType } from 'antd/es/menu/interface';
4
4
  import {
5
5
  Book,
6
6
  CircleUserRound,
7
+ Cloudy,
7
8
  Download,
8
9
  Feather,
9
10
  HardDriveDownload,
@@ -21,7 +22,14 @@ import { Flexbox } from 'react-layout-kit';
21
22
  import urlJoin from 'url-join';
22
23
 
23
24
  import type { MenuProps } from '@/components/Menu';
24
- import { DISCORD, DOCUMENTS, EMAIL_SUPPORT, GITHUB_ISSUES, mailTo } from '@/const/url';
25
+ import {
26
+ DISCORD,
27
+ DOCUMENTS,
28
+ EMAIL_SUPPORT,
29
+ GITHUB_ISSUES,
30
+ OFFICIAL_URL,
31
+ mailTo,
32
+ } from '@/const/url';
25
33
  import { isServerMode } from '@/const/version';
26
34
  import DataImporter from '@/features/DataImporter';
27
35
  import { useOpenSettings } from '@/hooks/useInterceptingRoutes';
@@ -29,6 +37,7 @@ import { usePWAInstall } from '@/hooks/usePWAInstall';
29
37
  import { useQueryRoute } from '@/hooks/useQueryRoute';
30
38
  import { configService } from '@/services/config';
31
39
  import { SettingsTabs } from '@/store/global/initialState';
40
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
32
41
  import { useUserStore } from '@/store/user';
33
42
  import { authSelectors } from '@/store/user/selectors';
34
43
 
@@ -62,6 +71,7 @@ export const useMenu = () => {
62
71
  const hasNewVersion = useNewVersion();
63
72
  const openSettings = useOpenSettings();
64
73
  const { t } = useTranslation(['common', 'setting', 'auth']);
74
+ const { showCloudPromotion } = useServerConfigStore(featureFlagsSelectors);
65
75
  const [isLogin, isLoginWithAuth, isLoginWithClerk, openUserProfile] = useUserStore((s) => [
66
76
  authSelectors.isLogin(s),
67
77
  authSelectors.isLoginWithAuth(s),
@@ -163,6 +173,15 @@ export const useMenu = () => {
163
173
  ].filter(Boolean) as ItemType[]);
164
174
 
165
175
  const helps: MenuProps['items'] = [
176
+ showCloudPromotion && {
177
+ icon: <Icon icon={Cloudy} />,
178
+ key: 'cloud',
179
+ label: (
180
+ <Link href={OFFICIAL_URL} target={'_blank'}>
181
+ {t('userPanel.cloud', { name: 'LobeChat Cloud' })}
182
+ </Link>
183
+ ),
184
+ },
166
185
  {
167
186
  icon: <Icon icon={DiscordIcon} />,
168
187
  key: 'discord',
@@ -209,7 +228,7 @@ export const useMenu = () => {
209
228
  {
210
229
  type: 'divider',
211
230
  },
212
- ];
231
+ ].filter(Boolean) as ItemType[];
213
232
 
214
233
  const mainItems = [
215
234
  {
@@ -2,9 +2,9 @@
2
2
  import { FunctionDeclarationSchemaType, FunctionDeclarationsTool } from '@google/generative-ai';
3
3
  import { JSONSchema7 } from 'json-schema';
4
4
  import OpenAI from 'openai';
5
- import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
 
7
- import { ChatStreamCallbacks, OpenAIChatMessage } from '@/libs/agent-runtime';
7
+ import { OpenAIChatMessage } from '@/libs/agent-runtime';
8
8
 
9
9
  import * as debugStreamModule from '../utils/debugStream';
10
10
  import { LobeGoogleAI } from './index';
@@ -383,7 +383,7 @@ describe('LobeGoogleAI', () => {
383
383
  role: 'user',
384
384
  },
385
385
  ];
386
- const model = 'gemini-pro-vision';
386
+ const model = 'gemini-1.5-flash-latest';
387
387
 
388
388
  // 调用 buildGoogleMessages 方法
389
389
  const contents = instance['buildGoogleMessages'](messages, model);
@@ -398,36 +398,6 @@ describe('LobeGoogleAI', () => {
398
398
  });
399
399
  });
400
400
 
401
- describe('convertModel', () => {
402
- it('should use default text model when no images are included in messages', () => {
403
- const messages: OpenAIChatMessage[] = [
404
- { content: 'Hello', role: 'user' },
405
- { content: 'Hi', role: 'assistant' },
406
- ];
407
-
408
- // 调用 buildGoogleMessages 方法
409
- const model = instance['convertModel']('gemini-pro-vision', messages);
410
-
411
- expect(model).toEqual('gemini-pro'); // 假设 'gemini-pro' 是默认文本模型
412
- });
413
-
414
- it('should use specified model when images are included in messages', () => {
415
- const messages: OpenAIChatMessage[] = [
416
- {
417
- content: [
418
- { type: 'text', text: 'Hello' },
419
- { type: 'image_url', image_url: { url: 'data:image/png;base64,...' } },
420
- ],
421
- role: 'user',
422
- },
423
- ];
424
-
425
- const model = instance['convertModel']('gemini-pro-vision', messages);
426
-
427
- expect(model).toEqual('gemini-pro-vision');
428
- });
429
- });
430
-
431
401
  describe('buildGoogleTools', () => {
432
402
  it('should return undefined when tools is undefined or empty', () => {
433
403
  expect(instance['buildGoogleTools'](undefined)).toBeUndefined();