@lobehub/lobehub 2.0.0-next.236 → 2.0.0-next.238

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 (32) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/changelog/v1.json +17 -0
  3. package/locales/en-US/oauth.json +1 -0
  4. package/locales/zh-CN/oauth.json +1 -0
  5. package/locales/zh-CN/subscription.json +1 -1
  6. package/package.json +1 -1
  7. package/packages/const/src/klavis.ts +1 -7
  8. package/packages/types/src/user/onboarding.ts +3 -1
  9. package/src/app/[variants]/(auth)/oauth/callback/success/page.tsx +44 -2
  10. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Header/Nav.tsx +19 -1
  11. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Header/index.tsx +1 -2
  12. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Editing.tsx +18 -9
  13. package/src/app/[variants]/(main)/settings/profile/features/KlavisAuthorizationList/index.tsx +6 -24
  14. package/src/app/[variants]/(main)/settings/profile/index.tsx +17 -3
  15. package/src/app/[variants]/onboarding/features/FullNameStep.tsx +18 -5
  16. package/src/app/[variants]/onboarding/features/InterestsStep.tsx +19 -7
  17. package/src/app/[variants]/onboarding/features/ProSettingsStep.tsx +30 -8
  18. package/src/app/[variants]/onboarding/features/ResponseLanguageStep.tsx +18 -4
  19. package/src/app/[variants]/onboarding/features/TelemetryStep.tsx +14 -5
  20. package/src/app/[variants]/onboarding/index.tsx +2 -1
  21. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +42 -2
  22. package/src/server/services/search/impls/exa/index.ts +1 -1
  23. package/src/server/services/search/impls/search1api/index.ts +1 -1
  24. package/src/server/services/search/impls/tavily/index.ts +1 -1
  25. package/src/store/tool/slices/klavisStore/action.test.ts +167 -2
  26. package/src/store/tool/slices/klavisStore/action.ts +9 -8
  27. package/src/store/tool/slices/klavisStore/initialState.ts +3 -0
  28. package/src/store/user/slices/auth/action.ts +1 -0
  29. package/src/store/user/slices/onboarding/action.test.ts +342 -0
  30. package/src/store/user/slices/onboarding/action.ts +4 -9
  31. package/src/store/user/slices/onboarding/selectors.test.ts +222 -0
  32. package/src/store/user/slices/onboarding/selectors.ts +6 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,72 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.238](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.237...v2.0.0-next.238)
6
+
7
+ <sup>Released on **2026-01-08**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Change the klavis Linear to LobeHub oauth Linear.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: Topic renaming input focus issue in context menu.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### What's improved
23
+
24
+ - **misc**: Change the klavis Linear to LobeHub oauth Linear, closes [#11339](https://github.com/lobehub/lobe-chat/issues/11339) ([ec8ff26](https://github.com/lobehub/lobe-chat/commit/ec8ff26))
25
+
26
+ #### What's fixed
27
+
28
+ - **misc**: Topic renaming input focus issue in context menu, closes [#11323](https://github.com/lobehub/lobe-chat/issues/11323) ([dd065fc](https://github.com/lobehub/lobe-chat/commit/dd065fc))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ## [Version 2.0.0-next.237](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.236...v2.0.0-next.237)
39
+
40
+ <sup>Released on **2026-01-08**</sup>
41
+
42
+ #### ✨ Features
43
+
44
+ - **ui**: Move new topic button to navigation panel.
45
+
46
+ #### 🐛 Bug Fixes
47
+
48
+ - **onboarding**: Prevent step overflow and misc improvements.
49
+
50
+ <br/>
51
+
52
+ <details>
53
+ <summary><kbd>Improvements and Fixes</kbd></summary>
54
+
55
+ #### What's improved
56
+
57
+ - **ui**: Move new topic button to navigation panel, closes [#11325](https://github.com/lobehub/lobe-chat/issues/11325) ([3d6b399](https://github.com/lobehub/lobe-chat/commit/3d6b399))
58
+
59
+ #### What's fixed
60
+
61
+ - **onboarding**: Prevent step overflow and misc improvements, closes [#11322](https://github.com/lobehub/lobe-chat/issues/11322) ([8586fd4](https://github.com/lobehub/lobe-chat/commit/8586fd4))
62
+
63
+ </details>
64
+
65
+ <div align="right">
66
+
67
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
68
+
69
+ </div>
70
+
5
71
  ## [Version 2.0.0-next.236](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.235...v2.0.0-next.236)
6
72
 
7
73
  <sup>Released on **2026-01-08**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,21 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Change the klavis Linear to LobeHub oauth Linear."
6
+ ],
7
+ "fixes": [
8
+ "Topic renaming input focus issue in context menu."
9
+ ]
10
+ },
11
+ "date": "2026-01-08",
12
+ "version": "2.0.0-next.238"
13
+ },
14
+ {
15
+ "children": {},
16
+ "date": "2026-01-08",
17
+ "version": "2.0.0-next.237"
18
+ },
2
19
  {
3
20
  "children": {},
4
21
  "date": "2026-01-08",
@@ -32,5 +32,6 @@
32
32
  "login.title": "Login to {{clientName}}",
33
33
  "login.userWelcome": "Welcome back, ",
34
34
  "success.subTitle": "You have successfully authorized the application to access your account. You may now close this page.",
35
+ "success.subTitleWithCountdown": "Authorization successful. Auto-closing in {{countdown}}s...",
35
36
  "success.title": "Authorization Successful"
36
37
  }
@@ -32,5 +32,6 @@
32
32
  "login.title": "登录 {{clientName}}",
33
33
  "login.userWelcome": "欢迎回来,",
34
34
  "success.subTitle": "应用已获得授权。你可以关闭此页面了",
35
+ "success.subTitleWithCountdown": "应用已获得授权。{{countdown}}秒后自动关闭…",
35
36
  "success.title": "授权完成"
36
37
  }
