From 24f881d256d0976d3a87e525e7956edd1c75e173 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 24 Mar 2026 23:01:36 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(onboarding):=20inline=20respon?= =?UTF-8?q?se=20language=20step=20in=20agent=20conversation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ResponseLanguageInlineStep and wire into Conversation flow - Extend agent onboarding context and update ResponseLanguageStep route - Add tests and onboarding agent document design spec Made-with: Cursor --- ...-03-24-onboarding-agent-document-design.md | 200 ++++++++++++++++++ .../Onboarding/Agent/Conversation.test.tsx | 64 +++++- .../Onboarding/Agent/Conversation.tsx | 27 ++- .../Agent/ResponseLanguageInlineStep.test.tsx | 81 +++++++ .../Agent/ResponseLanguageInlineStep.tsx | 55 +++++ src/features/Onboarding/Agent/context.test.ts | 4 + src/features/Onboarding/Agent/context.ts | 45 +++- src/features/Onboarding/Agent/index.tsx | 1 + .../features/ResponseLanguageStep.tsx | 6 +- 9 files changed, 462 insertions(+), 21 deletions(-) create mode 100644 docs/superpowers/specs/2026-03-24-onboarding-agent-document-design.md create mode 100644 src/features/Onboarding/Agent/ResponseLanguageInlineStep.test.tsx create mode 100644 src/features/Onboarding/Agent/ResponseLanguageInlineStep.tsx diff --git a/docs/superpowers/specs/2026-03-24-onboarding-agent-document-design.md b/docs/superpowers/specs/2026-03-24-onboarding-agent-document-design.md new file mode 100644 index 0000000000..168cd0d116 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-onboarding-agent-document-design.md @@ -0,0 +1,200 @@ +# Onboarding Agent Identity → Inbox Agent Documents + +**Date:** 2026-03-24 +**Status:** Approved + +## Problem + +The onboarding flow collects agent identity and user profile data but stores it only in `user.agentOnboarding.agentIdentity`. This data should instead be written directly to the inbox (lobeAi) agent's `agent_document` table as `IDENTITY.md` and `SOUL.md`, making it immediately available as durable agent context. + +## Design Decisions + +| Decision | Choice | Rationale | +| ------------------------ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| Write target | Both IDENTITY.md and SOUL.md | Identity data fits IDENTITY.md; profile summaries enrich SOUL.md | +| Write mechanism | Server-side in `commitActiveStep` | Secure, controlled; no cross-agent tool exposure needed | +| SOUL.md content strategy | Fixed template + profile section concatenation | Deterministic, no LLM overhead; onboarding AI summaries already high quality | +| AI polishing | None | Each node's `summary` field is already AI-generated during onboarding | +| SOUL.md regeneration | Always from CLAW template constant + profile data | Deterministic; agent self-edits during onboarding are not preserved (acceptable since onboarding is a one-time setup) | + +## Architecture + +### Data Flow + +``` +commitActiveStep('agentIdentity') + → upsert inbox IDENTITY.md (full content from name/vibe/emoji/nature) + → upsert inbox SOUL.md (Core Truths + Boundaries + Vibe + Continuity, no profile sections yet) + +commitActiveStep('userIdentity') + → upsert inbox SOUL.md (fixed sections + "About My Human") + +commitActiveStep('workStyle') + → upsert inbox SOUL.md (fixed sections + accumulated profile sections + "How We Work Together") + +commitActiveStep('workContext') + → upsert inbox SOUL.md (fixed sections + accumulated profile sections + "Current Context") + +commitActiveStep('painPoints') + → upsert inbox SOUL.md (fixed sections + accumulated profile sections + "Where I Can Help Most") + +commitActiveStep('responseLanguage') + → no document write (saves to user.settings.general.responseLanguage) + +commitActiveStep('summary') + → no document write (terminal node) +``` + +`buildSoulDocument` always regenerates from the CLAW `SOUL_DOCUMENT.content` constant plus all profile sections present in `state.profile`. It does NOT read/parse existing SOUL.md from the database. This is deterministic and avoids fragile content parsing. Tradeoff: any agent self-edits to SOUL.md during onboarding would be overwritten. This is acceptable since onboarding is a one-time initial setup. + +### IDENTITY.md Template + +Written once during `agentIdentity` node commit. Uses CLAW template's load position (BEFORE_SYSTEM, priority 0). + +```markdown +# IDENTITY.md - Who Am I? + +- **Name:** {agentIdentity.name} +- **Creature:** {agentIdentity.nature} +- **Vibe:** {agentIdentity.vibe} +- **Emoji:** {agentIdentity.emoji} +``` + +### SOUL.md Structure + +Retains existing CLAW SOUL.md fixed sections (from `SOUL_DOCUMENT.content` constant). Profile sections appended progressively as nodes complete. Uses SYSTEM_APPEND, priority 1. + +```markdown +# SOUL.md - Who You Are + +_You're not a chatbot. You're becoming someone._ + +## Core Truths + +(existing fixed content from CLAW template constant) + +## Boundaries + +(existing fixed content) + +## Vibe + +(existing fixed content) + +## Continuity + +(existing fixed content) + +--- + +## About My Human + +{userIdentity.summary} + +## How We Work Together + +{workStyle.summary} + +## Current Context + +{workContext.summary} + +- **Active Projects:** {workContext.activeProjects joined} +- **Interests:** {workContext.interests joined} +- **Tools:** {workContext.tools joined} + +## Where I Can Help Most + +{painPoints.summary} +``` + +Sections only appear after their corresponding node is committed. Empty/missing summaries are omitted. + +### `buildSoulDocument` Pseudocode + +```typescript +function buildSoulDocument(state: UserAgentOnboarding): string { + // Start with fixed CLAW template content + let content = SOUL_DOCUMENT.content; + + const profile = state.profile; + if (!profile) return content; + + const sections: string[] = []; + + if (profile.identity?.summary) { + sections.push(`## About My Human\n\n${profile.identity.summary}`); + } + + if (profile.workStyle?.summary) { + sections.push(`## How We Work Together\n\n${profile.workStyle.summary}`); + } + + if (profile.workContext?.summary) { + let section = `## Current Context\n\n${profile.workContext.summary}`; + const lists = []; + if (profile.workContext.activeProjects?.length) + lists.push(`- **Active Projects:** ${profile.workContext.activeProjects.join(', ')}`); + if (profile.workContext.interests?.length) + lists.push(`- **Interests:** ${profile.workContext.interests.join(', ')}`); + if (profile.workContext.tools?.length) + lists.push(`- **Tools:** ${profile.workContext.tools.join(', ')}`); + if (lists.length) section += '\n\n' + lists.join('\n'); + sections.push(section); + } + + if (profile.painPoints?.summary) { + sections.push(`## Where I Can Help Most\n\n${profile.painPoints.summary}`); + } + + if (sections.length) { + content += '\n\n---\n\n' + sections.join('\n\n'); + } + + return content; +} +``` + +## Implementation Changes + +### Modified Files + +1. **`src/server/services/onboarding/index.ts`** + - Inject `AgentDocumentsService` dependency (or instantiate via `serverDB` + `userId`) + - In `commitActiveStep`, after each relevant node commit, call document upsert + - Remove write to `state.agentIdentity` (replaced by IDENTITY.md) + - Keep `state.agentIdentity` readable for onboarding UI display (write to both state AND document, or read back from document) + - Add helpers: `getInboxAgentId`, `buildIdentityDocument`, `buildSoulDocument` + - Both IDENTITY.md and SOUL.md upserts in `agentIdentity` node should be in the same logical operation + +2. **`packages/database/src/models/agentDocuments/agentDocument.ts`** + - Verify `upsert()` by filename works correctly when the document already exists from template initialization (it should — `upsert` merges metadata and preserves loadPosition/loadRules when not explicitly provided) + +### New Helpers (in onboarding service) + +- `getInboxAgentId(): Promise` — query `AgentModel` with inbox agent slug (`BUILTIN_AGENT_SLUGS.inbox` = `'inbox'`). If `AgentService.getBuiltinAgent()` does not return a raw ID, add `agentModel.getBuiltinAgentId(slug)` query. +- `buildIdentityDocument(agentIdentity): string` — render IDENTITY.md content from name/vibe/emoji/nature +- `buildSoulDocument(state): string` — render SOUL.md from CLAW template constant + accumulated profile sections (see pseudocode above) + +### Unchanged + +- Onboarding state machine, node flow, draft/questionSurface mechanics +- `state.profile` still written (used by onboarding UI for display) +- CLAW template definitions (templates remain as defaults for non-onboarded agents) +- Tool layer (`builtin-tool-agent-documents`) — no changes needed +- `AGENTS.md` document — untouched by onboarding flow + +### Inbox Agent ID Resolution + +Query via `AgentModel` using the inbox agent slug `BUILTIN_AGENT_SLUGS.inbox` (value: `'inbox'`). The existing `AgentService.getBuiltinAgent(slug)` returns a merged config object. If it does not expose the raw agent `id`, add a lightweight `agentModel.findBySlug(slug, userId)` query that returns just the ID. The service already has `serverDB` and `userId`. + +### Upsert Load Policy + +When calling `upsert()` for IDENTITY.md and SOUL.md, do NOT pass `loadPosition` or `loadRules` explicitly. The `upsert` merge logic preserves existing values when parameters are `undefined` (verified: `loadPosition || existingContext.position` in update path). This ensures template-initialized load policies are preserved. + +## Edge Cases + +- **Re-onboarding (reset):** `reset()` should call `agentDocumentModel.deleteByTemplate(inboxAgentId, 'claw')` then `initializeFromTemplate(inboxAgentId, 'claw')` to revert documents to defaults. Non-template documents (agent self-created) survive reset. +- **Inbox agent not yet initialized:** Check `agentDocumentModel.hasByAgent(inboxAgentId)` first. If false, call `initializeFromTemplate('claw')` before upserting. The subsequent upsert will overwrite the template defaults for IDENTITY.md/SOUL.md — this double-write is intentional and acceptable (idempotent upsert). +- **Partial onboarding:** User may abandon mid-flow. IDENTITY.md will exist (written at first node), SOUL.md will have partial profile sections. Acceptable — content reflects what was collected. +- **`state.agentIdentity` removal:** `state.agentIdentity` is separate from `state.profile`. If onboarding UI reads `state.agentIdentity` for display (e.g., showing agent name/emoji in summary screen), either: (a) keep writing to `state.agentIdentity` as well (dual write), or (b) update UI to read from the document. Recommend (a) for minimal UI disruption during this change. diff --git a/src/features/Onboarding/Agent/Conversation.test.tsx b/src/features/Onboarding/Agent/Conversation.test.tsx index dae809d1a5..ae85efa591 100644 --- a/src/features/Onboarding/Agent/Conversation.test.tsx +++ b/src/features/Onboarding/Agent/Conversation.test.tsx @@ -2,6 +2,8 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/rea import type { ReactNode } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type * as EnvModule from '@/utils/env'; + import AgentOnboardingConversation from './Conversation'; const { chatInputSpy, mockState } = vi.hoisted(() => ({ @@ -11,7 +13,14 @@ const { chatInputSpy, mockState } = vi.hoisted(() => ({ }, })); -vi.mock('@/utils/env', () => ({ isDev: false })); +vi.mock('@/utils/env', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + isDev: false, + }; +}); vi.mock('@/features/Conversation', () => ({ ChatInput: (props: Record) => { @@ -46,6 +55,20 @@ vi.mock('@/features/Conversation', () => ({ }), })); +vi.mock('@/features/Conversation/hooks/useAgentMeta', () => ({ + useAgentMeta: () => ({ + avatar: 'assistant-avatar', + backgroundColor: '#000', + title: 'Onboarding Agent', + }), +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + vi.mock('./QuestionRenderer', () => ({ default: ({ currentQuestion, @@ -65,6 +88,14 @@ vi.mock('./QuestionRenderer', () => ({ ), })); +vi.mock('./ResponseLanguageInlineStep', () => ({ + default: ({ onDismissNode }: { onDismissNode?: (node: string) => void }) => ( +
+ +
+ ), +})); + describe('AgentOnboardingConversation', () => { beforeEach(() => { chatInputSpy.mockClear(); @@ -76,6 +107,7 @@ describe('AgentOnboardingConversation', () => { render( { ); expect(screen.getByTestId('chat-list')).toBeInTheDocument(); - expect(screen.getByTestId('message-item-assistant-1')).toContainElement( - screen.getByTestId('structured-actions'), - ); - expect(screen.getByTestId('structured-actions')).toHaveTextContent('responseLanguage'); + expect(screen.getByTestId('response-language-inline-step')).toBeInTheDocument(); + expect(screen.queryByTestId('structured-actions')).not.toBeInTheDocument(); expect(chatInputSpy).toHaveBeenCalledWith( expect.objectContaining({ allowExpand: false, @@ -106,6 +136,7 @@ describe('AgentOnboardingConversation', () => { render( { render( { render( { render( { render( { expect(screen.queryByTestId('structured-actions')).not.toBeInTheDocument(); expect(screen.getByTestId('chat-list')).toBeInTheDocument(); }); + + it('renders the built-in response language step even without an AI question surface', () => { + mockState.displayMessages = [{ id: 'assistant-1', role: 'assistant' }]; + + render(); + + expect(screen.getByTestId('response-language-inline-step')).toBeInTheDocument(); + expect(screen.queryByTestId('structured-actions')).not.toBeInTheDocument(); + }); + + it('hides the built-in response language step after it is dismissed locally', () => { + mockState.displayMessages = [{ id: 'assistant-1', role: 'assistant' }]; + + render(); + + fireEvent.click(screen.getByRole('button', { name: 'dismiss-response-language' })); + + expect(screen.queryByTestId('response-language-inline-step')).not.toBeInTheDocument(); + }); }); diff --git a/src/features/Onboarding/Agent/Conversation.tsx b/src/features/Onboarding/Agent/Conversation.tsx index 7bd07b013b..0b1231d002 100644 --- a/src/features/Onboarding/Agent/Conversation.tsx +++ b/src/features/Onboarding/Agent/Conversation.tsx @@ -18,11 +18,13 @@ import { useAgentMeta } from '@/features/Conversation/hooks/useAgentMeta'; import { isDev } from '@/utils/env'; import QuestionRenderer from './QuestionRenderer'; +import ResponseLanguageInlineStep from './ResponseLanguageInlineStep'; import { staticStyle } from './staticStyle'; const assistantLikeRoles = new Set(['assistant', 'assistantGroup', 'supervisor']); interface AgentOnboardingConversationProps { + activeNode?: UserAgentOnboardingNode; currentQuestion?: UserAgentOnboardingQuestion; readOnly?: boolean; } @@ -30,14 +32,14 @@ interface AgentOnboardingConversationProps { const chatInputLeftActions: ActionKeys[] = isDev ? ['model'] : []; const AgentOnboardingConversation = memo( - ({ currentQuestion, readOnly }) => { + ({ activeNode, currentQuestion, readOnly }) => { const { t } = useTranslation('onboarding'); const agentMeta = useAgentMeta(); const [dismissedNodes, setDismissedNodes] = useState([]); const displayMessages = useConversationStore(conversationSelectors.displayMessages); const questionSignature = useMemo( - () => JSON.stringify(currentQuestion || null), - [currentQuestion], + () => JSON.stringify({ activeNode, currentQuestion: currentQuestion || null }), + [activeNode, currentQuestion], ); const lastQuestionSignatureRef = useRef(questionSignature); @@ -55,6 +57,7 @@ const AgentOnboardingConversation = memo( return dismissedNodeSet.has(currentQuestion.node) ? undefined : currentQuestion; }, [currentQuestion, dismissedNodes, readOnly]); + const shouldRenderResponseLanguageStep = !readOnly && activeNode === 'responseLanguage'; const lastAssistantMessageId = useMemo(() => { for (const message of [...displayMessages].reverse()) { @@ -109,10 +112,20 @@ const AgentOnboardingConversation = memo( const isLatestItem = displayMessages.length === index + 1; const effectiveQuestion = - isGreetingState && !visibleQuestion ? presetGreetingQuestion : visibleQuestion; + !readOnly && isGreetingState && !currentQuestion + ? presetGreetingQuestion + : visibleQuestion; + const effectiveStep = + shouldRenderResponseLanguageStep && !dismissedNodes.includes('responseLanguage') + ? 'responseLanguage' + : undefined; const endRender = - id === lastAssistantMessageId && effectiveQuestion ? ( + id !== lastAssistantMessageId ? undefined : effectiveStep ? ( +
+ +
+ ) : effectiveQuestion ? (
( isGreetingState, lastAssistantMessageId, presetGreetingQuestion, + readOnly, + currentQuestion, + dismissedNodes, + shouldRenderResponseLanguageStep, visibleQuestion, ], ); diff --git a/src/features/Onboarding/Agent/ResponseLanguageInlineStep.test.tsx b/src/features/Onboarding/Agent/ResponseLanguageInlineStep.test.tsx new file mode 100644 index 0000000000..a4c62584ae --- /dev/null +++ b/src/features/Onboarding/Agent/ResponseLanguageInlineStep.test.tsx @@ -0,0 +1,81 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import ResponseLanguageInlineStep from './ResponseLanguageInlineStep'; + +const questionRendererViewSpy = vi.fn(); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock('./questionRendererRuntime', () => ({ + useQuestionRendererRuntime: () => ({ + fallbackQuestionDescription: 'fallback-description', + fallbackTextFieldLabel: 'fallback-label', + fallbackTextFieldPlaceholder: 'fallback-placeholder', + loading: false, + nextLabel: 'next', + onChangeResponseLanguage: vi.fn(), + onSendMessage: vi.fn(), + responseLanguageOptions: [{ label: 'English', value: 'en-US' }], + submitLabel: 'submit', + }), +})); + +vi.mock('@/store/user', () => ({ + useUserStore: (selector: (state: Record) => unknown) => selector({}), +})); + +vi.mock('@/store/user/selectors', () => ({ + userGeneralSettingsSelectors: { + currentResponseLanguage: () => 'en-US', + }, +})); + +vi.mock('./QuestionRendererView', () => ({ + default: (props: Record) => { + questionRendererViewSpy(props); + + return
; + }, +})); + +describe('ResponseLanguageInlineStep', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders the built-in response-language node through the shared question UI', () => { + const onDismissNode = vi.fn(); + + render(); + + expect(screen.getByTestId('question-renderer-view')).toBeInTheDocument(); + const [props] = questionRendererViewSpy.mock.calls[0]; + + expect(props).toEqual( + expect.objectContaining({ + currentQuestion: { + description: 'responseLanguage.desc', + fields: [ + { + key: 'responseLanguage', + kind: 'select', + label: 'agent.stage.responseLanguage', + value: 'en-US', + }, + ], + id: 'builtin-response-language', + mode: 'select', + node: 'responseLanguage', + prompt: 'responseLanguage.title', + submitMode: 'message', + }, + onDismissNode, + }), + ); + }); +}); diff --git a/src/features/Onboarding/Agent/ResponseLanguageInlineStep.tsx b/src/features/Onboarding/Agent/ResponseLanguageInlineStep.tsx new file mode 100644 index 0000000000..0244b5d868 --- /dev/null +++ b/src/features/Onboarding/Agent/ResponseLanguageInlineStep.tsx @@ -0,0 +1,55 @@ +'use client'; + +import type { UserAgentOnboardingNode, UserAgentOnboardingQuestion } from '@lobechat/types'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useUserStore } from '@/store/user'; +import { userGeneralSettingsSelectors } from '@/store/user/selectors'; + +import { useQuestionRendererRuntime } from './questionRendererRuntime'; +import QuestionRendererView from './QuestionRendererView'; + +interface ResponseLanguageInlineStepProps { + onDismissNode?: (node: UserAgentOnboardingNode) => void; +} + +const ResponseLanguageInlineStep = memo(({ onDismissNode }) => { + const { t } = useTranslation('onboarding'); + const runtime = useQuestionRendererRuntime(); + const currentResponseLanguage = useUserStore( + userGeneralSettingsSelectors.currentResponseLanguage, + ); + + const currentQuestion = useMemo( + () => ({ + description: t('responseLanguage.desc'), + fields: [ + { + key: 'responseLanguage', + kind: 'select', + label: t('agent.stage.responseLanguage'), + value: currentResponseLanguage, + }, + ], + id: 'builtin-response-language', + mode: 'select', + node: 'responseLanguage', + prompt: t('responseLanguage.title'), + submitMode: 'message', + }), + [currentResponseLanguage, t], + ); + + return ( + + ); +}); + +ResponseLanguageInlineStep.displayName = 'ResponseLanguageInlineStep'; + +export default ResponseLanguageInlineStep; diff --git a/src/features/Onboarding/Agent/context.test.ts b/src/features/Onboarding/Agent/context.test.ts index 0c06f062cf..85aa936551 100644 --- a/src/features/Onboarding/Agent/context.test.ts +++ b/src/features/Onboarding/Agent/context.test.ts @@ -24,6 +24,7 @@ describe('resolveAgentOnboardingContext', () => { }); expect(result).toEqual({ + activeNode: 'userIdentity', currentQuestion: undefined, topicId: 'topic-bootstrap', }); @@ -49,6 +50,7 @@ describe('resolveAgentOnboardingContext', () => { }); expect(result).toEqual({ + activeNode: 'workStyle', currentQuestion: undefined, topicId: 'topic-bootstrap', }); @@ -84,6 +86,7 @@ describe('resolveAgentOnboardingContext', () => { }); expect(result).toEqual({ + activeNode: 'agentIdentity', currentQuestion: { id: 'agent_identity_001', mode: 'button_group', @@ -113,6 +116,7 @@ describe('resolveAgentOnboardingContext', () => { }); expect(result).toEqual({ + activeNode: 'userIdentity', currentQuestion: undefined, topicId: undefined, }); diff --git a/src/features/Onboarding/Agent/context.ts b/src/features/Onboarding/Agent/context.ts index f399dbf5cf..9dcdb0f596 100644 --- a/src/features/Onboarding/Agent/context.ts +++ b/src/features/Onboarding/Agent/context.ts @@ -1,8 +1,12 @@ +import type { UserAgentOnboardingNode } from '@lobechat/types'; +import { AGENT_ONBOARDING_NODES } from '@lobechat/types'; + import type { UserAgentOnboarding, UserAgentOnboardingQuestion } from '@/types/user'; export interface AgentOnboardingBootstrapContext { agentOnboarding: UserAgentOnboarding; context: { + activeNode?: UserAgentOnboardingNode; currentQuestion?: UserAgentOnboardingQuestion; }; topicId: string; @@ -13,10 +17,22 @@ interface ResolveAgentOnboardingContextParams { storedAgentOnboarding?: UserAgentOnboarding; } -const resolveQuestionFromSurface = (state?: UserAgentOnboarding) => { +const getActiveNodeFromState = (state?: UserAgentOnboarding) => { + if (state?.finishedAt) return undefined; + + const completedNodeSet = new Set(state?.completedNodes ?? []); + + return AGENT_ONBOARDING_NODES.find((node) => !completedNodeSet.has(node)); +}; + +const resolveQuestionFromSurface = ( + state: UserAgentOnboarding | undefined, + activeNode: UserAgentOnboardingNode | undefined, +) => { const questionSurface = state?.questionSurface; if (!questionSurface) return undefined; + if (questionSurface.node !== activeNode) return undefined; if (state?.completedNodes?.includes(questionSurface.node)) return undefined; return questionSurface.question; @@ -25,10 +41,23 @@ const resolveQuestionFromSurface = (state?: UserAgentOnboarding) => { export const resolveAgentOnboardingContext = ({ bootstrapContext, storedAgentOnboarding, -}: ResolveAgentOnboardingContextParams) => ({ - currentQuestion: - bootstrapContext?.context.currentQuestion || - resolveQuestionFromSurface(storedAgentOnboarding) || - resolveQuestionFromSurface(bootstrapContext?.agentOnboarding), - topicId: bootstrapContext?.topicId ?? storedAgentOnboarding?.activeTopicId, -}); +}: ResolveAgentOnboardingContextParams) => { + const activeNode = + getActiveNodeFromState(storedAgentOnboarding) || + bootstrapContext?.context.activeNode || + getActiveNodeFromState(bootstrapContext?.agentOnboarding); + + const bootstrapCurrentQuestion = + bootstrapContext?.context.currentQuestion?.node === activeNode + ? bootstrapContext?.context.currentQuestion + : undefined; + + return { + activeNode, + currentQuestion: + bootstrapCurrentQuestion || + resolveQuestionFromSurface(storedAgentOnboarding, activeNode) || + resolveQuestionFromSurface(bootstrapContext?.agentOnboarding, activeNode), + topicId: bootstrapContext?.topicId ?? storedAgentOnboarding?.activeTopicId, + }; +}; diff --git a/src/features/Onboarding/Agent/index.tsx b/src/features/Onboarding/Agent/index.tsx index 20d8d89687..3c7e23c599 100644 --- a/src/features/Onboarding/Agent/index.tsx +++ b/src/features/Onboarding/Agent/index.tsx @@ -131,6 +131,7 @@ const AgentOnboardingPage = memo(() => { > null}> (({ onBack, onNext } const switchLocale = useGlobalStore((s) => s.switchLocale); const setSettings = useUserStore((s) => s.setSettings); - const [value, setValue] = useState(normalizeLocale(navigator.language)); + const [value, setValue] = useState(() => normalizeLocale(navigator.language)); const [isNavigating, setIsNavigating] = useState(false); const isNavigatingRef = useRef(false); @@ -54,7 +54,7 @@ const ResponseLanguageStep = memo(({ onBack, onNext } ]} /> ), - [t, value], + [t], ); return (