Block

Questionnaire Card

A one-question-at-a-time card for AI chat apps — designed to overlay the chat composer when an agent needs answers and clarifications from the user. Single-choice questions advance on click, multi-select and free-text questions get explicit pills, and skipped questions fall back to sensible defaults.

base-novaradix-novanew-york-v4 not supported

Dashboard setup

1 of 3
Which timeframe should the dashboard cover?

Installation

npx shadcn@latest add dreambaseai/registry/questionnaire-card

Installs questionnaire-card.tsx into your UI directory alongside the upstream questionnaire and button components for your selected preset. Pin a release with #v0.1.0.

Usage

Questions are plain JSON — single choice, multiple choice (multiple), free text (omit choices), and an optional "other" input (allowOther).

import {  QuestionnaireCard,  type QuestionnaireCardData,} from "@/components/ui/questionnaire-card";
const questionnaire: QuestionnaireCardData = {  id: "dashboard_setup",  title: "Dashboard setup",  questions: [    {      id: "timeframe",      prompt: "Which timeframe should the dashboard cover?",      choices: [        { value: "last_7_days", label: "Last 7 days" },        { value: "decide_for_me", label: "Decide for me" },      ],      defaultValue: "decide_for_me",    },    {      id: "focus",      prompt: "Which areas should it focus on?",      multiple: true,      allowOther: true,      choices: [        { value: "revenue", label: "Revenue" },        { value: "retention", label: "Retention" },      ],    },    { id: "notes", prompt: "Anything else the agent should know?" },  ],};
export function AgentQuestions() {  return (    <QuestionnaireCard      questionnaire={questionnaire}      onSubmit={({ questionnaireId, answers, reason }) => {        // reason is "submit" or "timeout"; skipped questions carry        // their defaultValue (or null).        sendToAgent(questionnaireId, answers, reason);      }}    />  );}

Collapse and reopen

Wire collapsed / onCollapsedChange to let users tuck the card away without losing answers — the form stays mounted while hidden.

Dashboard setup

1 of 3
Which timeframe should the dashboard cover?
const [collapsed, setCollapsed] = useState(false);
<QuestionnaireCard  questionnaire={questionnaire}  onSubmit={handleSubmit}  collapsed={collapsed}  onCollapsedChange={setCollapsed}/>

Opt-in timeout

With timeoutMs set, the card auto-submits whatever the user already answered (defaults fill the rest) and reports reason: "timeout". The demo below uses 65 seconds so you can watch the final-minute countdown appear.

Dashboard setup

1 of 3
Which timeframe should the dashboard cover?
<QuestionnaireCard  questionnaire={questionnaire}  onSubmit={handleSubmit}  // Opt-in: auto-submit collected answers after 10 minutes idle.  // Any interaction resets the countdown; 0 (default) disables it.  timeoutMs={600_000}/>

Disabled state

While disabled, the card is inert, keyboard submits are blocked, and the timeout is paused — it resumes (and fires if overdue) once re-enabled.

Dashboard setup

1 of 3
Which timeframe should the dashboard cover?
Finishing the previous step…

Props

PropTypeDescription
questionnaireQuestionnaireCardDataThe questionnaire to render. Changing its id fully resets the card.
onSubmit(submission: QuestionnaireCardSubmission) => voidCalled once per questionnaire with { questionnaireId, answers, reason }.
timeoutMsnumber = 0Opt-in idle timeout that auto-submits current answers. 0 disables.
disabledboolean = falseRenders the card inert and pauses the timeout (e.g. while a turn is in flight).
collapsedboolean = falseShows a reopen pill instead of the card; answers and timers survive.
onCollapsedChange(collapsed: boolean) => voidEnables the × collapse button and the reopen pill. Omit for read-only cards.
messagesQuestionnaireCardMessagesOverrides for user-facing status text and placeholders.

Source

"use client";
// ============================================// QUESTIONNAIRE CARD// ============================================//// Renders a questionnaire as a compact one-question-at-a-time card, designed// to overlay a chat composer while an AI agent waits for clarifications.// Single-choice questions advance on click (the last one submits);// multi-select and free-text questions get explicit Next/Submit pills.// Answers are collected from the form's FormData on submit and overlaid on// each question's defaultValue so skipped questions still carry a sensible// answer.//// The × collapses the card to a reopen pill without submitting; answers and// the auto-submit timer persist across the toggle. The opt-in timeout submits// the current answers (defaults fill the rest) whether open or collapsed, and// resets whenever the user interacts with the form.
import { useCallback, useEffect, useRef, useState } from "react";import {  ArrowRightIcon,  ChevronLeftIcon,  ChevronRightIcon,  ChevronUpIcon,  CircleCheckIcon,  ClockIcon,  MessagesSquareIcon,  PencilIcon,  XIcon,} from "lucide-react";import { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";
import { Button } from "@/components/ui/button";import {  Questionnaire,  QuestionnaireChoice,  QuestionnaireChoiceDescription,  QuestionnaireChoices,  QuestionnaireDescription,  QuestionnaireError,  QuestionnaireInput,  QuestionnaireItem,  QuestionnaireNext,  QuestionnairePrevious,  QuestionnaireProgress,  QuestionnaireSkip,  QuestionnaireSubmit,  QuestionnaireTitle,} from "@/components/ui/questionnaire";
// Package-level class merging keeps this file free of consumer aliases like// `@/lib/utils`.function cn(...inputs: ClassValue[]) {  return twMerge(clsx(inputs));}
// ============================================// TYPES// ============================================
export interface QuestionnaireCardChoice {  /** Stable machine value. Used as the answer value. */  value: string;  /** Human-readable label shown to the user. */  label: string;  /** Optional muted one-liner shown next to the label. */  description?: string;}
export interface QuestionnaireCardQuestion {  /** Stable id — becomes the key in the answers record. */  id: string;  /** Short, clear question shown as the item title. */  prompt: string;  /** Optional help text (visually hidden, exposed to screen readers). */  description?: string;  /** If true, the user may pick any subset of choices. */  multiple?: boolean;  /** If true, a free-text "other" input is shown alongside the choices. */  allowOther?: boolean;  /** Omit for a free-text question. */  choices?: QuestionnaireCardChoice[];  /** Fallback answer used when the question is skipped or times out. */  defaultValue?: string | string[] | null;}
export interface QuestionnaireCardData {  /** Unique id for this questionnaire. Changing it resets the card. */  id: string;  /** Accessible title (visually hidden). */  title: string;  description?: string;  questions: QuestionnaireCardQuestion[];}
export type QuestionnaireCardAnswerValue = string | string[] | null;
export type QuestionnaireCardAnswers = Record<  string,  QuestionnaireCardAnswerValue>;
export type QuestionnaireCardSubmitReason = "submit" | "timeout";
export interface QuestionnaireCardSubmission {  questionnaireId: string;  answers: QuestionnaireCardAnswers;  /** "submit" for user-driven submits, "timeout" for the auto-submit path. */  reason: QuestionnaireCardSubmitReason;}
export interface QuestionnaireCardMessages {  /** Reopen pill label while collapsed. Default: "Continue questions". */  continueLabel?: string;  /** Countdown notice; `{countdown}` is replaced, e.g. "45s" or "2m". */  autoSubmitNotice?: string;  /** Notice shown while `disabled`. Default: "Waiting…". */  disabledNotice?: string;  /** Hint appended to multi-select titles. Default: "Select all that apply". */  multipleHint?: string;  /** Free-text placeholder when the question also has choices. */  otherPlaceholder?: string;  /** Free-text placeholder for questions without choices. */  freeTextPlaceholder?: string;  /** aria-label for the free-text input. Default: "Another answer". */  otherInputLabel?: string;}
export interface QuestionnaireCardProps {  questionnaire: QuestionnaireCardData;  onSubmit: (submission: QuestionnaireCardSubmission) => void;  /** Auto-submit timeout in ms. 0 (default) disables the timeout. */  timeoutMs?: number;  /** While true, the card is non-interactive and the timeout is paused. */  disabled?: boolean;  /** Collapsed shows a reopen pill in place of the card; the form stays   * mounted (hidden) so answers and the auto-submit timer survive. */  collapsed?: boolean;  /** Enables the × collapse button and the reopen pill. When omitted   * (e.g. a read-only transcript card), the × is not rendered. */  onCollapsedChange?: (collapsed: boolean) => void;  /** Override user-facing status and placeholder text. */  messages?: QuestionnaireCardMessages;  className?: string;}
// Only show the countdown hint in the final minute.const COUNTDOWN_VISIBLE_MS = 60_000;
// Brief pause after a single-choice click so the selection registers visually// before the card advances.const AUTO_ADVANCE_DELAY_MS = 150;
const DEFAULT_MESSAGES = {  continueLabel: "Continue questions",  autoSubmitNotice: "Auto-submits in {countdown}",  disabledNotice: "Waiting…",  multipleHint: "Select all that apply",  otherPlaceholder: "Or type another answer…",  freeTextPlaceholder: "Type your answer…",  otherInputLabel: "Another answer",} as const satisfies Required<QuestionnaireCardMessages>;
// Pill-shaped choice row: the letter-shortcut badge leads the row and doubles// as the selection indicator; label + description share one truncating line// and an arrow slides in on hover/selection.const CHOICE_CLASSNAME = cn(  "min-h-0 items-center rounded-full border-0 px-2 py-2 dark:bg-transparent",  "hover:bg-muted/60 data-checked:bg-muted dark:data-checked:bg-muted",  "[&_[data-slot=questionnaire-choice-indicator]]:hidden",  "[&_[data-slot=questionnaire-choice-shortcut]]:order-first [&_[data-slot=questionnaire-choice-shortcut]]:ms-0 [&_[data-slot=questionnaire-choice-shortcut]]:size-7 [&_[data-slot=questionnaire-choice-shortcut]]:translate-y-0 [&_[data-slot=questionnaire-choice-shortcut]]:rounded-full [&_[data-slot=questionnaire-choice-shortcut]]:text-[11px]",  "data-checked:[&_[data-slot=questionnaire-choice-shortcut]]:border-transparent data-checked:[&_[data-slot=questionnaire-choice-shortcut]]:bg-transparent data-checked:[&_[data-slot=questionnaire-choice-shortcut]]:text-primary",  "[&_[data-slot=questionnaire-choice-label]]:flex-row [&_[data-slot=questionnaire-choice-label]]:items-center [&_[data-slot=questionnaire-choice-label]]:gap-2",);
const HEADER_BUTTON_CLASSNAME =  "text-muted-foreground min-h-0 size-6 rounded-full sm:min-h-0";
const PILL_BUTTON_CLASSNAME = "min-h-0 rounded-full sm:min-h-0";
function defaultAnswerFor(  question: QuestionnaireCardQuestion,): QuestionnaireCardAnswerValue {  if (question.defaultValue !== undefined) {    return question.defaultValue;  }  return question.multiple ? [] : null;}
/** Build the answers payload from defaults alone (timeout with no input). */export function buildDefaultAnswers(  questionnaire: QuestionnaireCardData,): QuestionnaireCardAnswers {  const answers: QuestionnaireCardAnswers = {};  for (const question of questionnaire.questions) {    answers[question.id] = defaultAnswerFor(question);  }  return answers;}
/** * Overlay the submitted FormData on per-question defaults. Choices and the * free-text input share the question id as their field name; entries not * matching a known choice value are free text. Free text is appended for * `multiple` questions and overrides the selection for single-choice ones. */export function collectAnswers(  questionnaire: QuestionnaireCardData,  formData: FormData,): QuestionnaireCardAnswers {  const answers: QuestionnaireCardAnswers = {};  for (const question of questionnaire.questions) {    const entries = formData      .getAll(question.id)      .filter((v): v is string => typeof v === "string" && v.trim().length > 0);
    if (entries.length === 0) {      answers[question.id] = defaultAnswerFor(question);      continue;    }
    if (!question.choices?.length) {      answers[question.id] = entries[0];      continue;    }
    const choiceValues = new Set(question.choices.map((c) => c.value));    const selected = entries.filter((v) => choiceValues.has(v));    const other = entries.filter((v) => !choiceValues.has(v));
    if (question.multiple) {      answers[question.id] = [...selected, ...other];    } else {      answers[question.id] = other[0] ?? selected[0] ?? null;    }  }  return answers;}
/** True when the question needs explicit Next/Submit (no click-to-advance). */function needsExplicitAdvance(question: QuestionnaireCardQuestion): boolean {  return Boolean(question.multiple) || !question.choices?.length;}
export function QuestionnaireCard({  questionnaire,  onSubmit,  timeoutMs = 0,  disabled = false,  collapsed = false,  onCollapsedChange,  messages,  className,}: QuestionnaireCardProps) {  const text = { ...DEFAULT_MESSAGES, ...messages };  const submittedRef = useRef(false);  const formRef = useRef<HTMLFormElement>(null);  const autoNextRef = useRef<HTMLButtonElement>(null);  const autoAdvanceTimerRef = useRef<number | null>(null);  const activeNameRef = useRef<string>(questionnaire.questions[0]?.id ?? "");  const [activeName, setActiveName] = useState<string>(    questionnaire.questions[0]?.id ?? "",  );  const [otherValues, setOtherValues] = useState<Record<string, string>>({});
  const clearAutoAdvance = useCallback(() => {    if (autoAdvanceTimerRef.current === null) {      return;    }    window.clearTimeout(autoAdvanceTimerRef.current);    autoAdvanceTimerRef.current = null;  }, []);
  useEffect(() => clearAutoAdvance, [clearAutoAdvance]);
  const submitAnswers = useCallback(    (      answers: QuestionnaireCardAnswers,      reason: QuestionnaireCardSubmitReason,    ) => {      // The disabled guard backs up the inert wrapper — keyboard-driven      // submits (Enter, shortcuts) must not slip through while paused.      if (submittedRef.current || disabled) {        return;      }      submittedRef.current = true;      onSubmit({ questionnaireId: questionnaire.id, answers, reason });    },    [questionnaire.id, onSubmit, disabled],  );
  const handleFormSubmit = useCallback(    (event: React.FormEvent<HTMLFormElement>) => {      event.preventDefault();      submitAnswers(        collectAnswers(questionnaire, new FormData(event.currentTarget)),        "submit",      );    },    [questionnaire, submitAnswers],  );
  // Timeout auto-submit: keep whatever the user already selected/typed,  // defaults fill only the unanswered questions. Runs open or collapsed —  // the hidden form still yields its FormData.  const submitWithCurrentAnswers = useCallback(() => {    const form = formRef.current;    submitAnswers(      form        ? collectAnswers(questionnaire, new FormData(form))        : buildDefaultAnswers(questionnaire),      "timeout",    );  }, [questionnaire, submitAnswers]);
  // Click-to-advance for single-choice questions: selecting a choice moves to  // the next question; on the last question it submits the whole form.  const handleChoiceSelected = useCallback(    (question: QuestionnaireCardQuestion) => {      clearAutoAdvance();      if (        needsExplicitAdvance(question) ||        otherValues[question.id]?.trim() ||        disabled      ) {        return;      }      const originatingQuestionId = question.id;      const timeoutId = window.setTimeout(() => {        if (autoAdvanceTimerRef.current !== timeoutId) {          return;        }        autoAdvanceTimerRef.current = null;        if (          submittedRef.current ||          activeNameRef.current !== originatingQuestionId        ) {          return;        }        const lastId =          questionnaire.questions[questionnaire.questions.length - 1]?.id;        if (activeNameRef.current === lastId) {          formRef.current?.requestSubmit();        } else {          autoNextRef.current?.click();        }      }, AUTO_ADVANCE_DELAY_MS);      autoAdvanceTimerRef.current = timeoutId;    },    [questionnaire, disabled, otherValues, clearAutoAdvance],  );
  const handleOtherValueChange = useCallback(    (questionId: string, value: string) => {      setOtherValues((current) => ({ ...current, [questionId]: value }));      if (value.trim()) {        clearAutoAdvance();      }    },    [clearAutoAdvance],  );
  // ============================================  // AUTO-SUBMIT TIMEOUT (opt-in via timeoutMs > 0)  // ============================================  const [remainingMs, setRemainingMs] = useState<number>(timeoutMs);  // Seeded in the tick effect (kept out of render — `Date.now()` is impure).  const deadlineRef = useRef<number>(0);
  // Any form interaction (choice change, typing, question navigation) resets  // the deadline.  const resetDeadline = useCallback(() => {    if (timeoutMs <= 0) {      return;    }    deadlineRef.current = Date.now() + timeoutMs;    setRemainingMs(timeoutMs);  }, [timeoutMs]);
  const handleItemChange = useCallback(    (name: string) => {      clearAutoAdvance();      activeNameRef.current = name;      setActiveName(name);      resetDeadline();    },    [resetDeadline, clearAutoAdvance],  );
  // Replacing the questionnaire (new id) resets every piece of interaction  // state: submission guard, active question, free-text values, timers.  const previousIdRef = useRef(questionnaire.id);  useEffect(() => {    if (previousIdRef.current === questionnaire.id) {      return;    }    previousIdRef.current = questionnaire.id;    submittedRef.current = false;    clearAutoAdvance();    const firstId = questionnaire.questions[0]?.id ?? "";    activeNameRef.current = firstId;    setActiveName(firstId);    setOtherValues({});    deadlineRef.current = 0;    setRemainingMs(timeoutMs);  }, [questionnaire, timeoutMs, clearAutoAdvance]);
  // Tick the countdown every second.  useEffect(() => {    if (timeoutMs <= 0) {      return;    }    // Seed the deadline on first run (0 = unset); preserved across `disabled`    // toggles so pausing doesn't restart the countdown.    if (deadlineRef.current === 0) {      deadlineRef.current = Date.now() + timeoutMs;    }    const id = setInterval(() => {      // Pause while disabled; the deadline resumes (and fires if already      // past) once the card is interactive again.      if (submittedRef.current || disabled) {        return;      }      const remaining = deadlineRef.current - Date.now();      if (remaining <= 0) {        submitWithCurrentAnswers();        setRemainingMs(0);        return;      }      setRemainingMs(remaining);    }, 1000);    return () => clearInterval(id);  }, [timeoutMs, submitWithCurrentAnswers, disabled, questionnaire.id]);
  const showCountdown =    timeoutMs > 0 && remainingMs > 0 && remainingMs <= COUNTDOWN_VISIBLE_MS;  const countdownSeconds = Math.max(0, Math.ceil(remainingMs / 1000));  const countdownLabel =    countdownSeconds >= 60      ? `${Math.ceil(countdownSeconds / 60)}m`      : `${countdownSeconds}s`;  const autoSubmitNotice = text.autoSubmitNotice.replace(    "{countdown}",    countdownLabel,  );
  const activeIndex =    Math.max(      0,      questionnaire.questions.findIndex((q) => q.id === activeName),    ) + 1;
  return (    <div      className={cn(disabled && "pointer-events-none opacity-60", className)}      aria-disabled={disabled}      inert={disabled || undefined}    >      {collapsed && onCollapsedChange ? (        <div className="pointer-events-auto flex justify-end">          <Button            type="button"            variant="secondary"            size="sm"            disabled={disabled}            className="rounded-full shadow-lg"            onClick={() => onCollapsedChange(false)}          >            <MessagesSquareIcon data-icon="inline-start" />            {text.continueLabel}            <span>              {activeIndex} of {questionnaire.questions.length}            </span>            {showCountdown ? <span>&middot; {autoSubmitNotice}</span> : null}            <ChevronUpIcon data-icon="inline-end" />          </Button>        </div>      ) : null}
      {/* `hidden` (not unmount) while collapsed — the form's answers and the          running auto-submit timer must survive the toggle. */}      <div        hidden={collapsed}        className="border-border bg-popover relative w-full rounded-2xl border p-4 shadow-xl"      >        <h4 className="sr-only">{questionnaire.title}</h4>
        <Questionnaire          items={questionnaire.questions.map((q) => ({            name: q.id,            required: false,            choices: q.choices?.map((c) => ({ value: c.value })),          }))}          ref={formRef}          shortcuts="letters"          onSubmit={handleFormSubmit}          onItemChange={handleItemChange}          onChange={resetDeadline}          className="gap-3"        >          {/* Progress + navigation cluster, pinned to the card's top-right. */}          <div className="absolute top-4 right-4 flex items-center gap-0.5">            <QuestionnairePrevious              variant="ghost"              size="icon"              aria-label="Previous question"              disabled={disabled}              className={HEADER_BUTTON_CLASSNAME}            >              <ChevronLeftIcon className="size-3.5" />            </QuestionnairePrevious>            <QuestionnaireProgress              className="text-muted-foreground min-w-0 px-1 text-xs tabular-nums"              render={(props, state) => (                <div {...(props as React.HTMLAttributes<HTMLDivElement>)}>                  {state.current} of {state.total}                </div>              )}            />            <QuestionnaireNext              variant="ghost"              size="icon"              aria-label="Next question"              disabled={disabled}              className={HEADER_BUTTON_CLASSNAME}            >              <ChevronRightIcon className="size-3.5" />            </QuestionnaireNext>            {onCollapsedChange ? (              <Button                type="button"                variant="ghost"                size="icon"                disabled={disabled}                className={HEADER_BUTTON_CLASSNAME}                onClick={() => onCollapsedChange(true)}                aria-label="Hide questionnaire"              >                <XIcon className="size-3.5" />              </Button>            ) : null}          </div>
          {/* Invisible Next used by click-to-advance; on the last question the            form is submitted directly instead. */}          <QuestionnaireNext            ref={autoNextRef}            tabIndex={-1}            aria-hidden            disabled={disabled}            className="hidden"          />
          {questionnaire.questions.map((question) => (            <QuestionnaireItem              key={question.id}              name={question.id}              multiple={question.multiple}              required={false}              className="gap-3"            >              <QuestionnaireTitle className="flex flex-col gap-0.5 pr-28 text-sm font-semibold lg:flex-row lg:items-end lg:gap-2">                {question.prompt}                {question.multiple ? (                  <span className="text-muted-foreground text-xs font-normal">                    {text.multipleHint}                  </span>                ) : null}              </QuestionnaireTitle>              {question.description ? (                <QuestionnaireDescription className="sr-only">                  {question.description}                </QuestionnaireDescription>              ) : null}              {question.choices?.length ? (                <QuestionnaireChoices className="gap-0.5">                  {question.choices.map((choice) => (                    <QuestionnaireChoice                      key={choice.value}                      value={choice.value}                      title={                        choice.label +                        (choice.description ? " - " + choice.description : "")                      }                      onChange={() => handleChoiceSelected(question)}                      className={cn(                        "truncate",                        CHOICE_CLASSNAME,                        // Square badges signal checkbox semantics; circles stay                        // for single choice.                        question.multiple &&                          "**:data-[slot=questionnaire-choice-shortcut]:rounded-md",                      )}                    >                      <span className="font-medium whitespace-nowrap">                        {choice.label}                      </span>                      {choice.description ? (                        <QuestionnaireChoiceDescription className="truncate">                          {choice.description}                        </QuestionnaireChoiceDescription>                      ) : null}                      {question.multiple ? (                        <CircleCheckIcon className="text-muted-foreground group-data-checked/questionnaire-choice:text-primary ms-auto me-2 size-4 shrink-0 opacity-0 transition-opacity group-hover/questionnaire-choice:opacity-100 group-data-checked/questionnaire-choice:opacity-100" />                      ) : (                        <ArrowRightIcon className="text-muted-foreground ms-auto me-2 size-4 shrink-0 opacity-0 transition-opacity group-hover/questionnaire-choice:opacity-100 group-data-checked/questionnaire-choice:opacity-100" />                      )}                    </QuestionnaireChoice>                  ))}                </QuestionnaireChoices>              ) : null}
              {/* Footer row: free-text input (pencil) + Skip, plus explicit                Next/Submit for multi-select and free-text questions. */}              <div className="flex items-center gap-2.5 px-2">                {question.allowOther || !question.choices?.length ? (                  <>                    <span                      aria-hidden="true"                      className="border-input text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-full border"                    >                      <PencilIcon className="size-3.5" />                    </span>                    <QuestionnaireInput                      aria-label={text.otherInputLabel}                      value={otherValues[question.id] ?? ""}                      onChange={(event) =>                        handleOtherValueChange(question.id, event.target.value)                      }                      className="h-8 min-h-8 border-0 bg-transparent px-0 shadow-none focus-visible:border-0 focus-visible:ring-0 sm:min-h-0 dark:bg-transparent"                      placeholder={                        question.choices?.length                          ? text.otherPlaceholder                          : text.freeTextPlaceholder                      }                    />                  </>                ) : (                  <div className="min-w-0 flex-1" />                )}                <QuestionnaireSkip                  variant="outline"                  size="sm"                  disabled={disabled}                  className={PILL_BUTTON_CLASSNAME}                />                {needsExplicitAdvance(question) ||                otherValues[question.id]?.trim() ? (                  <>                    <QuestionnaireNext                      variant="secondary"                      size="sm"                      disabled={disabled}                      className={PILL_BUTTON_CLASSNAME}                    />                    <QuestionnaireSubmit                      variant="secondary"                      size="sm"                      disabled={disabled}                      className={PILL_BUTTON_CLASSNAME}                    />                  </>                ) : null}              </div>              <QuestionnaireError className="mt-0 px-2 text-xs" />            </QuestionnaireItem>          ))}        </Questionnaire>
        {disabled ? (          <div className="text-muted-foreground mt-2 flex items-center justify-end gap-1.5 text-xs">            <ClockIcon className="size-3" />            <span>{text.disabledNotice}</span>          </div>        ) : showCountdown ? (          <div className="text-muted-foreground mt-2 flex items-center justify-end gap-1.5 text-xs">            <ClockIcon className="size-3" />            <span>{autoSubmitNotice}</span>          </div>        ) : null}      </div>    </div>  );}