diff --git a/frontend/src/components/BoardOnboardingChat.tsx b/frontend/src/components/BoardOnboardingChat.tsx index f0b09c47..bb67c100 100644 --- a/frontend/src/components/BoardOnboardingChat.tsx +++ b/frontend/src/components/BoardOnboardingChat.tsx @@ -1,6 +1,7 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { RefreshCcw } from "lucide-react"; import { DialogFooter, @@ -120,16 +121,40 @@ export function BoardOnboardingChat({ const isPageActive = usePageActive(); const [session, setSession] = useState(null); const [loading, setLoading] = useState(false); + const [awaitingAssistantFingerprint, setAwaitingAssistantFingerprint] = + useState(null); + const [awaitingKind, setAwaitingKind] = useState< + "answer" | "extra_context" | null + >(null); + const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState( + null, + ); const [otherText, setOtherText] = useState(""); const [extraContext, setExtraContext] = useState(""); const [extraContextOpen, setExtraContextOpen] = useState(false); const [error, setError] = useState(null); const [selectedOptions, setSelectedOptions] = useState([]); + const freeTextRef = useRef(null); + const extraContextRef = useRef(null); const normalizedMessages = useMemo( () => normalizeMessages(session?.messages), [session?.messages], ); + const lastAssistantFingerprint = useMemo(() => { + const rawMessages = session?.messages; + if (!rawMessages || !Array.isArray(rawMessages)) return ""; + for (let idx = rawMessages.length - 1; idx >= 0; idx -= 1) { + const entry = rawMessages[idx]; + if (!entry || typeof entry !== "object") continue; + const raw = entry as Record; + if (raw.role !== "assistant") continue; + const content = typeof raw.content === "string" ? raw.content : ""; + const timestamp = typeof raw.timestamp === "string" ? raw.timestamp : ""; + return `${timestamp}|${content}`; + } + return ""; + }, [session?.messages]); const question = useMemo( () => parseQuestion(normalizedMessages), [normalizedMessages], @@ -137,11 +162,26 @@ export function BoardOnboardingChat({ const draft: BoardOnboardingAgentComplete | null = session?.draft_goal ?? null; + const isAwaitingAgent = useMemo(() => { + if (!awaitingAssistantFingerprint) return false; + return lastAssistantFingerprint === awaitingAssistantFingerprint; + }, [awaitingAssistantFingerprint, lastAssistantFingerprint]); + const wantsFreeText = useMemo( () => selectedOptions.some((label) => isFreeTextOption(label)), [selectedOptions], ); + useEffect(() => { + if (!wantsFreeText) return; + freeTextRef.current?.focus(); + }, [wantsFreeText]); + + useEffect(() => { + if (!extraContextOpen) return; + extraContextRef.current?.focus(); + }, [extraContextOpen]); + useEffect(() => { setSelectedOptions([]); setOtherText(""); @@ -194,8 +234,12 @@ export function BoardOnboardingChat({ const handleAnswer = useCallback( async (value: string, freeText?: string) => { + const fingerprintBefore = lastAssistantFingerprint; setLoading(true); setError(null); + setAwaitingAssistantFingerprint(null); + setAwaitingKind(null); + setLastSubmittedAnswer(null); try { const result = await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost( @@ -208,6 +252,12 @@ export function BoardOnboardingChat({ if (result.status !== 200) throw new Error("Unable to submit answer."); setSession(result.data); setOtherText(""); + setSelectedOptions([]); + setAwaitingAssistantFingerprint(fingerprintBefore); + setAwaitingKind("answer"); + setLastSubmittedAnswer( + freeText ? `${value}: ${freeText}` : value, + ); } catch (err) { setError( err instanceof Error ? err.message : "Failed to submit answer.", @@ -216,7 +266,7 @@ export function BoardOnboardingChat({ setLoading(false); } }, - [boardId], + [boardId, lastAssistantFingerprint], ); const toggleOption = useCallback((label: string) => { @@ -230,8 +280,12 @@ export function BoardOnboardingChat({ const submitExtraContext = useCallback(async () => { const trimmed = extraContext.trim(); if (!trimmed) return; + const fingerprintBefore = lastAssistantFingerprint; setLoading(true); setError(null); + setAwaitingAssistantFingerprint(null); + setAwaitingKind(null); + setLastSubmittedAnswer(null); try { const result = await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost(boardId, { @@ -242,6 +296,10 @@ export function BoardOnboardingChat({ throw new Error("Unable to submit extra context."); setSession(result.data); setExtraContext(""); + setExtraContextOpen(false); + setAwaitingAssistantFingerprint(fingerprintBefore); + setAwaitingKind("extra_context"); + setLastSubmittedAnswer("Additional context"); } catch (err) { setError( err instanceof Error ? err.message : "Failed to submit extra context.", @@ -249,7 +307,7 @@ export function BoardOnboardingChat({ } finally { setLoading(false); } - }, [boardId, extraContext]); + }, [boardId, extraContext, lastAssistantFingerprint]); const submitAnswer = useCallback(() => { const trimmedOther = otherText.trim(); @@ -259,6 +317,15 @@ export function BoardOnboardingChat({ void handleAnswer(answer, wantsFreeText ? trimmedOther : undefined); }, [handleAnswer, otherText, selectedOptions, wantsFreeText]); + useEffect(() => { + if (!awaitingAssistantFingerprint) return; + if (lastAssistantFingerprint !== awaitingAssistantFingerprint) { + setAwaitingAssistantFingerprint(null); + setAwaitingKind(null); + setLastSubmittedAnswer(null); + } + }, [awaitingAssistantFingerprint, lastAssistantFingerprint]); + const confirmGoal = async () => { if (!draft) return; setLoading(true); @@ -303,6 +370,29 @@ export function BoardOnboardingChat({

Review the lead agent draft and confirm.

+ {isAwaitingAgent ? ( +
+
+ + + {awaitingKind === "extra_context" + ? "Updating the draft…" + : "Waiting for the agent…"} + +
+ {lastSubmittedAnswer ? ( +

+ Sent:{" "} + + {lastSubmittedAnswer} + +

+ ) : null} +

+ This usually takes a few seconds. +

+
+ ) : null}

Objective

{draft.objective || "—"}

@@ -402,26 +492,28 @@ export function BoardOnboardingChat({ size="sm" type="button" onClick={() => setExtraContextOpen((prev) => !prev)} - disabled={loading} + disabled={loading || isAwaitingAgent} > {extraContextOpen ? "Hide" : "Add"}
- {extraContextOpen ? ( -
-