fix(board): refine final question and free-text options in onboarding
This commit is contained in:
@@ -180,8 +180,12 @@ async def start_onboarding(
|
|||||||
"- 1 question to choose a unique name for the board lead agent (first-name style).\n"
|
"- 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"
|
"- 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"
|
" (communication style, autonomy, update cadence, and output formatting).\n"
|
||||||
'- Always include a final question: "Anything else we should know?" (constraints, context,\n'
|
'- Always include a final question (and only once): "Anything else we should know?"\n'
|
||||||
' preferences). Provide an option like "Yes (I\'ll type it)" so they can fill the free-text field.\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'
|
'- 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"
|
" to update the draft (until the user confirms).\n"
|
||||||
"Do NOT respond in OpenClaw chat.\n"
|
"Do NOT respond in OpenClaw chat.\n"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -53,6 +52,11 @@ type Question = {
|
|||||||
options: QuestionOption[];
|
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 => {
|
const normalizeQuestion = (value: unknown): Question | null => {
|
||||||
if (!value || typeof value !== "object") return null;
|
if (!value || typeof value !== "object") return null;
|
||||||
const data = value as { question?: unknown; options?: unknown };
|
const data = value as { question?: unknown; options?: unknown };
|
||||||
@@ -131,14 +135,19 @@ export function BoardOnboardingChat({
|
|||||||
const draft: BoardOnboardingAgentComplete | null =
|
const draft: BoardOnboardingAgentComplete | null =
|
||||||
session?.draft_goal ?? null;
|
session?.draft_goal ?? null;
|
||||||
|
|
||||||
|
const wantsFreeText = useMemo(
|
||||||
|
() => selectedOptions.some((label) => isFreeTextOption(label)),
|
||||||
|
[selectedOptions],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedOptions([]);
|
setSelectedOptions([]);
|
||||||
setOtherText("");
|
setOtherText("");
|
||||||
}, [question?.question]);
|
}, [question?.question]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (draft) setExtraContextOpen(true);
|
if (!wantsFreeText) setOtherText("");
|
||||||
}, [draft]);
|
}, [wantsFreeText]);
|
||||||
|
|
||||||
const startSession = useCallback(async () => {
|
const startSession = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -237,11 +246,11 @@ export function BoardOnboardingChat({
|
|||||||
|
|
||||||
const submitAnswer = useCallback(() => {
|
const submitAnswer = useCallback(() => {
|
||||||
const trimmedOther = otherText.trim();
|
const trimmedOther = otherText.trim();
|
||||||
if (selectedOptions.length === 0 && !trimmedOther) return;
|
if (selectedOptions.length === 0) return;
|
||||||
const answer =
|
if (wantsFreeText && !trimmedOther) return;
|
||||||
selectedOptions.length > 0 ? selectedOptions.join(", ") : "Other";
|
const answer = selectedOptions.join(", ");
|
||||||
void handleAnswer(answer, trimmedOther || undefined);
|
void handleAnswer(answer, wantsFreeText ? trimmedOther : undefined);
|
||||||
}, [handleAnswer, otherText, selectedOptions]);
|
}, [handleAnswer, otherText, selectedOptions, wantsFreeText]);
|
||||||
|
|
||||||
const confirmGoal = async () => {
|
const confirmGoal = async () => {
|
||||||
if (!draft) return;
|
if (!draft) return;
|
||||||
@@ -455,23 +464,34 @@ export function BoardOnboardingChat({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{wantsFreeText ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Input
|
<Textarea
|
||||||
placeholder="Other..."
|
className="min-h-[84px]"
|
||||||
|
placeholder="Type your answer..."
|
||||||
value={otherText}
|
value={otherText}
|
||||||
onChange={(event) => setOtherText(event.target.value)}
|
onChange={(event) => setOtherText(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
if (!(event.ctrlKey || event.metaKey)) return;
|
||||||
if (event.key !== "Enter") return;
|
if (event.key !== "Enter") return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
submitAnswer();
|
submitAnswer();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Tip: press Ctrl+Enter (or Cmd+Enter) to send.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="space-y-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={submitAnswer}
|
onClick={submitAnswer}
|
||||||
disabled={
|
disabled={
|
||||||
loading || (selectedOptions.length === 0 && !otherText.trim())
|
loading ||
|
||||||
|
selectedOptions.length === 0 ||
|
||||||
|
(wantsFreeText && !otherText.trim())
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{loading ? "Sending..." : "Next"}
|
{loading ? "Sending..." : "Next"}
|
||||||
@@ -480,58 +500,6 @@ export function BoardOnboardingChat({
|
|||||||
<p className="text-xs text-slate-500">Sending your answer…</p>
|
<p className="text-xs text-slate-500">Sending your answer…</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-3">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<p className="text-sm font-semibold text-slate-900">
|
|
||||||
Extra context (optional)
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setExtraContextOpen((prev) => !prev)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{extraContextOpen ? "Hide" : "Add"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{extraContextOpen ? (
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
<Textarea
|
|
||||||
className="min-h-[84px]"
|
|
||||||
placeholder="Anything else that will help the agent plan/act? (constraints, context, preferences, links, etc.)"
|
|
||||||
value={extraContext}
|
|
||||||
onChange={(event) => setExtraContext(event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (!(event.ctrlKey || event.metaKey)) return;
|
|
||||||
if (event.key !== "Enter") return;
|
|
||||||
event.preventDefault();
|
|
||||||
if (loading) return;
|
|
||||||
void submitExtraContext();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => void submitExtraContext()}
|
|
||||||
disabled={loading || !extraContext.trim()}
|
|
||||||
>
|
|
||||||
{loading ? "Sending..." : "Send context"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-500">
|
|
||||||
Tip: press Ctrl+Enter (or Cmd+Enter) to send.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="mt-2 text-xs text-slate-600">
|
|
||||||
Add anything that wasn't covered in the agent's
|
|
||||||
questions.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
|
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
|
||||||
|
|||||||
Reference in New Issue
Block a user