feat(onboarding): adapt agent onboarding UI for mobile (#15019)

- Welcome.mobile: dedicated mobile greeting, push to bottom, static text (no typewriter)
- NameSuggestions: chips variant for mobile (horizontal scroll, emoji + name only)
- LobeMessage: add align/horizontal/disableTypewriter props, default flex-start
- CompletionPanel: explicit align=center, mobile-friendly sizes and block button
- ModeSwitch: mobile media query — avoid input area via safe-area-inset-bottom
- _layout: remove inner border/radius and outer padding on mobile
- Classic: gate ModeSwitch behind isDev (align with Agent page)
This commit is contained in:
Innei
2026-05-20 19:37:05 +08:00
committed by GitHub
parent 2b2abca0ae
commit c261c06098
9 changed files with 234 additions and 45 deletions
@@ -5,6 +5,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAgentMeta } from '@/features/Conversation/hooks/useAgentMeta';
import { useIsMobile } from '@/hooks/useIsMobile';
import LobeMessage from '@/routes/onboarding/components/LobeMessage';
import FeedbackPanel from './FeedbackPanel';
@@ -21,25 +22,37 @@ interface CompletionPanelProps {
const CompletionPanel = memo<CompletionPanelProps>(
({ feedbackSubmitted, finishTargetUrl, showFeedback, topicId }) => {
const { t } = useTranslation('onboarding');
const isMobile = useIsMobile();
const agentMeta = useAgentMeta();
return (
<Center height={'100%'} width={'100%'}>
<Flexbox align={'center'} className={staticStyle.completionEnter} gap={14} width={'100%'}>
<Flexbox align={'center'} gap={14} style={{ maxWidth: 600, width: '100%' }}>
<Center height={'100%'} paddingInline={isMobile ? 16 : 0} width={'100%'}>
<Flexbox
align={'center'}
className={staticStyle.completionEnter}
gap={isMobile ? 12 : 14}
width={'100%'}
>
<Flexbox
align={'center'}
gap={isMobile ? 12 : 14}
style={{ maxWidth: 600, width: '100%' }}
>
<LobeMessage
align={'center'}
avatar={agentMeta.avatar}
avatarSize={72}
fontSize={32}
gap={16}
avatarSize={isMobile ? 56 : 72}
fontSize={isMobile ? 24 : 32}
gap={isMobile ? 12 : 16}
sentences={[
t('agent.completion.sentence.readyWithName', { name: agentMeta.title }),
t('agent.completion.sentence.readyWhenYouAre'),
]}
/>
<Text fontSize={16} type={'secondary'}>
<Text fontSize={isMobile ? 14 : 16} type={'secondary'}>
{t('agent.completionSubtitle')}
</Text>
<Button
block={isMobile}
size={'large'}
style={{ marginTop: 8 }}
type={'primary'}
+13 -7
View File
@@ -18,12 +18,14 @@ import {
} from '@/features/Conversation';
import { dataSelectors, messageStateSelectors } from '@/features/Conversation/store';
import WideScreenContainer from '@/features/WideScreenContainer';
import { useIsMobile } from '@/hooks/useIsMobile';
import type { OnboardingPhase } from '@/types/user';
import { isDev } from '@/utils/env';
import CompletionPanel from './CompletionPanel';
import NameSuggestions from './NameSuggestions';
import Welcome from './Welcome';
import WelcomeMobile from './Welcome.mobile';
import WrapUpHint from './WrapUpHint';
const assistantLikeRoles = new Set(['assistant', 'assistantGroup', 'supervisor']);
@@ -62,6 +64,7 @@ const AgentOnboardingConversation = memo<AgentOnboardingConversationProps>(
showFeedback,
topicId,
}) => {
const isMobile = useIsMobile();
const displayMessages = useConversationStore(conversationSelectors.displayMessages);
const pendingInterventionCount = useConversationStore(
(s) => dataSelectors.pendingInterventions(s).length,
@@ -151,8 +154,8 @@ const AgentOnboardingConversation = memo<AgentOnboardingConversationProps>(
const greetingWelcome = useMemo(() => {
if (!shouldShowGreetingWelcome) return undefined;
return <Welcome />;
}, [shouldShowGreetingWelcome]);
return isMobile ? <WelcomeMobile /> : <Welcome />;
}, [shouldShowGreetingWelcome, isMobile]);
const agentMarketplaceSpacer = useMemo(() => {
if (!hasAgentMarketplaceIntervention) return undefined;
@@ -210,11 +213,14 @@ const AgentOnboardingConversation = memo<AgentOnboardingConversationProps>(
phase={phase}
onAfterFinish={onAfterWrapUp}
/>
{shouldShowGreetingWelcome && (
<WideScreenContainer>
<NameSuggestions />
</WideScreenContainer>
)}
{shouldShowGreetingWelcome &&
(isMobile ? (
<NameSuggestions variant={'chips'} />
) : (
<WideScreenContainer>
<NameSuggestions />
</WideScreenContainer>
))}
<ChatInput
disableFollowUpVariant
disableQueue
@@ -1,5 +1,5 @@
import { ActionIcon, Block, Flexbox, FluentEmoji, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import { createStaticStyles, cssVar } from 'antd-style';
import { RefreshCw } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,6 +14,37 @@ import {
const SUGGESTIONS_PER_GROUP = 3;
const styles = createStaticStyles(({ css }) => ({
chip: css`
cursor: pointer;
flex-shrink: 0;
padding-block: 6px;
padding-inline: 10px;
border: 1px solid ${cssVar.colorBorder};
border-radius: 999px;
background: ${cssVar.colorBgContainer};
transition: background 0.15s;
&:hover {
background: ${cssVar.colorBgTextHover};
}
`,
chipRow: css`
scrollbar-width: none;
overflow-x: auto;
flex-wrap: nowrap;
&::-webkit-scrollbar {
display: none;
}
`,
}));
const sampleSuggestions = (count: number, excludeIds: string[] = []): NameSuggestionItem[] => {
const remaining = nameSuggestionPool.filter((item) => !excludeIds.includes(item.id));
const target = Math.min(count, remaining.length);
@@ -25,7 +56,11 @@ const sampleSuggestions = (count: number, excludeIds: string[] = []): NameSugges
return picked;
};
const NameSuggestions = memo(() => {
interface NameSuggestionsProps {
variant?: 'cards' | 'chips';
}
const NameSuggestions = memo<NameSuggestionsProps>(({ variant = 'cards' }) => {
const { t, i18n } = useTranslation('onboarding');
const updateInputMessage = useConversationStore((s) => s.updateInputMessage);
const editor = useConversationStore((s) => s.editor);
@@ -54,6 +89,50 @@ const NameSuggestions = memo(() => {
[t, updateInputMessage, editor],
);
if (variant === 'chips') {
return (
<Flexbox gap={6} paddingInline={24}>
<Flexbox horizontal align={'center'} gap={4} justify={'space-between'}>
<Text fontSize={12} type={'secondary'}>
{t('agent.welcome.suggestion.title')}
</Text>
<Flexbox
horizontal
align={'center'}
gap={2}
style={{ cursor: 'pointer' }}
onClick={handleRefresh}
>
<ActionIcon icon={RefreshCw} size={'small'} />
<Text fontSize={11} type={'secondary'}>
{t('agent.welcome.suggestion.switch')}
</Text>
</Flexbox>
</Flexbox>
<Flexbox horizontal className={styles.chipRow} gap={6}>
{items.map((item) => {
const { name, prompt } = resolveNameSuggestion(item, i18n.language);
return (
<Flexbox
horizontal
align={'center'}
className={styles.chip}
gap={6}
key={item.id}
onClick={() => handleSelect(prompt, item.emoji)}
>
<FluentEmoji emoji={item.emoji} size={16} type={'anim'} />
<Text fontSize={13} weight={500}>
{name}
</Text>
</Flexbox>
);
})}
</Flexbox>
</Flexbox>
);
}
return (
<Flexbox gap={12}>
<Flexbox horizontal align={'center'} gap={8} justify={'space-between'}>
@@ -0,0 +1,43 @@
import { Flexbox, Markdown } from '@lobehub/ui';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import LobeMessage from '@/routes/onboarding/components/LobeMessage';
import { staticStyle } from './staticStyle';
const WelcomeMobile = memo(() => {
const { t } = useTranslation('onboarding');
return (
<>
<Flexbox flex={1} />
<Flexbox
className={staticStyle.greetingTextAnimated}
gap={12}
paddingBlock={'0 12px'}
paddingInline={6}
width={'100%'}
>
<LobeMessage
disableTypewriter
avatarSize={36}
fontSize={18}
gap={10}
sentences={[
t('agent.welcome.sentence.1'),
t('agent.welcome.sentence.2'),
t('agent.welcome.sentence.3'),
]}
/>
<Markdown fontSize={13} variant={'chat'}>
{t('agent.welcome')}
</Markdown>
</Flexbox>
</>
);
});
WelcomeMobile.displayName = 'WelcomeMobile';
export default WelcomeMobile;
+2 -1
View File
@@ -14,6 +14,7 @@ import InterestsStep from '@/routes/onboarding/features/InterestsStep';
import ProSettingsStep from '@/routes/onboarding/features/ProSettingsStep';
import { useUserStore } from '@/store/user';
import { onboardingSelectors } from '@/store/user/selectors';
import { isDev } from '@/utils/env';
const ClassicOnboardingPage = memo(() => {
const navigate = useNavigate();
@@ -65,7 +66,7 @@ const ClassicOnboardingPage = memo(() => {
return (
<OnboardingContainer>
<Flexbox gap={24} style={{ maxWidth: contentMaxWidth, width: '100%' }}>
<ModeSwitch />
{isDev && <ModeSwitch />}
{renderStep()}
</Flexbox>
</OnboardingContainer>
@@ -14,7 +14,7 @@ import { useServerConfigStore } from '@/store/serverConfig';
const COLLAPSED_STORAGE_KEY = 'LOBE_ONBOARDING_MODE_SWITCH_COLLAPSED';
const styles = createStaticStyles(({ css, cssVar }) => ({
const styles = createStaticStyles(({ css, cssVar, responsive }) => ({
anchor: css`
position: fixed;
z-index: 10;
@@ -25,6 +25,11 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
flex-direction: column;
gap: 8px;
align-items: flex-end;
${responsive.mobile} {
inset-block-end: calc(env(safe-area-inset-bottom, 0px) + 96px);
inset-inline-end: 12px;
}
`,
anchorWithLabel: css`
align-items: stretch;
+19 -8
View File
@@ -14,6 +14,7 @@ import { ProductLogo } from '@/components/Branding';
import LangButton from '@/features/User/UserPanel/LangButton';
import ThemeButton from '@/features/User/UserPanel/ThemeButton';
import { useIsDark } from '@/hooks/useIsDark';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useServerConfigStore } from '@/store/serverConfig';
import { useUserStore } from '@/store/user';
@@ -21,6 +22,7 @@ import { styles } from './style';
const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
const isDarkMode = useIsDark();
const isMobile = useIsMobile();
const theme = useTheme();
const { t } = useTranslation('onboarding');
const { pathname } = useLocation();
@@ -53,18 +55,29 @@ const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
);
return (
<Flexbox className={styles.outerContainer} height={'100%'} padding={8} width={'100%'}>
<Flexbox
className={styles.outerContainer}
height={'100%'}
padding={isMobile ? 0 : 8}
width={'100%'}
>
<Flexbox
className={cx(isDarkMode ? styles.innerContainerDark : styles.innerContainerLight)}
height={'100%'}
width={'100%'}
className={cx(
isMobile
? styles.innerContainerMobile
: isDarkMode
? styles.innerContainerDark
: styles.innerContainerLight,
)}
>
<Flexbox
horizontal
align={'center'}
gap={8}
justify={'space-between'}
padding={16}
padding={isMobile ? 12 : 16}
width={'100%'}
>
<ProductLogo color={theme.colorText} size={28} type={'text'} />
@@ -80,8 +93,8 @@ const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
{children}
</Center>
{showModeSwitchAndSkipFooter && (
<Center paddingBlock={'0 8px'} paddingInline={16}>
<Text fontSize={12} type={'secondary'}>
<Center paddingBlock={isMobile ? '0 12px' : '0 8px'} paddingInline={16}>
<Text fontSize={12} style={{ textAlign: 'center' }} type={'secondary'}>
<Trans
ns={'onboarding'}
components={{
@@ -92,9 +105,7 @@ const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
/>
),
modeText: <Text as={'span'} />,
skipLink: (
<Text as={'span'} style={{ cursor: 'pointer' }} onClick={handleSkip} />
),
skipLink: <Text as={'span'} style={{ cursor: 'pointer' }} onClick={handleSkip} />,
skipText: <Text as={'span'} style={{ cursor: 'pointer' }} />,
}}
i18nKey={
+6
View File
@@ -30,6 +30,12 @@ export const styles = createStaticStyles(({ css, cssVar }) => ({
background: ${cssVar.colorBgContainer};
`,
innerContainerMobile: css`
position: relative;
overflow: hidden auto;
background: ${cssVar.colorBgContainer};
`,
// Outer container
outerContainer: css`
position: relative;
@@ -11,37 +11,62 @@ import { ProductLogo } from '@/components/Branding';
interface LobeMessageProps extends Omit<FlexboxProps, 'children'> {
avatar?: string;
avatarSize?: number;
disableTypewriter?: boolean;
fontSize?: number;
gap?: number;
horizontal?: boolean;
sentences: TypewriterEffectProps['sentences'];
}
const LobeMessage = memo<LobeMessageProps>(
({ gap = 8, avatar, avatarSize, sentences, fontSize = 24, ...rest }) => {
({
gap = 8,
align,
avatar,
avatarSize,
horizontal,
disableTypewriter,
sentences,
fontSize = 24,
...rest
}) => {
const { i18n } = useTranslation();
const locale = i18n.language;
const resolvedAlign = align ?? 'flex-start';
const textCentered = resolvedAlign === 'center';
return (
<Flexbox gap={gap} {...rest}>
<Flexbox align={'flex-start'}>
{avatar ? (
<Avatar avatar={avatar} size={avatarSize || fontSize * 2} />
<Flexbox align={resolvedAlign} gap={gap} horizontal={horizontal} {...rest}>
{avatar ? (
<Avatar avatar={avatar} size={avatarSize || fontSize * 2} style={{ flexShrink: 0 }} />
) : (
<ProductLogo size={avatarSize || fontSize * 2} style={{ flexShrink: 0 }} />
)}
<Text
as={'h1'}
fontSize={fontSize}
weight={'bold'}
style={{
lineHeight: 1.3,
textAlign: textCentered ? 'center' : undefined,
wordBreak: 'break-word',
}}
>
{disableTypewriter ? (
(sentences[0] ?? '')
) : (
<ProductLogo size={avatarSize || fontSize * 2} />
<TypewriterEffect
cursorCharacter={<LoadingDots size={fontSize} variant={'pulse'} />}
cursorFade={false}
deletePauseDuration={1000}
deletingSpeed={16}
hideCursorWhileTyping={'afterTyping'}
key={locale}
pauseDuration={16_000}
sentences={sentences}
typingSpeed={32}
/>
)}
</Flexbox>
<Text as={'h1'} fontSize={fontSize} weight={'bold'}>
<TypewriterEffect
cursorCharacter={<LoadingDots size={fontSize} variant={'pulse'} />}
cursorFade={false}
deletePauseDuration={1000}
deletingSpeed={16}
hideCursorWhileTyping={'afterTyping'}
key={locale}
pauseDuration={16_000}
sentences={sentences}
typingSpeed={32}
/>
</Text>
</Flexbox>
);