"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { getApiBaseUrl } from "@/lib/api-base"; const apiBase = getApiBaseUrl(); type BoardDraft = { board_type?: string; objective?: string | null; success_metrics?: Record | null; target_date?: string | null; }; type BoardSummary = { id: string; name: string; slug: string; board_type?: string; objective?: string | null; success_metrics?: Record | null; target_date?: string | null; goal_confirmed?: boolean; }; type OnboardingSession = { id: string; board_id: string; session_key: string; status: string; messages?: Array<{ role: string; content: string }> | null; draft_goal?: BoardDraft | null; }; type QuestionOption = { id: string; label: string }; type Question = { question: string; options: QuestionOption[]; }; 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; const options: QuestionOption[] = data.options .map((option, index) => { if (typeof option === "string") { return { id: String(index + 1), label: option }; } 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; if (!label) return null; return { id: typeof raw.id === "string" ? raw.id : String(index + 1), label, }; } return null; }) .filter((option): option is QuestionOption => Boolean(option)); if (!options.length) return null; return { question: data.question, options }; }; const parseQuestion = (messages?: Array<{ role: string; content: string }> | null) => { if (!messages?.length) return null; const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant"); if (!lastAssistant?.content) return null; try { return normalizeQuestion(JSON.parse(lastAssistant.content)); } catch { const match = lastAssistant.content.match(/```(?:json)?\s*([\s\S]*?)```/); if (match) { try { return normalizeQuestion(JSON.parse(match[1])); } catch { return null; } } } return null; }; export function BoardOnboardingChat({ boardId, onConfirmed, }: { boardId: string; onConfirmed: (board: BoardSummary) => void; }) { const { getToken } = useAuth(); const [session, setSession] = useState(null); const [loading, setLoading] = useState(false); const [otherText, setOtherText] = useState(""); const [error, setError] = useState(null); const [selectedOptions, setSelectedOptions] = useState([]); const question = useMemo(() => parseQuestion(session?.messages), [session]); const draft = session?.draft_goal ?? null; useEffect(() => { setSelectedOptions([]); setOtherText(""); }, [question?.question]); const authFetch = useCallback( async (url: string, options: RequestInit = {}) => { const token = await getToken(); return fetch(url, { ...options, headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(options.headers ?? {}), }, }); }, [getToken] ); const startSession = useCallback(async () => { setLoading(true); setError(null); try { const res = await authFetch(`${apiBase}/api/v1/boards/${boardId}/onboarding/start`, { method: "POST", body: JSON.stringify({}), }); if (!res.ok) throw new Error("Unable to start onboarding."); const data = (await res.json()) as OnboardingSession; setSession(data); } catch (err) { setError(err instanceof Error ? err.message : "Failed to start onboarding."); } finally { setLoading(false); } }, [authFetch, boardId]); const refreshSession = useCallback(async () => { try { const res = await authFetch(`${apiBase}/api/v1/boards/${boardId}/onboarding`); if (!res.ok) return; const data = (await res.json()) as OnboardingSession; setSession(data); } catch { // ignore } }, [authFetch, boardId]); useEffect(() => { startSession(); const interval = setInterval(refreshSession, 2000); return () => clearInterval(interval); }, [startSession, refreshSession]); const handleAnswer = useCallback( async (value: string, freeText?: string) => { setLoading(true); setError(null); try { const res = await authFetch( `${apiBase}/api/v1/boards/${boardId}/onboarding/answer`, { method: "POST", body: JSON.stringify({ answer: value, other_text: freeText ?? null, }), } ); if (!res.ok) throw new Error("Unable to submit answer."); const data = (await res.json()) as OnboardingSession; setSession(data); setOtherText(""); } catch (err) { setError(err instanceof Error ? err.message : "Failed to submit answer."); } finally { setLoading(false); } }, [authFetch, boardId] ); const toggleOption = useCallback((label: string) => { setSelectedOptions((prev) => prev.includes(label) ? prev.filter((item) => item !== label) : [...prev, label] ); }, []); const submitAnswer = useCallback(() => { const trimmedOther = otherText.trim(); const answer = selectedOptions.length > 0 ? selectedOptions.join(", ") : "Other"; if (!answer && !trimmedOther) return; void handleAnswer(answer, trimmedOther || undefined); }, [handleAnswer, otherText, selectedOptions]); const confirmGoal = async () => { if (!draft) return; setLoading(true); setError(null); try { const res = await authFetch( `${apiBase}/api/v1/boards/${boardId}/onboarding/confirm`, { method: "POST", body: JSON.stringify({ board_type: draft.board_type ?? "goal", objective: draft.objective ?? null, success_metrics: draft.success_metrics ?? null, target_date: draft.target_date ?? null, }), } ); if (!res.ok) throw new Error("Unable to confirm board goal."); const updated = await res.json(); onConfirmed(updated); } catch (err) { setError(err instanceof Error ? err.message : "Failed to confirm board goal."); } finally { setLoading(false); } }; return (
Board onboarding {error ? (
{error}
) : null} {draft ? (

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"}

) : question ? (

{question.question}

{question.options.map((option) => { const isSelected = selectedOptions.includes(option.label); return ( ); })}
setOtherText(event.target.value)} /> {loading ? (

Sending your answer…

) : null}
) : (
{loading ? "Waiting for the lead agent..." : "Preparing onboarding..."}
)}
); }