feat: allow extra context during board onboarding
This commit is contained in:
@@ -180,6 +180,10 @@ 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'
|
||||||
|
' 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"
|
"Do NOT respond in OpenClaw chat.\n"
|
||||||
"All onboarding responses MUST be sent to Mission Control via API.\n"
|
"All onboarding responses MUST be sent to Mission Control via API.\n"
|
||||||
f"Mission Control base URL: {base_url}\n"
|
f"Mission Control base URL: {base_url}\n"
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ def _error_payload(*, detail: Any, request_id: str | None) -> dict[str, Any]:
|
|||||||
return payload
|
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.
|
# `RequestValidationError` is expected user input; don't log at ERROR.
|
||||||
request_id = _get_request_id(request)
|
request_id = _get_request_id(request)
|
||||||
return JSONResponse(
|
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)
|
request_id = _get_request_id(request)
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"response_validation_error",
|
"response_validation_error",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} 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 { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost,
|
answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost,
|
||||||
@@ -55,7 +56,8 @@ type Question = {
|
|||||||
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 };
|
||||||
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
|
const options: QuestionOption[] = data.options
|
||||||
.map((option, index) => {
|
.map((option, index) => {
|
||||||
if (typeof option === "string") {
|
if (typeof option === "string") {
|
||||||
@@ -64,7 +66,11 @@ const normalizeQuestion = (value: unknown): Question | null => {
|
|||||||
if (option && typeof option === "object") {
|
if (option && typeof option === "object") {
|
||||||
const raw = option as { id?: unknown; label?: unknown };
|
const raw = option as { id?: unknown; label?: unknown };
|
||||||
const label =
|
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;
|
if (!label) return null;
|
||||||
return {
|
return {
|
||||||
id: typeof raw.id === "string" ? raw.id : String(index + 1),
|
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) => {
|
const parseQuestion = (messages?: NormalizedMessage[] | null) => {
|
||||||
if (!messages?.length) return 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;
|
if (!lastAssistant?.content) return null;
|
||||||
try {
|
try {
|
||||||
return normalizeQuestion(JSON.parse(lastAssistant.content));
|
return normalizeQuestion(JSON.parse(lastAssistant.content));
|
||||||
@@ -107,6 +115,8 @@ export function BoardOnboardingChat({
|
|||||||
const [session, setSession] = useState<BoardOnboardingRead | null>(null);
|
const [session, setSession] = useState<BoardOnboardingRead | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [otherText, setOtherText] = useState("");
|
const [otherText, setOtherText] = useState("");
|
||||||
|
const [extraContext, setExtraContext] = useState("");
|
||||||
|
const [extraContextOpen, setExtraContextOpen] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -114,14 +124,22 @@ export function BoardOnboardingChat({
|
|||||||
() => normalizeMessages(session?.messages),
|
() => normalizeMessages(session?.messages),
|
||||||
[session?.messages],
|
[session?.messages],
|
||||||
);
|
);
|
||||||
const question = useMemo(() => parseQuestion(normalizedMessages), [normalizedMessages]);
|
const question = useMemo(
|
||||||
const draft: BoardOnboardingAgentComplete | null = session?.draft_goal ?? null;
|
() => parseQuestion(normalizedMessages),
|
||||||
|
[normalizedMessages],
|
||||||
|
);
|
||||||
|
const draft: BoardOnboardingAgentComplete | null =
|
||||||
|
session?.draft_goal ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedOptions([]);
|
setSelectedOptions([]);
|
||||||
setOtherText("");
|
setOtherText("");
|
||||||
}, [question?.question]);
|
}, [question?.question]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (draft) setExtraContextOpen(true);
|
||||||
|
}, [draft]);
|
||||||
|
|
||||||
const startSession = useCallback(async () => {
|
const startSession = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -133,7 +151,9 @@ export function BoardOnboardingChat({
|
|||||||
if (result.status !== 200) throw new Error("Unable to start onboarding.");
|
if (result.status !== 200) throw new Error("Unable to start onboarding.");
|
||||||
setSession(result.data);
|
setSession(result.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to start onboarding.");
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to start onboarding.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -141,7 +161,8 @@ export function BoardOnboardingChat({
|
|||||||
|
|
||||||
const refreshSession = useCallback(async () => {
|
const refreshSession = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await getOnboardingApiV1BoardsBoardIdOnboardingGet(boardId);
|
const result =
|
||||||
|
await getOnboardingApiV1BoardsBoardIdOnboardingGet(boardId);
|
||||||
if (result.status !== 200) return;
|
if (result.status !== 200) return;
|
||||||
setSession(result.data);
|
setSession(result.data);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -160,18 +181,21 @@ export function BoardOnboardingChat({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost(
|
const result =
|
||||||
boardId,
|
await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost(
|
||||||
{
|
boardId,
|
||||||
answer: value,
|
{
|
||||||
other_text: freeText ?? null,
|
answer: value,
|
||||||
},
|
other_text: freeText ?? null,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
if (result.status !== 200) throw new Error("Unable to submit answer.");
|
if (result.status !== 200) throw new Error("Unable to submit answer.");
|
||||||
setSession(result.data);
|
setSession(result.data);
|
||||||
setOtherText("");
|
setOtherText("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to submit answer.");
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to submit answer.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -181,10 +205,36 @@ export function BoardOnboardingChat({
|
|||||||
|
|
||||||
const toggleOption = useCallback((label: string) => {
|
const toggleOption = useCallback((label: string) => {
|
||||||
setSelectedOptions((prev) =>
|
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 submitAnswer = useCallback(() => {
|
||||||
const trimmedOther = otherText.trim();
|
const trimmedOther = otherText.trim();
|
||||||
if (selectedOptions.length === 0 && !trimmedOther) return;
|
if (selectedOptions.length === 0 && !trimmedOther) return;
|
||||||
@@ -198,19 +248,23 @@ export function BoardOnboardingChat({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost(
|
const result =
|
||||||
boardId,
|
await confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost(
|
||||||
{
|
boardId,
|
||||||
board_type: draft.board_type ?? "goal",
|
{
|
||||||
objective: draft.objective ?? null,
|
board_type: draft.board_type ?? "goal",
|
||||||
success_metrics: draft.success_metrics ?? null,
|
objective: draft.objective ?? null,
|
||||||
target_date: draft.target_date ?? 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.");
|
);
|
||||||
|
if (result.status !== 200)
|
||||||
|
throw new Error("Unable to confirm board goal.");
|
||||||
onConfirmed(result.data);
|
onConfirmed(result.data);
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -233,90 +287,147 @@ export function BoardOnboardingChat({
|
|||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
Review the lead agent draft and confirm.
|
Review the lead agent draft and confirm.
|
||||||
</p>
|
</p>
|
||||||
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm">
|
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm">
|
||||||
<p className="font-semibold text-slate-900">Objective</p>
|
<p className="font-semibold text-slate-900">Objective</p>
|
||||||
<p className="text-slate-700">{draft.objective || "—"}</p>
|
<p className="text-slate-700">{draft.objective || "—"}</p>
|
||||||
<p className="mt-3 font-semibold text-slate-900">Success metrics</p>
|
<p className="mt-3 font-semibold text-slate-900">Success metrics</p>
|
||||||
<pre className="mt-1 whitespace-pre-wrap text-xs text-slate-600">
|
<pre className="mt-1 whitespace-pre-wrap text-xs text-slate-600">
|
||||||
{JSON.stringify(draft.success_metrics ?? {}, null, 2)}
|
{JSON.stringify(draft.success_metrics ?? {}, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<p className="mt-3 font-semibold text-slate-900">Target date</p>
|
<p className="mt-3 font-semibold text-slate-900">Target date</p>
|
||||||
<p className="text-slate-700">{draft.target_date || "—"}</p>
|
<p className="text-slate-700">{draft.target_date || "—"}</p>
|
||||||
<p className="mt-3 font-semibold text-slate-900">Board type</p>
|
<p className="mt-3 font-semibold text-slate-900">Board type</p>
|
||||||
<p className="text-slate-700">{draft.board_type || "goal"}</p>
|
<p className="text-slate-700">{draft.board_type || "goal"}</p>
|
||||||
{draft.user_profile ? (
|
{draft.user_profile ? (
|
||||||
<>
|
<>
|
||||||
<p className="mt-4 font-semibold text-slate-900">User profile</p>
|
<p className="mt-4 font-semibold text-slate-900">
|
||||||
<p className="text-slate-700">
|
User profile
|
||||||
<span className="font-medium text-slate-900">Preferred name:</span>{" "}
|
</p>
|
||||||
{draft.user_profile.preferred_name || "—"}
|
<p className="text-slate-700">
|
||||||
</p>
|
<span className="font-medium text-slate-900">
|
||||||
<p className="text-slate-700">
|
Preferred name:
|
||||||
<span className="font-medium text-slate-900">Pronouns:</span>{" "}
|
</span>{" "}
|
||||||
{draft.user_profile.pronouns || "—"}
|
{draft.user_profile.preferred_name || "—"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-700">
|
<p className="text-slate-700">
|
||||||
<span className="font-medium text-slate-900">Timezone:</span>{" "}
|
<span className="font-medium text-slate-900">Pronouns:</span>{" "}
|
||||||
{draft.user_profile.timezone || "—"}
|
{draft.user_profile.pronouns || "—"}
|
||||||
</p>
|
</p>
|
||||||
</>
|
<p className="text-slate-700">
|
||||||
) : null}
|
<span className="font-medium text-slate-900">Timezone:</span>{" "}
|
||||||
{draft.lead_agent ? (
|
{draft.user_profile.timezone || "—"}
|
||||||
<>
|
</p>
|
||||||
<p className="mt-4 font-semibold text-slate-900">
|
</>
|
||||||
Lead agent preferences
|
) : null}
|
||||||
</p>
|
{draft.lead_agent ? (
|
||||||
<p className="text-slate-700">
|
<>
|
||||||
<span className="font-medium text-slate-900">Name:</span>{" "}
|
<p className="mt-4 font-semibold text-slate-900">
|
||||||
{draft.lead_agent.name || "—"}
|
Lead agent preferences
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-700">
|
<p className="text-slate-700">
|
||||||
<span className="font-medium text-slate-900">Role:</span>{" "}
|
<span className="font-medium text-slate-900">Name:</span>{" "}
|
||||||
{draft.lead_agent.identity_profile?.role || "—"}
|
{draft.lead_agent.name || "—"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-700">
|
<p className="text-slate-700">
|
||||||
<span className="font-medium text-slate-900">
|
<span className="font-medium text-slate-900">Role:</span>{" "}
|
||||||
Communication:
|
{draft.lead_agent.identity_profile?.role || "—"}
|
||||||
</span>{" "}
|
</p>
|
||||||
{draft.lead_agent.identity_profile?.communication_style || "—"}
|
<p className="text-slate-700">
|
||||||
</p>
|
<span className="font-medium text-slate-900">
|
||||||
<p className="text-slate-700">
|
Communication:
|
||||||
<span className="font-medium text-slate-900">Emoji:</span>{" "}
|
</span>{" "}
|
||||||
{draft.lead_agent.identity_profile?.emoji || "—"}
|
{draft.lead_agent.identity_profile?.communication_style ||
|
||||||
</p>
|
"—"}
|
||||||
<p className="text-slate-700">
|
</p>
|
||||||
<span className="font-medium text-slate-900">Autonomy:</span>{" "}
|
<p className="text-slate-700">
|
||||||
{draft.lead_agent.autonomy_level || "—"}
|
<span className="font-medium text-slate-900">Emoji:</span>{" "}
|
||||||
</p>
|
{draft.lead_agent.identity_profile?.emoji || "—"}
|
||||||
<p className="text-slate-700">
|
</p>
|
||||||
<span className="font-medium text-slate-900">Verbosity:</span>{" "}
|
<p className="text-slate-700">
|
||||||
{draft.lead_agent.verbosity || "—"}
|
<span className="font-medium text-slate-900">Autonomy:</span>{" "}
|
||||||
</p>
|
{draft.lead_agent.autonomy_level || "—"}
|
||||||
<p className="text-slate-700">
|
</p>
|
||||||
<span className="font-medium text-slate-900">
|
<p className="text-slate-700">
|
||||||
Output format:
|
<span className="font-medium text-slate-900">Verbosity:</span>{" "}
|
||||||
</span>{" "}
|
{draft.lead_agent.verbosity || "—"}
|
||||||
{draft.lead_agent.output_format || "—"}
|
</p>
|
||||||
</p>
|
<p className="text-slate-700">
|
||||||
<p className="text-slate-700">
|
<span className="font-medium text-slate-900">
|
||||||
<span className="font-medium text-slate-900">
|
Output format:
|
||||||
Update cadence:
|
</span>{" "}
|
||||||
</span>{" "}
|
{draft.lead_agent.output_format || "—"}
|
||||||
{draft.lead_agent.update_cadence || "—"}
|
</p>
|
||||||
</p>
|
<p className="text-slate-700">
|
||||||
{draft.lead_agent.custom_instructions ? (
|
<span className="font-medium text-slate-900">
|
||||||
<>
|
Update cadence:
|
||||||
<p className="mt-3 font-semibold text-slate-900">
|
</span>{" "}
|
||||||
Custom instructions
|
{draft.lead_agent.update_cadence || "—"}
|
||||||
</p>
|
</p>
|
||||||
<pre className="mt-1 whitespace-pre-wrap text-xs text-slate-600">
|
{draft.lead_agent.custom_instructions ? (
|
||||||
{draft.lead_agent.custom_instructions}
|
<>
|
||||||
</pre>
|
<p className="mt-3 font-semibold text-slate-900">
|
||||||
</>
|
Custom instructions
|
||||||
) : null}
|
</p>
|
||||||
</>
|
<pre className="mt-1 whitespace-pre-wrap text-xs text-slate-600">
|
||||||
) : null}
|
{draft.lead_agent.custom_instructions}
|
||||||
</div>
|
</pre>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</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 the agent should know before you confirm? (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>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button onClick={confirmGoal} disabled={loading}>
|
<Button onClick={confirmGoal} disabled={loading}>
|
||||||
Confirm goal
|
Confirm goal
|
||||||
@@ -325,7 +436,9 @@ export function BoardOnboardingChat({
|
|||||||
</div>
|
</div>
|
||||||
) : question ? (
|
) : question ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm font-medium text-slate-900">{question.question}</p>
|
<p className="text-sm font-medium text-slate-900">
|
||||||
|
{question.question}
|
||||||
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{question.options.map((option) => {
|
{question.options.map((option) => {
|
||||||
const isSelected = selectedOptions.includes(option.label);
|
const isSelected = selectedOptions.includes(option.label);
|
||||||
@@ -358,8 +471,7 @@ export function BoardOnboardingChat({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={submitAnswer}
|
onClick={submitAnswer}
|
||||||
disabled={
|
disabled={
|
||||||
loading ||
|
loading || (selectedOptions.length === 0 && !otherText.trim())
|
||||||
(selectedOptions.length === 0 && !otherText.trim())
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{loading ? "Sending..." : "Next"}
|
{loading ? "Sending..." : "Next"}
|
||||||
@@ -368,10 +480,64 @@ 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">
|
||||||
{loading ? "Waiting for the lead agent..." : "Preparing onboarding..."}
|
{loading
|
||||||
|
? "Waiting for the lead agent..."
|
||||||
|
: "Preparing onboarding..."}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user