diff --git a/locales/en-US/onboarding.json b/locales/en-US/onboarding.json index 8414f08fdf..521fd9f17a 100644 --- a/locales/en-US/onboarding.json +++ b/locales/en-US/onboarding.json @@ -51,6 +51,11 @@ "agent.welcome.sentence.1": "So nice to meet you! Let’s 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", diff --git a/locales/zh-CN/onboarding.json b/locales/zh-CN/onboarding.json index 2a8f13118f..33f37d1626 100644 --- a/locales/zh-CN/onboarding.json +++ b/locales/zh-CN/onboarding.json @@ -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": "商业与战略", diff --git a/packages/builtin-agents/src/agents/web-onboarding/systemRole.ts b/packages/builtin-agents/src/agents/web-onboarding/systemRole.ts index a4df0a9e11..fd67e47dab 100644 --- a/packages/builtin-agents/src/agents/web-onboarding/systemRole.ts +++ b/packages/builtin-agents/src/agents/web-onboarding/systemRole.ts @@ -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") diff --git a/src/features/Onboarding/Agent/Conversation.tsx b/src/features/Onboarding/Agent/Conversation.tsx index 15eea5cfbf..00735b4718 100644 --- a/src/features/Onboarding/Agent/Conversation.tsx +++ b/src/features/Onboarding/Agent/Conversation.tsx @@ -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 | 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( - ({ 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( /> {!readOnly && !onboardingFinished && ( - + <> + + + )} ); diff --git a/src/features/Onboarding/Agent/WrapUpHint.tsx b/src/features/Onboarding/Agent/WrapUpHint.tsx new file mode 100644 index 0000000000..2514b7492b --- /dev/null +++ b/src/features/Onboarding/Agent/WrapUpHint.tsx @@ -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 | 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(({ 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 ( + + + + ); +}); + +WrapUpHint.displayName = 'OnboardingWrapUpHint'; + +export default WrapUpHint; diff --git a/src/features/Onboarding/Agent/index.tsx b/src/features/Onboarding/Agent/index.tsx index fb9cf4c3ab..7cac8ece8d 100644 --- a/src/features/Onboarding/Agent/index.tsx +++ b/src/features/Onboarding/Agent/index.tsx @@ -155,12 +155,15 @@ const AgentOnboardingPage = memo(() => { > null}> diff --git a/src/locales/default/onboarding.ts b/src/locales/default/onboarding.ts index a2ee02c9a0..ca42d05847 100644 --- a/src/locales/default/onboarding.ts +++ b/src/locales/default/onboarding.ts @@ -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':