From 175126a44bbb9588e73c573cc6b958d59d63d100 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 7 Feb 2026 12:33:00 +0530 Subject: [PATCH] fix(board): refine final question and free-text options in onboarding --- backend/app/api/board_onboarding.py | 8 +- .../src/components/BoardOnboardingChat.tsx | 112 +++++++----------- 2 files changed, 46 insertions(+), 74 deletions(-) diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index ee6e9cd3..0b63cd43 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -180,8 +180,12 @@ async def start_onboarding( "- 1 question to choose a unique name for the board lead agent (first-name style).\n" "- 2-4 questions to capture the user's preferences for how the board lead should work\n" " (communication style, autonomy, update cadence, and output formatting).\n" - '- Always include a final question: "Anything else we should know?" (constraints, context,\n' - ' preferences). Provide an option like "Yes (I\'ll type it)" so they can fill the free-text field.\n' + '- Always include a final question (and only once): "Anything else we should know?"\n' + " (constraints, context, preferences). This MUST be the last question.\n" + ' Provide an option like "Yes (I\'ll type it)" so they can enter free-text.\n' + " Do NOT ask for additional context on earlier questions.\n" + " Only include a free-text option on earlier questions if a typed answer is necessary;\n" + ' when you do, make the option label include "I\'ll type it" (e.g., "Other (I\'ll type it)").\n' '- If the user sends an "Additional context" message later, incorporate it and resend status=complete\n' " to update the draft (until the user confirms).\n" "Do NOT respond in OpenClaw chat.\n" diff --git a/frontend/src/components/BoardOnboardingChat.tsx b/frontend/src/components/BoardOnboardingChat.tsx index 14a8deaf..5d4e006d 100644 --- a/frontend/src/components/BoardOnboardingChat.tsx +++ b/frontend/src/components/BoardOnboardingChat.tsx @@ -8,7 +8,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { @@ -53,6 +52,11 @@ type Question = { options: QuestionOption[]; }; +const FREE_TEXT_OPTION_RE = + /(i'?ll type|i will type|type it|type my|other|custom|free\\s*text)/i; + +const isFreeTextOption = (label: string) => FREE_TEXT_OPTION_RE.test(label); + const normalizeQuestion = (value: unknown): Question | null => { if (!value || typeof value !== "object") return null; const data = value as { question?: unknown; options?: unknown }; @@ -131,14 +135,19 @@ export function BoardOnboardingChat({ const draft: BoardOnboardingAgentComplete | null = session?.draft_goal ?? null; + const wantsFreeText = useMemo( + () => selectedOptions.some((label) => isFreeTextOption(label)), + [selectedOptions], + ); + useEffect(() => { setSelectedOptions([]); setOtherText(""); }, [question?.question]); useEffect(() => { - if (draft) setExtraContextOpen(true); - }, [draft]); + if (!wantsFreeText) setOtherText(""); + }, [wantsFreeText]); const startSession = useCallback(async () => { setLoading(true); @@ -237,11 +246,11 @@ export function BoardOnboardingChat({ const submitAnswer = useCallback(() => { const trimmedOther = otherText.trim(); - if (selectedOptions.length === 0 && !trimmedOther) return; - const answer = - selectedOptions.length > 0 ? selectedOptions.join(", ") : "Other"; - void handleAnswer(answer, trimmedOther || undefined); - }, [handleAnswer, otherText, selectedOptions]); + if (selectedOptions.length === 0) return; + if (wantsFreeText && !trimmedOther) return; + const answer = selectedOptions.join(", "); + void handleAnswer(answer, wantsFreeText ? trimmedOther : undefined); + }, [handleAnswer, otherText, selectedOptions, wantsFreeText]); const confirmGoal = async () => { if (!draft) return; @@ -455,23 +464,34 @@ export function BoardOnboardingChat({ ); })} + {wantsFreeText ? ( +
+