Questionnaire
The questions an agent asks before it commits to a plan, as a card in the chat thread. One question per step: the prompt across the top, the answers as bordered rows with a checkbox or a number key, an optional free-text Other row, and a footer with a pill per step next to Previous and Next. Questions slide in the direction of travel while the card animates to the next one's height.
Which harness controls should stay in the top bar?
Installation
Questionnaire is part of BoardUI Pro.
Dark mode ready - this component follows BoardUI semantic tokens automatically. Set up theme switching.
Multiple select
The default mode. Every row is a checkbox card: press anywhere on it to tick it, and as many as you like. Next moves to the next question, Previousgoes back with the ticks intact, and the step pills jump straight to any question. The last question's button reads Done and fires onComplete with every answer.
{/* The default: every row is a checkbox and Next moves on */}<Questionnaire select="multiple" questions={[ { id: "controls", question: "Which harness controls should stay in the top bar?", options: [ { value: "upsell", label: "Show up sell toggle", description: "Forces the upsell card up." }, { value: "wallpaper", label: "Wallpaper picker", description: "The chat wallpaper choices." }, ], other: true, // adds the free-text row; typing ticks it stepLabel: "Top bar", // pill label, "Step 1" otherwise }, ]} onComplete={(answers) => { // answers.controls → { values: ["upsell", "wallpaper"], other: "Model picker" } }}/>Single select
Set select to single on the card or on a question. The checkboxes become number keys, and picking a row, with a click or its digit, moves on by itself after advanceDelay so the choice is seen landing. Coming back shows the pick highlighted. The Other row waits for text instead: Enter sends it.
{/* Single select numbers the rows; a pick moves on after 180ms */}<Questionnaire select="single" advanceDelay={180} questions={[ { id: "permission", question: "How should the permission pill behave while an agent is running?", options: [ { value: "lock", label: "Lock it", description: "Changes wait until the run finishes." }, { value: "queue", label: "Queue the change", description: "Applies from the next turn." }, { value: "apply", label: "Apply immediately", description: "Interrupts the current tool call." }, ], other: { placeholder: "Describe the behaviour" }, }, ]} onComplete={(answers) => { // answers.permission → { values: ["queue"] } or { values: [], other: "..." } }}/>
{/* Mix modes per question: the card-level select is only the default */}<Questionnaire questions={[{ id: "a", select: "multiple", /* … */ }, { id: "b", select: "single", /* … */ }]} />Answers and steps
Answers are keyed by question id: values holds the picked option values in option order, and other the free text while that row is selected. Own them with answers and onAnswersChange, and the visible question with step and onStepChange, or seed either with the default props and let the card run itself. onDismiss shows the corner dismiss; labels rewords the buttons and the Other row.
const [step, setStep] = useState(0);const [answers, setAnswers] = useState<QuestionnaireAnswers>({});
<Questionnaire questions={questions} step={step} onStepChange={setStep} answers={answers} onAnswersChange={setAnswers} onComplete={(final) => submitPlan(final)} onDismiss={() => setOpen(false)} labels={{ previous: "Back", next: "Continue", complete: "Send answers" }}/>
{/* Or start somewhere and let the card own it */}<Questionnaire questions={questions} defaultStep={1} defaultAnswers={{ controls: { values: ["upsell"] } }} />