diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index b6eb18b3..ee6e9cd3 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -180,6 +180,10 @@ 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' + '- 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" "All onboarding responses MUST be sent to Mission Control via API.\n" f"Mission Control base URL: {base_url}\n" diff --git a/backend/app/core/error_handling.py b/backend/app/core/error_handling.py index cfa53854..c97b825c 100644 --- a/backend/app/core/error_handling.py +++ b/backend/app/core/error_handling.py @@ -95,7 +95,9 @@ def _error_payload(*, detail: Any, request_id: str | None) -> dict[str, Any]: return payload -async def _request_validation_handler(request: Request, exc: RequestValidationError) -> JSONResponse: +async def _request_validation_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: # `RequestValidationError` is expected user input; don't log at ERROR. request_id = _get_request_id(request) return JSONResponse( @@ -104,7 +106,9 @@ async def _request_validation_handler(request: Request, exc: RequestValidationEr ) -async def _response_validation_handler(request: Request, exc: ResponseValidationError) -> JSONResponse: +async def _response_validation_handler( + request: Request, exc: ResponseValidationError +) -> JSONResponse: request_id = _get_request_id(request) logger.exception( "response_validation_error", diff --git a/frontend/src/components/BoardOnboardingChat.tsx b/frontend/src/components/BoardOnboardingChat.tsx index abd47b20..14a8deaf 100644 --- a/frontend/src/components/BoardOnboardingChat.tsx +++ b/frontend/src/components/BoardOnboardingChat.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost, @@ -55,7 +56,8 @@ type Question = { const normalizeQuestion = (value: unknown): Question | null => { if (!value || typeof value !== "object") return null; const data = value as { question?: unknown; options?: unknown }; - if (typeof data.question !== "string" || !Array.isArray(data.options)) return null; + if (typeof data.question !== "string" || !Array.isArray(data.options)) + return null; const options: QuestionOption[] = data.options .map((option, index) => { if (typeof option === "string") { @@ -64,7 +66,11 @@ const normalizeQuestion = (value: unknown): Question | null => { if (option && typeof option === "object") { const raw = option as { id?: unknown; label?: unknown }; const label = - typeof raw.label === "string" ? raw.label : typeof raw.id === "string" ? raw.id : null; + typeof raw.label === "string" + ? raw.label + : typeof raw.id === "string" + ? raw.id + : null; if (!label) return null; return { id: typeof raw.id === "string" ? raw.id : String(index + 1), @@ -80,7 +86,9 @@ const normalizeQuestion = (value: unknown): Question | null => { const parseQuestion = (messages?: NormalizedMessage[] | null) => { if (!messages?.length) return null; - const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant"); + const lastAssistant = [...messages] + .reverse() + .find((msg) => msg.role === "assistant"); if (!lastAssistant?.content) return null; try { return normalizeQuestion(JSON.parse(lastAssistant.content)); @@ -107,6 +115,8 @@ export function BoardOnboardingChat({ const [session, setSession] = useState(null); const [loading, setLoading] = useState(false); const [otherText, setOtherText] = useState(""); + const [extraContext, setExtraContext] = useState(""); + const [extraContextOpen, setExtraContextOpen] = useState(false); const [error, setError] = useState(null); const [selectedOptions, setSelectedOptions] = useState([]); @@ -114,14 +124,22 @@ export function BoardOnboardingChat({ () => normalizeMessages(session?.messages), [session?.messages], ); - const question = useMemo(() => parseQuestion(normalizedMessages), [normalizedMessages]); - const draft: BoardOnboardingAgentComplete | null = session?.draft_goal ?? null; + const question = useMemo( + () => parseQuestion(normalizedMessages), + [normalizedMessages], + ); + const draft: BoardOnboardingAgentComplete | null = + session?.draft_goal ?? null; useEffect(() => { setSelectedOptions([]); setOtherText(""); }, [question?.question]); + useEffect(() => { + if (draft) setExtraContextOpen(true); + }, [draft]); + const startSession = useCallback(async () => { setLoading(true); setError(null); @@ -133,7 +151,9 @@ export function BoardOnboardingChat({ if (result.status !== 200) throw new Error("Unable to start onboarding."); setSession(result.data); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to start onboarding."); + setError( + err instanceof Error ? err.message : "Failed to start onboarding.", + ); } finally { setLoading(false); } @@ -141,7 +161,8 @@ export function BoardOnboardingChat({ const refreshSession = useCallback(async () => { try { - const result = await getOnboardingApiV1BoardsBoardIdOnboardingGet(boardId); + const result = + await getOnboardingApiV1BoardsBoardIdOnboardingGet(boardId); if (result.status !== 200) return; setSession(result.data); } catch { @@ -160,18 +181,21 @@ export function BoardOnboardingChat({ setLoading(true); setError(null); try { - const result = await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost( - boardId, - { - answer: value, - other_text: freeText ?? null, - }, - ); + const result = + await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost( + boardId, + { + answer: value, + other_text: freeText ?? null, + }, + ); if (result.status !== 200) throw new Error("Unable to submit answer."); setSession(result.data); setOtherText(""); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to submit answer."); + setError( + err instanceof Error ? err.message : "Failed to submit answer.", + ); } finally { setLoading(false); } @@ -181,10 +205,36 @@ export function BoardOnboardingChat({ const toggleOption = useCallback((label: string) => { setSelectedOptions((prev) => - prev.includes(label) ? prev.filter((item) => item !== label) : [...prev, label] + prev.includes(label) + ? prev.filter((item) => item !== label) + : [...prev, label], ); }, []); + const submitExtraContext = useCallback(async () => { + const trimmed = extraContext.trim(); + if (!trimmed) return; + setLoading(true); + setError(null); + try { + const result = + await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost(boardId, { + answer: "Additional context", + other_text: trimmed, + }); + if (result.status !== 200) + throw new Error("Unable to submit extra context."); + setSession(result.data); + setExtraContext(""); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to submit extra context.", + ); + } finally { + setLoading(false); + } + }, [boardId, extraContext]); + const submitAnswer = useCallback(() => { const trimmedOther = otherText.trim(); if (selectedOptions.length === 0 && !trimmedOther) return; @@ -198,19 +248,23 @@ export function BoardOnboardingChat({ setLoading(true); setError(null); try { - const result = await confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost( - boardId, - { - board_type: draft.board_type ?? "goal", - objective: draft.objective ?? null, - success_metrics: draft.success_metrics ?? null, - target_date: draft.target_date ?? null, - }, - ); - if (result.status !== 200) throw new Error("Unable to confirm board goal."); + const result = + await confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost( + boardId, + { + board_type: draft.board_type ?? "goal", + objective: draft.objective ?? null, + success_metrics: draft.success_metrics ?? null, + target_date: draft.target_date ?? null, + }, + ); + if (result.status !== 200) + throw new Error("Unable to confirm board goal."); onConfirmed(result.data); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to confirm board goal."); + setError( + err instanceof Error ? err.message : "Failed to confirm board goal.", + ); } finally { setLoading(false); } @@ -233,90 +287,147 @@ export function BoardOnboardingChat({

Review the lead agent draft and confirm.

-
-

Objective

-

{draft.objective || "—"}

-

Success metrics

-
-	              {JSON.stringify(draft.success_metrics ?? {}, null, 2)}
-	            
-

Target date

-

{draft.target_date || "—"}

-

Board type

-

{draft.board_type || "goal"}

- {draft.user_profile ? ( - <> -

User profile

-

- Preferred name:{" "} - {draft.user_profile.preferred_name || "—"} -

-

- Pronouns:{" "} - {draft.user_profile.pronouns || "—"} -

-

- Timezone:{" "} - {draft.user_profile.timezone || "—"} -

- - ) : null} - {draft.lead_agent ? ( - <> -

- Lead agent preferences -

-

- Name:{" "} - {draft.lead_agent.name || "—"} -

-

- Role:{" "} - {draft.lead_agent.identity_profile?.role || "—"} -

-

- - Communication: - {" "} - {draft.lead_agent.identity_profile?.communication_style || "—"} -

-

- Emoji:{" "} - {draft.lead_agent.identity_profile?.emoji || "—"} -

-

- Autonomy:{" "} - {draft.lead_agent.autonomy_level || "—"} -

-

- Verbosity:{" "} - {draft.lead_agent.verbosity || "—"} -

-

- - Output format: - {" "} - {draft.lead_agent.output_format || "—"} -

-

- - Update cadence: - {" "} - {draft.lead_agent.update_cadence || "—"} -

- {draft.lead_agent.custom_instructions ? ( - <> -

- Custom instructions -

-
-	                      {draft.lead_agent.custom_instructions}
-	                    
- - ) : null} - - ) : null} -
+
+

Objective

+

{draft.objective || "—"}

+

Success metrics

+
+              {JSON.stringify(draft.success_metrics ?? {}, null, 2)}
+            
+

Target date

+

{draft.target_date || "—"}

+

Board type

+

{draft.board_type || "goal"}

+ {draft.user_profile ? ( + <> +

+ User profile +

+

+ + Preferred name: + {" "} + {draft.user_profile.preferred_name || "—"} +

+

+ Pronouns:{" "} + {draft.user_profile.pronouns || "—"} +

+

+ Timezone:{" "} + {draft.user_profile.timezone || "—"} +

+ + ) : null} + {draft.lead_agent ? ( + <> +

+ Lead agent preferences +

+

+ Name:{" "} + {draft.lead_agent.name || "—"} +

+

+ Role:{" "} + {draft.lead_agent.identity_profile?.role || "—"} +

+

+ + Communication: + {" "} + {draft.lead_agent.identity_profile?.communication_style || + "—"} +

+

+ Emoji:{" "} + {draft.lead_agent.identity_profile?.emoji || "—"} +

+

+ Autonomy:{" "} + {draft.lead_agent.autonomy_level || "—"} +

+

+ Verbosity:{" "} + {draft.lead_agent.verbosity || "—"} +

+

+ + Output format: + {" "} + {draft.lead_agent.output_format || "—"} +

+

+ + Update cadence: + {" "} + {draft.lead_agent.update_cadence || "—"} +

+ {draft.lead_agent.custom_instructions ? ( + <> +

+ Custom instructions +

+
+                      {draft.lead_agent.custom_instructions}
+                    
+ + ) : null} + + ) : null} +
+
+
+

+ Extra context (optional) +

+ +
+ {extraContextOpen ? ( +
+