import { useMemo, useState } from 'react'; import { useT } from '../i18n'; import type { DirectionCard, QuestionForm } from '../artifacts/question-form'; import { formatFormAnswers } from '../artifacts/question-form'; interface Props { form: QuestionForm; // Whether the user can still submit answers. The owning AssistantMessage // disables the form when the assistant turn is no longer the most recent // one (i.e. the user has already moved past it). interactive: boolean; // Pre-existing answers — when we detect a follow-up user message that // begins with "[form answers — ]", we parse it back out and pass it // here so the rendered form reflects what was sent. submittedAnswers?: Record; onSubmit?: (text: string, answers: Record) => void; } export function QuestionFormView({ form, interactive, submittedAnswers, onSubmit }: Props) { const t = useT(); const initial = useMemo(() => buildInitialState(form, submittedAnswers), [form, submittedAnswers]); const [answers, setAnswers] = useState>(initial); const locked = !interactive || !onSubmit || submittedAnswers !== undefined; function update(id: string, value: string | string[]) { if (locked) return; setAnswers((prev) => ({ ...prev, [id]: value })); } function toggleCheckbox(id: string, option: string, maxSelections?: number) { if (locked) return; setAnswers((prev) => { const current = Array.isArray(prev[id]) ? (prev[id] as string[]) : []; const has = current.includes(option); if (!has && maxSelections !== undefined && current.length >= maxSelections) { return prev; } const next = has ? current.filter((v) => v !== option) : [...current, option]; return { ...prev, [id]: next }; }); } function missingRequired(): string | null { for (const q of form.questions) { if (!q.required) continue; const v = answers[q.id]; if (Array.isArray(v) ? v.length === 0 : !(typeof v === 'string' && v.trim().length > 0)) { return q.label; } } return null; } function handleSubmit() { if (locked || !onSubmit) return; if (!withinSelectionLimits) return; const missing = missingRequired(); if (missing) { // Soft inline guard — surface via aria but don't alert; the disabled // state of the submit button covers most cases. return; } onSubmit(formatFormAnswers(form, answers), answers); } const required = form.questions.filter((q) => q.required); const withinSelectionLimits = form.questions.every((q) => { if (q.type !== 'checkbox' || q.maxSelections === undefined) return true; const v = answers[q.id]; return !Array.isArray(v) || v.length <= q.maxSelections; }); const ready = withinSelectionLimits && required.every((q) => { const v = answers[q.id]; return Array.isArray(v) ? v.length > 0 : typeof v === 'string' && v.trim().length > 0; }); return (
?
{form.title}
{form.description ? (
{form.description}
) : null}
{locked ? {t('qf.answered')} : null}
{form.questions.map((q) => { const value = answers[q.id]; return (
{q.help ?
{q.help}
: null} {q.type === 'radio' && q.options ? (
{q.options.map((opt) => ( ))}
) : null} {q.type === 'checkbox' && q.options ? (
{q.options.map((opt) => { const arr = Array.isArray(value) ? value : []; const on = arr.includes(opt); const maxed = q.maxSelections !== undefined && !on && arr.length >= q.maxSelections; return ( ); })}
) : null} {q.type === 'select' && q.options ? ( ) : null} {q.type === 'text' ? ( update(q.id, e.target.value)} /> ) : null} {q.type === 'textarea' ? (