mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
✨ 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:
@@ -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'}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user