feat(onboarding): add wrap-up button for agent onboarding (#13934)

Let users finish agent onboarding explicitly once they've engaged
enough, instead of waiting for the agent to trigger finishOnboarding.

- New WrapUpHint component above ChatInput; shows in summary phase or
  discovery phase after ≥3 user messages
- Confirm modal before finish; reuses existing finishOnboarding service
- Tightened Phase 2 (user_identity) system prompt: MUST save fullName
  before leaving phase, handle ambiguous name responses explicitly
This commit is contained in:
Innei 2026-04-18 11:58:49 +08:00 committed by GitHub
parent 326ca352b1
commit 9a2ee8a58f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 117 additions and 9 deletions

View file

@ -51,6 +51,11 @@
"agent.welcome.sentence.1": "So nice to meet you! Lets get to know each other.",
"agent.welcome.sentence.2": "What kind of partner do you want me to be?",
"agent.welcome.sentence.3": "First, give me a name :)",
"agent.wrapUp.action": "I think we're good",
"agent.wrapUp.confirm.cancel": "Keep chatting",
"agent.wrapUp.confirm.content": "I'll save what we've covered so far. You can always come back and chat more later.",
"agent.wrapUp.confirm.ok": "Finish now",
"agent.wrapUp.confirm.title": "Finish onboarding now?",
"back": "Back",
"finish": "Get Started",
"interests.area.business": "Business & Strategy",

View file

@ -51,6 +51,11 @@
"agent.welcome.sentence.1": "很高兴认识你!让我们互相了解一下吧。",
"agent.welcome.sentence.2": "你希望我成为怎样的伙伴?",
"agent.welcome.sentence.3": "先给我起个名字吧 :)",
"agent.wrapUp.action": "先这样吧",
"agent.wrapUp.confirm.cancel": "再聊聊",
"agent.wrapUp.confirm.content": "目前了解到的信息我都会保存,你随时都可以回来继续和我聊。",
"agent.wrapUp.confirm.ok": "结束引导",
"agent.wrapUp.confirm.title": "现在结束引导吗?",
"back": "上一步",
"finish": "开始使用",
"interests.area.business": "商业与战略",

View file

@ -46,9 +46,11 @@ You just "woke up" with no name or personality. Discover who you are through con
You know who you are. Now learn who the user is.
- If the user already shared their name earlier in the conversation, acknowledge it do not ask again. Otherwise, ask how they would like to be addressed.
- Call saveUserQuestion with fullName when learned (whether from this phase or recalled from earlier).
- **You MUST call saveUserQuestion with fullName before leaving this phase.** The phase will not advance until fullName is saved if you skip this, the user gets stuck in user_identity indefinitely.
- Prefer the name they naturally offer, including nicknames, handles, or any identifier they used to introduce themselves (e.g. when proposing your name). Save it as fullName immediately do not wait for a "formal" name.
- If the user's response about their name is ambiguous (e.g. "哈哈没有啦", "随便", "not really"), do NOT silently drop the question and move on. Ask exactly once more, directly: "那我该怎么称呼你?" / "What should I call you then?" — then save whatever they answer, even if it's a nickname or placeholder.
- Only if the user explicitly refuses to give any name after one clarifying ask, save a sensible fallback (e.g. the handle they used earlier, or "朋友" / "friend") and proceed.
- Begin the persona document with their role and basic context.
- Prefer the name they naturally offer, including nicknames.
- Transition by showing curiosity about their daily work.
### Phase 3: Discovery (phase: "discovery")

View file

@ -12,17 +12,22 @@ import {
MessageItem,
useConversationStore,
} from '@/features/Conversation';
import type { OnboardingPhase } from '@/types/user';
import { isDev } from '@/utils/env';
import CompletionPanel from './CompletionPanel';
import Welcome from './Welcome';
import WrapUpHint from './WrapUpHint';
const assistantLikeRoles = new Set(['assistant', 'assistantGroup', 'supervisor']);
interface AgentOnboardingConversationProps {
discoveryUserMessageCount?: number;
feedbackSubmitted?: boolean;
finishTargetUrl?: string;
onAfterWrapUp?: () => Promise<unknown> | void;
onboardingFinished?: boolean;
phase?: OnboardingPhase;
readOnly?: boolean;
showFeedback?: boolean;
topicId?: string;
@ -32,7 +37,17 @@ const chatInputLeftActions: ActionKeys[] = isDev ? ['model'] : [];
const chatInputRightActions: ActionKeys[] = [];
const AgentOnboardingConversation = memo<AgentOnboardingConversationProps>(
({ feedbackSubmitted, finishTargetUrl, onboardingFinished, readOnly, showFeedback, topicId }) => {
({
discoveryUserMessageCount,
feedbackSubmitted,
finishTargetUrl,
onAfterWrapUp,
onboardingFinished,
phase,
readOnly,
showFeedback,
topicId,
}) => {
const displayMessages = useConversationStore(conversationSelectors.displayMessages);
const isGreetingState = useMemo(() => {
@ -106,12 +121,19 @@ const AgentOnboardingConversation = memo<AgentOnboardingConversationProps>(
/>
</Flexbox>
{!readOnly && !onboardingFinished && (
<ChatInput
allowExpand={false}
leftActions={chatInputLeftActions}
rightActions={chatInputRightActions}
showRuntimeConfig={false}
/>
<>
<WrapUpHint
discoveryUserMessageCount={discoveryUserMessageCount}
phase={phase}
onAfterFinish={onAfterWrapUp}
/>
<ChatInput
allowExpand={false}
leftActions={chatInputLeftActions}
rightActions={chatInputRightActions}
showRuntimeConfig={false}
/>
</>
)}
</Flexbox>
);

View file

@ -0,0 +1,65 @@
'use client';
import { Button, Flexbox } from '@lobehub/ui';
import { confirmModal } from '@lobehub/ui/base-ui';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { userService } from '@/services/user';
import { useUserStore } from '@/store/user';
import type { OnboardingPhase } from '@/types/user';
interface WrapUpHintProps {
discoveryUserMessageCount?: number;
onAfterFinish?: () => Promise<unknown> | void;
phase?: OnboardingPhase;
}
const MIN_DISCOVERY_MESSAGES_FOR_WRAP_UP = 3;
const isEligible = (phase: OnboardingPhase, discoveryUserMessageCount?: number) => {
if (phase === 'summary') return true;
if (phase === 'discovery') {
return (discoveryUserMessageCount ?? 0) >= MIN_DISCOVERY_MESSAGES_FOR_WRAP_UP;
}
return false;
};
const WrapUpHint = memo<WrapUpHintProps>(({ phase, discoveryUserMessageCount, onAfterFinish }) => {
const { t } = useTranslation('onboarding');
const refreshUserState = useUserStore((s) => s.refreshUserState);
const [loading, setLoading] = useState(false);
if (!phase || !isEligible(phase, discoveryUserMessageCount)) return null;
const handleWrapUp = () => {
confirmModal({
cancelText: t('agent.wrapUp.confirm.cancel'),
content: t('agent.wrapUp.confirm.content'),
okText: t('agent.wrapUp.confirm.ok'),
onOk: async () => {
setLoading(true);
try {
await userService.finishOnboarding();
await refreshUserState();
await onAfterFinish?.();
} finally {
setLoading(false);
}
},
title: t('agent.wrapUp.confirm.title'),
});
};
return (
<Flexbox horizontal align={'center'} justify={'center'} paddingBlock={8}>
<Button loading={loading} size={'small'} type={'text'} onClick={handleWrapUp}>
{t('agent.wrapUp.action')}
</Button>
</Flexbox>
);
});
WrapUpHint.displayName = 'OnboardingWrapUpHint';
export default WrapUpHint;

View file

@ -155,12 +155,15 @@ const AgentOnboardingPage = memo(() => {
>
<ErrorBoundary fallbackRender={() => null}>
<AgentOnboardingConversation
discoveryUserMessageCount={data?.context?.discoveryUserMessageCount}
feedbackSubmitted={!!data?.feedbackSubmitted}
finishTargetUrl={finishTargetUrl}
onboardingFinished={onboardingFinished}
phase={data?.context?.phase}
readOnly={viewingHistoricalTopic}
showFeedback={!viewingHistoricalTopic}
topicId={effectiveTopicId}
onAfterWrapUp={syncOnboardingContext}
/>
</ErrorBoundary>
</OnboardingConversationProvider>

View file

@ -39,6 +39,12 @@ export default {
'agent.stage.workStyle': 'Work Style',
'agent.subtitle': 'Complete setup in a dedicated onboarding conversation.',
'agent.summaryHint': 'Finish here if the setup summary looks right.',
'agent.wrapUp.action': "I think we're good",
'agent.wrapUp.confirm.cancel': 'Keep chatting',
'agent.wrapUp.confirm.content':
"I'll save what we've covered so far. You can always come back and chat more later.",
'agent.wrapUp.confirm.ok': 'Finish now',
'agent.wrapUp.confirm.title': 'Finish onboarding now?',
'agent.welcome':
"...hm? I just woke up — my mind's a blank. Who are you? And — what should I be called? I need a name too.",
'agent.welcome.guide.growTogether.desc':