@@ -330,7 +330,7 @@
330
330
  "switchToYearly.desc": "切换后,支付差价后立即生效年付计费,起始日期继承原计划。",
331
331
  "switchToYearly.title": "切换为年付",
332
332
  "tab.billing": "账单管理",
333
- "tab.funds": "点数管理",
333
+ "tab.funds": "积分管理",
334
334
  "tab.plans": "订阅计划",
335
335
  "tab.referral": "推荐奖励",
336
336
  "tab.spend": "点数明细",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.236",
3
+ "version": "2.0.0-next.238",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent 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",
@@ -1,4 +1,4 @@
1
- import { IconType, SiCaldotcom, SiGithub, SiLinear } from '@icons-pack/react-simple-icons';
1
+ import { IconType, SiCaldotcom, SiGithub } from '@icons-pack/react-simple-icons';
2
2
  import { Klavis } from 'klavis';
3
3
 
4
4
  export interface KlavisServerType {
@@ -40,12 +40,6 @@ export const KLAVIS_SERVER_TYPES: KlavisServerType[] = [
40
40
  label: 'Airtable',
41
41
  serverName: Klavis.McpServerName.Airtable,
42
42
  },
43
- {
44
- icon: SiLinear,
45
- identifier: 'linear',
46
- label: 'Linear',
47
- serverName: Klavis.McpServerName.Linear,
48
- },
49
43
  {
50
44
  icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googlesheets.svg',
51
45
  identifier: 'google-sheets',
@@ -9,8 +9,10 @@ export interface UserOnboarding {
9
9
  version: number;
10
10
  }
11
11
 
12
+ export const MAX_ONBOARDING_STEPS = 5;
13
+
12
14
  export const UserOnboardingSchema = z.object({
13
- currentStep: z.number().optional(),
15
+ currentStep: z.number().min(1).max(MAX_ONBOARDING_STEPS).optional(),
14
16
  finishedAt: z.string().optional(),
15
17
  version: z.number(),
16
18
  });
@@ -2,11 +2,48 @@
2
2
 
3
3
  import { FluentEmoji, Text } from '@lobehub/ui';
4
4
  import { Result } from 'antd';
5
- import React, { memo } from 'react';
5
+ import { useSearchParams } from 'next/navigation';
6
+ import React, { memo, useEffect, useState } from 'react';
6
7
  import { useTranslation } from 'react-i18next';
7
8
 
8
9
  const SuccessPage = memo(() => {
9
10
  const { t } = useTranslation('oauth');
11
+ const searchParams = useSearchParams();
12
+ const [countdown, setCountdown] = useState(3);
13
+
14
+ useEffect(() => {
15
+ // Check if this is a LobeHub Skill OAuth callback
16
+ const provider = searchParams.get('provider');
17
+
18
+ if (provider && window.opener) {
19
+ // Notify parent window about successful OAuth
20
+ window.opener.postMessage(
21
+ {
22
+ provider,
23
+ type: 'LOBEHUB_SKILL_AUTH_SUCCESS',
24
+ },
25
+ window.location.origin,
26
+ );
27
+
28
+ // Start countdown and close window after 3 seconds
29
+ let timeLeft = 3;
30
+ setCountdown(timeLeft);
31
+
32
+ const countdownTimer = setInterval(() => {
33
+ timeLeft -= 1;
34
+ setCountdown(timeLeft);
35
+
36
+ if (timeLeft <= 0) {
37
+ clearInterval(countdownTimer);
38
+ window.close();
39
+ }
40
+ }, 1000);
41
+
42
+ return () => clearInterval(countdownTimer);
43
+ }
44
+ }, [searchParams]);
45
+
46
+ const provider = searchParams.get('provider');
10
47
 
11
48
  return (
12
49
  <Result
@@ -14,7 +51,12 @@ const SuccessPage = memo(() => {
14
51
  status="success"
15
52
  subTitle={
16
53
  <Text fontSize={16} type="secondary">
17
- {t('success.subTitle')}
54
+ {provider
55
+ ? t('success.subTitleWithCountdown', {
56
+ countdown,
57
+ defaultValue: `You may close this page. Auto-closing in ${countdown}s...`,
58
+ })
59
+ : t('success.subTitle')}
18
60
  </Text>
19
61
  }
20
62
  title={
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { Flexbox } from '@lobehub/ui';
4
4
  import { BotPromptIcon } from '@lobehub/ui/icons';
5
- import { SearchIcon } from 'lucide-react';
5
+ import { MessageSquarePlusIcon, SearchIcon } from 'lucide-react';
6
6
  import { usePathname } from 'next/navigation';
7
7
  import { memo } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
@@ -11,6 +11,7 @@ import urlJoin from 'url-join';
11
11
 
12
12
  import NavItem from '@/features/NavPanel/components/NavItem';
13
13
  import { useQueryRoute } from '@/hooks/useQueryRoute';
14
+ import { useActionSWR } from '@/libs/swr';
14
15
  import { useAgentStore } from '@/store/agent';
15
16
  import { builtinAgentSelectors } from '@/store/agent/selectors';
16
17
  import { useChatStore } from '@/store/chat';
@@ -19,6 +20,7 @@ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfi
19
20
 
20
21
  const Nav = memo(() => {
21
22
  const { t } = useTranslation('chat');
23
+ const { t: tTopic } = useTranslation('topic');
22
24
  const isInbox = useAgentStore(builtinAgentSelectors.isInboxAgent);
23
25
  const params = useParams();
24
26
  const agentId = params.aid;
@@ -29,9 +31,25 @@ const Nav = memo(() => {
29
31
  const toggleCommandMenu = useGlobalStore((s) => s.toggleCommandMenu);
30
32
  const hideProfile = isInbox || !isAgentEditable;
31
33
  const switchTopic = useChatStore((s) => s.switchTopic);
34
+ const [openNewTopicOrSaveTopic] = useChatStore((s) => [s.openNewTopicOrSaveTopic]);
35
+
36
+ const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
37
+ const handleNewTopic = () => {
38
+ // If in agent sub-route, navigate back to agent chat first
39
+ if (isProfileActive && agentId) {
40
+ router.push(urlJoin('/agent', agentId));
41
+ }
42
+ mutate();
43
+ };
32
44
 
33
45
  return (
34
46
  <Flexbox gap={1} paddingInline={4}>
47
+ <NavItem
48
+ icon={MessageSquarePlusIcon}
49
+ loading={isValidating}
50
+ onClick={handleNewTopic}
51
+ title={tTopic('actions.addNewTopic')}
52
+ />
35
53
  {!hideProfile && (
36
54
  <NavItem
37
55
  active={isProfileActive}
@@ -4,14 +4,13 @@ import { type PropsWithChildren, memo } from 'react';
4
4
 
5
5
  import SideBarHeaderLayout from '@/features/NavPanel/SideBarHeaderLayout';
6
6
 
7
- import AddTopicButon from './AddTopicButon';
8
7
  import Agent from './Agent';
9
8
  import Nav from './Nav';
10
9
 
11
10
  const HeaderInfo = memo<PropsWithChildren>(() => {
12
11
  return (
13
12
  <>
14
- <SideBarHeaderLayout left={<Agent />} right={<AddTopicButon />} />
13
+ <SideBarHeaderLayout left={<Agent />} />
15
14
  <Nav />
16
15
  </>
17
16
  );
@@ -1,5 +1,6 @@
1
- import { Input, Popover } from '@lobehub/ui';
2
- import { memo, useCallback, useState } from 'react';
1
+ import { Input, type InputProps, Popover } from '@lobehub/ui';
2
+ import type { InputRef } from 'antd';
3
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
3
4
 
4
5
  import { useChatStore } from '@/store/chat';
5
6
 
@@ -9,6 +10,18 @@ interface EditingProps {
9
10
  toggleEditing: (visible?: boolean) => void;
10
11
  }
11
12
 
13
+ function FocusableInput({ ...props }: InputProps) {
14
+ const ref = useRef<InputRef>(null);
15
+ useEffect(() => {
16
+ queueMicrotask(() => {
17
+ if (ref.current) {
18
+ ref.current.input?.focus();
19
+ }
20
+ });
21
+ }, []);
22
+ return <Input {...props} ref={ref} />;
23
+ }
24
+
12
25
  const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
13
26
  const [newTitle, setNewTitle] = useState(title);
14
27
  const [editing, updateTopicTitle] = useChatStore((s) => [
@@ -41,19 +54,14 @@ const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
41
54
  );
42
55
  }
43
56
  }
44
- toggleEditing(false);
45
57
  }, [newTitle, title, id, updateTopicTitle, toggleEditing]);
46
58
 
47
59
  return (
48
60
  <Popover
49
61
  content={
50
- <Input
51
- autoFocus
62
+ <FocusableInput
52
63
  defaultValue={title}
53
- onBlur={() => {
54
- handleUpdate();
55
- toggleEditing(false);
56
- }}
64
+ onBlur={handleUpdate}
57
65
  onChange={(e) => setNewTitle(e.target.value)}
58
66
  onClick={(e) => e.stopPropagation()}
59
67
  onPressEnter={() => {
@@ -64,6 +72,7 @@ const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
64
72
  }
65
73
  onOpenChange={(open) => {
66
74
  if (!open) handleUpdate();
75
+
67
76
  toggleEditing(open);
68
77
  }}
69
78
  open={editing}
@@ -4,11 +4,8 @@ import { memo, useCallback, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { modal } from '@/components/AntdStaticMethods';
7
- import { useServerConfigStore } from '@/store/serverConfig';
8
- import { serverConfigSelectors } from '@/store/serverConfig/selectors';
9
7
  import { useToolStore } from '@/store/tool';
10
- import { type KlavisServer, KlavisServerStatus } from '@/store/tool/slices/klavisStore';
11
- import { type ToolStore } from '@/store/tool/store';
8
+ import { type KlavisServer } from '@/store/tool/slices/klavisStore';
12
9
 
13
10
  interface KlavisAuthItemProps {
14
11
  server: KlavisServer;
@@ -54,11 +51,6 @@ const KlavisAuthItem = memo<KlavisAuthItemProps>(({ server }) => {
54
51
  return <IconComponent size={14} />;
55
52
  };
56
53
 
57
- // 只显示已连接的服务器
58
- if (server.status !== KlavisServerStatus.CONNECTED) {
59
- return null;
60
- }
61
-
62
54
  return (
63
55
  <Tag closable onClose={handleRevoke}>
64
56
  <Flexbox align="center" gap={4} horizontal style={{ opacity: isRevoking ? 0.5 : 1 }}>
@@ -69,24 +61,14 @@ const KlavisAuthItem = memo<KlavisAuthItemProps>(({ server }) => {
69
61
  );
70
62
  });
71
63
 
72
- export const KlavisAuthorizationList = memo(() => {
73
- const enableKlavis = useServerConfigStore(serverConfigSelectors.enableKlavis);
74
- const useFetchUserKlavisServers = useToolStore((s: ToolStore) => s.useFetchUserKlavisServers);
75
- const servers = useToolStore((s: ToolStore) => s.servers);
76
-
77
- // 获取已授权的服务器列表
78
- useFetchUserKlavisServers(enableKlavis);
79
-
80
- // 只显示已连接的服务器
81
- const connectedServers = servers.filter((s) => s.status === KlavisServerStatus.CONNECTED);
82
-
83
- if (!enableKlavis || connectedServers.length === 0) {
84
- return null;
85
- }
64
+ interface KlavisAuthorizationListProps {
65
+ servers: KlavisServer[];
66
+ }
86
67
 
68
+ export const KlavisAuthorizationList = memo<KlavisAuthorizationListProps>(({ servers }) => {
87
69
  return (
88
70
  <Flexbox gap={8} horizontal wrap="wrap">
89
- {connectedServers.map((server) => (
71
+ {servers.map((server) => (
90
72
  <KlavisAuthItem key={server.identifier} server={server} />
91
73
  ))}
92
74
  </Flexbox>
@@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next';
9
9
  import SettingHeader from '@/app/[variants]/(main)/settings/features/SettingHeader';
10
10
  import { useServerConfigStore } from '@/store/serverConfig';
11
11
  import { serverConfigSelectors } from '@/store/serverConfig/selectors';
12
+ import { useToolStore } from '@/store/tool';
13
+ import { KlavisServerStatus } from '@/store/tool/slices/klavisStore';
12
14
  import { useUserStore } from '@/store/user';
13
15
  import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
14
16
 
@@ -37,9 +39,21 @@ const ProfileSetting = ({ mobile }: ProfileSettingProps) => {
37
39
  const isLoadedAuthProviders = useUserStore(authSelectors.isLoadedAuthProviders);
38
40
  const fetchAuthProviders = useUserStore((s) => s.fetchAuthProviders);
39
41
  const enableKlavis = useServerConfigStore(serverConfigSelectors.enableKlavis);
42
+ const [servers, isServersInit, useFetchUserKlavisServers] = useToolStore((s) => [
43
+ s.servers,
44
+ s.isServersInit,
45
+ s.useFetchUserKlavisServers,
46
+ ]);
47
+ const connectedServers = servers.filter((s) => s.status === KlavisServerStatus.CONNECTED);
48
+
49
+ // Fetch Klavis servers
50
+ useFetchUserKlavisServers(enableKlavis);
40
51
 
41
52
  const isLoginWithAuth = isLoginWithNextAuth || isLoginWithBetterAuth;
42
- const isLoading = !isUserLoaded || (isLoginWithAuth && !isLoadedAuthProviders);
53
+ const isLoading =
54
+ !isUserLoaded ||
55
+ (isLoginWithAuth && !isLoadedAuthProviders) ||
56
+ (enableKlavis && !isServersInit);
43
57
 
44
58
  useEffect(() => {
45
59
  if (isLoginWithAuth) {
@@ -110,11 +124,11 @@ const ProfileSetting = ({ mobile }: ProfileSettingProps) => {
110
124
  )}
111
125
 
112
126
  {/* Klavis Authorizations Row */}
113
- {enableKlavis && (
127
+ {enableKlavis && connectedServers.length > 0 && (
114
128
  <>
115
129
  <Divider style={{ margin: 0 }} />
116
130
  <ProfileRow label={t('profile.authorizations.title')} mobile={mobile}>
117
- <KlavisAuthorizationList />
131
+ <KlavisAuthorizationList servers={connectedServers} />
118
132
  </ProfileRow>
119
133
  </>
120
134
  )}
@@ -4,7 +4,7 @@ import { SendButton } from '@lobehub/editor/react';
4
4
  import { Button, Flexbox, Icon, Input } from '@lobehub/ui';
5
5
  import { cssVar } from 'antd-style';
6
6
  import { SignatureIcon, Undo2Icon } from 'lucide-react';
7
- import { memo, useState } from 'react';
7
+ import { memo, useCallback, useRef, useState } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
10
  import { useUserStore } from '@/store/user';
@@ -23,13 +23,25 @@ const FullNameStep = memo<FullNameStepProps>(({ onBack, onNext }) => {
23
23
  const updateFullName = useUserStore((s) => s.updateFullName);
24
24
 
25
25
  const [value, setValue] = useState(existingFullName || '');
26
+ const [isNavigating, setIsNavigating] = useState(false);
27
+ const isNavigatingRef = useRef(false);
26
28
 
27
- const handleNext = () => {
29
+ const handleNext = useCallback(() => {
30
+ if (isNavigatingRef.current) return;
31
+ isNavigatingRef.current = true;
32
+ setIsNavigating(true);
28
33
  if (value.trim()) {
29
34
  updateFullName(value.trim());
30
35
  }
31
36
  onNext();
32
- };
37
+ }, [value, updateFullName, onNext]);
38
+
39
+ const handleBack = useCallback(() => {
40
+ if (isNavigatingRef.current) return;
41
+ isNavigatingRef.current = true;
42
+ setIsNavigating(true);
43
+ onBack();
44
+ }, [onBack]);
33
45
 
34
46
  return (
35
47
  <Flexbox gap={16}>
@@ -59,7 +71,7 @@ const FullNameStep = memo<FullNameStepProps>(({ onBack, onNext }) => {
59
71
  }}
60
72
  suffix={
61
73
  <SendButton
62
- disabled={!value?.trim()}
74
+ disabled={!value?.trim() || isNavigating}
63
75
  onClick={handleNext}
64
76
  style={{
65
77
  zoom: 1.5,
@@ -73,8 +85,9 @@ const FullNameStep = memo<FullNameStepProps>(({ onBack, onNext }) => {
73
85
  </Flexbox>
74
86
  <Flexbox horizontal justify={'flex-start'} style={{ marginTop: 32 }}>
75
87
  <Button
88
+ disabled={isNavigating}
76
89
  icon={Undo2Icon}
77
- onClick={onBack}
90
+ onClick={handleBack}
78
91
  style={{
79
92
  color: cssVar.colorTextDescription,
80
93
  }}
@@ -3,7 +3,7 @@
3
3
  import { Block, Button, Flexbox, Icon, Input, Text } from '@lobehub/ui';
4
4
  import { cssVar } from 'antd-style';
5
5
  import { BriefcaseIcon, Undo2Icon } from 'lucide-react';
6
- import { memo, useCallback, useMemo, useState } from 'react';
6
+ import { memo, useCallback, useMemo, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
9
  import { useUserStore } from '@/store/user';
@@ -25,6 +25,8 @@ const InterestsStep = memo<InterestsStepProps>(({ onBack, onNext }) => {
25
25
  const [selectedInterests, setSelectedInterests] = useState<string[]>(existingInterests);
26
26
  const [customInput, setCustomInput] = useState('');
27
27
  const [showCustomInput, setShowCustomInput] = useState(false);
28
+ const [isNavigating, setIsNavigating] = useState(false);
29
+ const isNavigatingRef = useRef(false);
28
30
 
29
31
  const areas = useMemo(
30
32
  () =>
@@ -49,7 +51,11 @@ const InterestsStep = memo<InterestsStepProps>(({ onBack, onNext }) => {
49
51
  }
50
52
  }, [customInput, selectedInterests]);
51
53
 
52
- const handleNext = useCallback(async () => {
54
+ const handleNext = useCallback(() => {
55
+ if (isNavigatingRef.current) return;
56
+ isNavigatingRef.current = true;
57
+ setIsNavigating(true);
58
+
53
59
  // Include custom input value if "other" is active and has content
54
60
  const finalInterests = [...selectedInterests];
55
61
  const trimmedCustom = customInput.trim();
@@ -60,12 +66,17 @@ const InterestsStep = memo<InterestsStepProps>(({ onBack, onNext }) => {
60
66
  // Deduplicate
61
67
  const uniqueInterests = [...new Set(finalInterests)];
62
68
 
63
- if (uniqueInterests.length > 0) {
64
- await updateInterests(uniqueInterests);
65
- }
69
+ updateInterests(uniqueInterests);
66
70
  onNext();
67
71
  }, [selectedInterests, customInput, showCustomInput, updateInterests, onNext]);
68
72
 
73
+ const handleBack = useCallback(() => {
74
+ if (isNavigatingRef.current) return;
75
+ isNavigatingRef.current = true;
76
+ setIsNavigating(true);
77
+ onBack();
78
+ }, [onBack]);
79
+
69
80
  return (
70
81
  <Flexbox gap={16}>
71
82
  <LobeMessage
@@ -138,14 +149,15 @@ const InterestsStep = memo<InterestsStepProps>(({ onBack, onNext }) => {
138
149
  )}
139
150
  <Flexbox horizontal justify={'space-between'} style={{ marginTop: 32 }}>
140
151
  <Button
152
+ disabled={isNavigating}
141
153
  icon={Undo2Icon}
142
- onClick={onBack}
154
+ onClick={handleBack}
143
155
  style={{ color: cssVar.colorTextDescription }}
144
156
  type={'text'}
145
157
  >
146
158
  {t('back')}
147
159
  </Button>
148
- <Button onClick={handleNext} type={'primary'}>
160
+ <Button disabled={isNavigating} onClick={handleNext} type={'primary'}>
149
161
  {t('next')}
150
162
  </Button>
151
163
  </Flexbox>
@@ -3,7 +3,7 @@
3
3
  import { Button, Flexbox, Text } from '@lobehub/ui';
4
4
  import { cssVar } from 'antd-style';
5
5
  import { Undo2Icon } from 'lucide-react';
6
- import { memo } from 'react';
6
+ import { memo, useCallback, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { useNavigate } from 'react-router-dom';
9
9
 
@@ -34,14 +34,30 @@ const ProSettingsStep = memo<ProSettingsStepProps>(({ onBack }) => {
34
34
  (s) => settingsSelectors.currentSettings(s).defaultAgent?.config,
35
35
  );
36
36
 
37
- const handleFinish = () => {
37
+ const [isNavigating, setIsNavigating] = useState(false);
38
+ const isNavigatingRef = useRef(false);
39
+
40
+ const handleFinish = useCallback(() => {
41
+ if (isNavigatingRef.current) return;
42
+ isNavigatingRef.current = true;
43
+ setIsNavigating(true);
38
44
  finishOnboarding();
39
45
  navigate('/');
40
- };
46
+ }, [finishOnboarding, navigate]);
47
+
48
+ const handleBack = useCallback(() => {
49
+ if (isNavigatingRef.current) return;
50
+ isNavigatingRef.current = true;
51
+ setIsNavigating(true);
52
+ onBack();
53
+ }, [onBack]);
41
54
 
42
- const handleModelChange = ({ model, provider }: { model: string; provider: string }) => {
43
- updateDefaultModel(model, provider);
44
- };
55
+ const handleModelChange = useCallback(
56
+ ({ model, provider }: { model: string; provider: string }) => {
57
+ updateDefaultModel(model, provider);
58
+ },
59
+ [updateDefaultModel],
60
+ );
45
61
 
46
62
  return (
47
63
  <Flexbox gap={16}>
@@ -70,8 +86,9 @@ const ProSettingsStep = memo<ProSettingsStepProps>(({ onBack }) => {
70
86
 
71
87
  <Flexbox align={'center'} horizontal justify={'space-between'} style={{ marginTop: 16 }}>
72
88
  <Button
89
+ disabled={isNavigating}
73
90
  icon={Undo2Icon}
74
- onClick={onBack}
91
+ onClick={handleBack}
75
92
  style={{
76
93
  color: cssVar.colorTextDescription,
77
94
  }}
@@ -79,7 +96,12 @@ const ProSettingsStep = memo<ProSettingsStepProps>(({ onBack }) => {
79
96
  >
80
97
  {t('back')}
81
98
  </Button>
82
- <Button onClick={handleFinish} style={{ minWidth: 120 }} type="primary">
99
+ <Button
100
+ disabled={isNavigating}
101
+ onClick={handleFinish}
102
+ style={{ minWidth: 120 }}
103
+ type="primary"
104
+ >
83
105
  {t('finish')}
84
106
  </Button>
85
107
  </Flexbox>