"use client"; import { useMemo, useState } from "react"; import { type OpenCodeQuestionRequest, type QuestionAnswer, useOpenCodeRuntimeExtras, } from "@assistant-ui/react-opencode"; import { AlertCircleIcon, CheckCircle2Icon, CircleHelpIcon, LoaderIcon, XCircleIcon, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; type PendingQuestionState = { selectedLabels: string[]; customAnswer: string; }; const buildEmptyQuestionState = ( request: OpenCodeQuestionRequest, ): PendingQuestionState[] => request.questions.map(() => ({ selectedLabels: [], customAnswer: "", })); const encodeQuestionAnswers = ( request: OpenCodeQuestionRequest, formState: PendingQuestionState[], ): QuestionAnswer[] => request.questions.map((question, index) => { const current = formState[index]; if (!current) return []; const answers = [...current.selectedLabels]; const customValue = current.customAnswer.trim(); const customAllowed = question.custom !== false; if (customAllowed && customValue) { answers.push(`Other: ${customValue}`); } return answers; }); export const getQuestionSummary = (request?: OpenCodeQuestionRequest) => { if (!request || request.questions.length === 0) return "Waiting for input"; if (request.questions.length === 1) { return request.questions[0]?.header || "1 question"; } return `${request.questions.length} questions`; }; export const OpenCodeQuestionCard = ({ request, state, answers, }: { request: OpenCodeQuestionRequest; state: "pending" | "answered" | "rejected"; answers?: readonly QuestionAnswer[]; }) => { const { replyToQuestion, rejectQuestion } = useOpenCodeRuntimeExtras(); const [formState, setFormState] = useState(() => buildEmptyQuestionState(request), ); const [submissionState, setSubmissionState] = useState< "reply" | "reject" | null >(null); const [error, setError] = useState(null); const isPending = state === "pending"; const encodedAnswers = useMemo( () => encodeQuestionAnswers(request, formState), [formState, request], ); const canSubmit = isPending && encodedAnswers.every((answer) => answer.length > 0); const updateQuestionState = ( index: number, updater: (current: PendingQuestionState) => PendingQuestionState, ) => { setFormState((current) => current.map((item, itemIndex) => itemIndex === index ? updater(item) : item, ), ); }; const submitAnswers = async () => { if (!canSubmit) return; setSubmissionState("reply"); setError(null); try { await replyToQuestion(request.id, encodedAnswers); } catch (err) { setError( err instanceof Error ? err.message : "Failed to submit answers.", ); setSubmissionState(null); } }; const rejectRequest = async () => { setSubmissionState("reject"); setError(null); try { await rejectQuestion(request.id); } catch (err) { setError( err instanceof Error ? err.message : "Failed to reject request.", ); setSubmissionState(null); } }; return (
{isPending ? ( ) : state === "rejected" ? ( ) : ( )}
{isPending ? "Answer required" : state === "rejected" ? "Question dismissed" : "Answers submitted"} {getQuestionSummary(request)}
{request.questions.length === 1 ? "1 prompt" : `${request.questions.length} prompts`}
{request.questions.map((question, index) => { const current = formState[index] ?? { selectedLabels: [], customAnswer: "", }; const answeredValues = answers?.[index] ?? []; const customAllowed = question.custom !== false; return (

{question.header}

{question.question}

{isPending ? ( <> {question.options.length > 0 ? ( question.multiple ? (
{question.options.map((option) => { const checked = current.selectedLabels.includes( option.label, ); const optionId = `${request.id}-${index}-${option.label}`; return (
{ updateQuestionState(index, (item) => ({ ...item, selectedLabels: nextChecked === true ? [...item.selectedLabels, option.label] : item.selectedLabels.filter( (label) => label !== option.label, ), })); }} />

{option.description}

); })}
) : (
{question.options.map((option) => { const selected = current.selectedLabels.includes( option.label, ); return ( ); })}
) ) : null} {customAllowed ? (