♻️ refactor: gate agent onboarding with dedicated business flag (#13472)

* ♻️ refactor: gate agent onboarding with dedicated business flag

Made-with: Cursor

* 🗑️ chore(migrations): remove agent onboarding column from users table

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): enable agent onboarding based on environment and add redirect to classic onboarding

- Updated AGENT_ONBOARDING_ENABLED to be true in development mode.
- Introduced RedirectToClassicOnboarding component to handle navigation to classic onboarding.
- Simplified ClassicOnboardingPage by removing the mode switch button for non-development environments.
- Adjusted OnBoardingContainer to conditionally render the skip onboarding button based on the current route.

This change enhances the onboarding experience by ensuring that the agent onboarding feature is only available in development, while also improving navigation for users.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(test): inline emoji-mart and @lobehub/* deps in Vitest to fix ESM JSON import error

Widen server.deps.inline to include `emoji-mart` and all `@lobehub/*`
packages so their transitive `@emoji-mart/data` import (a .json main
entry) goes through Vite's transform pipeline instead of Node's native
ESM loader, which requires `with { type: "json" }`.

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2026-04-01 19:38:14 +08:00 committed by GitHub
parent 60a59e89f6
commit 6ecae1bbd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 48 additions and 32 deletions

View file

@ -3,4 +3,8 @@ export * from './branding';
export * from './llm'; export * from './llm';
export * from './url'; export * from './url';
const isDev = process.env.NODE_ENV === 'development';
export const ENABLE_BUSINESS_FEATURES = false; export const ENABLE_BUSINESS_FEATURES = false;
export const AGENT_ONBOARDING_ENABLED = isDev;

View file

@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "agent_onboarding" jsonb;

View file

@ -2,11 +2,12 @@
import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents'; import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents';
import { SESSION_CHAT_URL } from '@lobechat/const'; import { SESSION_CHAT_URL } from '@lobechat/const';
import { Button, ErrorBoundary, Flexbox, Text } from '@lobehub/ui'; import { Button, ErrorBoundary, Flexbox } from '@lobehub/ui';
import { Drawer } from 'antd'; import { Drawer } from 'antd';
import { History } from 'lucide-react'; import { History } from 'lucide-react';
import { memo, useMemo, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Loading from '@/components/Loading/BrandTextLoading'; import Loading from '@/components/Loading/BrandTextLoading';
import ModeSwitch from '@/features/Onboarding/components/ModeSwitch'; import ModeSwitch from '@/features/Onboarding/components/ModeSwitch';
@ -25,6 +26,19 @@ import AgentOnboardingDebugExportButton from './DebugExportButton';
import HistoryPanel from './HistoryPanel'; import HistoryPanel from './HistoryPanel';
import OnboardingConversationProvider from './OnboardingConversationProvider'; import OnboardingConversationProvider from './OnboardingConversationProvider';
const CLASSIC_ONBOARDING_PATH = '/onboarding/classic';
const RedirectToClassicOnboarding = memo(() => {
const navigate = useNavigate();
useEffect(() => {
navigate(CLASSIC_ONBOARDING_PATH, { replace: true });
}, [navigate]);
return <Loading debugId="AgentOnboardingRedirectClassic" />;
});
RedirectToClassicOnboarding.displayName = 'RedirectToClassicOnboarding';
const AgentOnboardingPage = memo(() => { const AgentOnboardingPage = memo(() => {
const { t } = useTranslation('onboarding'); const { t } = useTranslation('onboarding');
const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent); const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent);
@ -89,13 +103,7 @@ const AgentOnboardingPage = memo(() => {
if (error) { if (error) {
return ( return (
<OnboardingContainer> <OnboardingContainer>
<Flexbox gap={16} style={{ maxWidth: 720, width: '100%' }}> <RedirectToClassicOnboarding />
<ModeSwitch />
<Flexbox gap={8}>
<Text weight={'bold'}>Failed to initialize onboarding.</Text>
<Button onClick={() => mutate()}>Retry</Button>
</Flexbox>
</Flexbox>
</OnboardingContainer> </OnboardingContainer>
); );
} }

View file

@ -1,9 +1,8 @@
'use client'; 'use client';
import { MAX_ONBOARDING_STEPS } from '@lobechat/types'; import { MAX_ONBOARDING_STEPS } from '@lobechat/types';
import { Button, Flexbox } from '@lobehub/ui'; import { Flexbox } from '@lobehub/ui';
import { memo, useState } from 'react'; import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Loading from '@/components/Loading/BrandTextLoading'; import Loading from '@/components/Loading/BrandTextLoading';
import ModeSwitch from '@/features/Onboarding/components/ModeSwitch'; import ModeSwitch from '@/features/Onboarding/components/ModeSwitch';
@ -15,10 +14,8 @@ import ResponseLanguageStep from '@/routes/onboarding/features/ResponseLanguageS
import TelemetryStep from '@/routes/onboarding/features/TelemetryStep'; import TelemetryStep from '@/routes/onboarding/features/TelemetryStep';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import { onboardingSelectors } from '@/store/user/selectors'; import { onboardingSelectors } from '@/store/user/selectors';
import { isDev } from '@/utils/env';
const ClassicOnboardingPage = memo(() => { const ClassicOnboardingPage = memo(() => {
const { t } = useTranslation('onboarding');
const [isUserStateInit, currentStep, goToNextStep, goToPreviousStep, resetOnboarding] = const [isUserStateInit, currentStep, goToNextStep, goToPreviousStep, resetOnboarding] =
useUserStore((s) => [ useUserStore((s) => [
s.isUserStateInit, s.isUserStateInit,
@ -69,15 +66,7 @@ const ClassicOnboardingPage = memo(() => {
return ( return (
<OnboardingContainer> <OnboardingContainer>
<Flexbox gap={24} style={{ maxWidth: 480, width: '100%' }}> <Flexbox gap={24} style={{ maxWidth: 480, width: '100%' }}>
<ModeSwitch <ModeSwitch />
actions={
isDev ? (
<Button danger loading={isResetting} size={'small'} onClick={handleReset}>
{t('agent.modeSwitch.reset')}
</Button>
) : undefined
}
/>
{renderStep()} {renderStep()}
</Flexbox> </Flexbox>
</OnboardingContainer> </OnboardingContainer>

View file

@ -29,6 +29,19 @@ describe('OnBoardingContainer', () => {
expect(screen.getByText('Lang Button')).toBeInTheDocument(); expect(screen.getByText('Lang Button')).toBeInTheDocument();
expect(screen.getByText('Theme Button')).toBeInTheDocument(); expect(screen.getByText('Theme Button')).toBeInTheDocument();
expect(screen.getByText('Onboarding Content')).toBeInTheDocument(); expect(screen.getByText('Onboarding Content')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'agent.skipOnboarding' })).not.toBeInTheDocument();
expect(screen.queryByText('© 2026 LobeHub. All rights reserved.')).not.toBeInTheDocument(); expect(screen.queryByText('© 2026 LobeHub. All rights reserved.')).not.toBeInTheDocument();
}); });
it('shows skip onboarding on agent onboarding route', () => {
render(
<MemoryRouter initialEntries={['/onboarding/agent']}>
<OnBoardingContainer>
<div>Onboarding Content</div>
</OnBoardingContainer>
</MemoryRouter>,
);
expect(screen.getByRole('button', { name: 'agent.skipOnboarding' })).toBeInTheDocument();
});
}); });

View file

@ -5,7 +5,7 @@ import { Divider } from 'antd';
import { cx, useTheme } from 'antd-style'; import { cx, useTheme } from 'antd-style';
import { type FC, type PropsWithChildren, useCallback } from 'react'; import { type FC, type PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { ProductLogo } from '@/components/Branding'; import { ProductLogo } from '@/components/Branding';
import LangButton from '@/features/User/UserPanel/LangButton'; import LangButton from '@/features/User/UserPanel/LangButton';
@ -19,8 +19,10 @@ const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
const isDarkMode = useIsDark(); const isDarkMode = useIsDark();
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation('onboarding'); const { t } = useTranslation('onboarding');
const { pathname } = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const finishOnboarding = useUserStore((s) => s.finishOnboarding); const finishOnboarding = useUserStore((s) => s.finishOnboarding);
const isAgentOnboarding = pathname.startsWith('/onboarding/agent');
const handleSkip = useCallback(() => { const handleSkip = useCallback(() => {
finishOnboarding(); finishOnboarding();
@ -49,9 +51,11 @@ const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
<Divider className={styles.divider} orientation={'vertical'} /> <Divider className={styles.divider} orientation={'vertical'} />
<ThemeButton placement={'bottomRight'} size={18} /> <ThemeButton placement={'bottomRight'} size={18} />
</Flexbox> </Flexbox>
<Button size={'small'} type={'text'} onClick={handleSkip}> {isAgentOnboarding ? (
{t('agent.skipOnboarding')} <Button size={'small'} type={'text'} onClick={handleSkip}>
</Button> {t('agent.skipOnboarding')}
</Button>
) : null}
</Flexbox> </Flexbox>
</Flexbox> </Flexbox>
<Center height={'100%'} width={'100%'}> <Center height={'100%'} width={'100%'}>

View file

@ -1,5 +1,4 @@
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const'; import { AGENT_ONBOARDING_ENABLED } from '@lobechat/business-const';
import { isDev } from '@lobechat/utils';
import { import {
ChartNetworkIcon, ChartNetworkIcon,
CodeXmlIcon, CodeXmlIcon,
@ -13,11 +12,9 @@ import {
/** Default target when the user opens `/onboarding`. Flip to `'agent'` when agent onboarding is ready to ship as the primary flow. */ /** Default target when the user opens `/onboarding`. Flip to `'agent'` when agent onboarding is ready to ship as the primary flow. */
export type DefaultOnboardingEntryVariant = 'agent' | 'classic'; export type DefaultOnboardingEntryVariant = 'agent' | 'classic';
export { AGENT_ONBOARDING_ENABLED };
export const DEFAULT_ONBOARDING_ENTRY_VARIANT: DefaultOnboardingEntryVariant = 'classic'; export const DEFAULT_ONBOARDING_ENTRY_VARIANT: DefaultOnboardingEntryVariant = 'classic';
export const AGENT_ONBOARDING_ENABLED = ENABLE_BUSINESS_FEATURES || isDev;
const resolveDefaultOnboardingPath = (variant: DefaultOnboardingEntryVariant) => const resolveDefaultOnboardingPath = (variant: DefaultOnboardingEntryVariant) =>
variant === 'agent' && AGENT_ONBOARDING_ENABLED ? '/onboarding/agent' : '/onboarding/classic'; variant === 'agent' && AGENT_ONBOARDING_ENABLED ? '/onboarding/agent' : '/onboarding/classic';

View file

@ -113,6 +113,7 @@ export default defineConfig({
inline: [ inline: [
'vitest-canvas-mock', 'vitest-canvas-mock',
/@emoji-mart/, /@emoji-mart/,
'emoji-mart',
'@lobehub/ui', '@lobehub/ui',
'@lobehub/fluent-emoji', '@lobehub/fluent-emoji',
'@pierre/diffs', '@pierre/diffs',
@ -120,6 +121,7 @@ export default defineConfig({
'lru_map', 'lru_map',
'lexical', 'lexical',
/@lexical\//, /@lexical\//,
/@lobehub\//,
], ],
}, },
}, },