Inkbox

Component

Questionnaire

Questions asked one at a time: white choice cards with the brand's radio or checkbox and a shortcut key, a typed answer, a progress count and Previous, Skip and Next pills.

npx -y github:anwarlemu/inkbox-brand-cli#release add questionnaire
Preview
Question 1 of 3
Where should @acme tell you about new mail?

Choose a channel, or type another one.

Disabled
Which iMessage line should the agent use?

No dedicated lines are available to assign right now.

Usage

Use when an agent or a setup flow needs a few answers in a row (how to route calls, what to include in a summary). Pass the questions as items to Questionnaire, then render a QuestionnaireItem per question with a QuestionnaireTitle, a QuestionnaireDescription, QuestionnaireChoices of QuestionnaireChoice (add QuestionnaireInput for another answer) and a QuestionnaireError; end with QuestionnaireActions holding Previous, Skip, Next and Submit. Mark required questions; set multiple to allow several answers. onSubmit receives the native form event: read the answers with FormData.

Avoid

  • More than one question on screen
  • Animating between questions
  • A required question without its error message
  • A purple card fill for a chosen answer

Exports: Questionnaire, QuestionnaireActions, QuestionnaireChoice, QuestionnaireChoiceDescription, QuestionnaireChoices, QuestionnaireDescription, QuestionnaireError, QuestionnaireInput, QuestionnaireItem, QuestionnaireNext, QuestionnairePrevious, QuestionnaireProgress, QuestionnaireSkip, QuestionnaireSubmit, QuestionnaireTitle. This component is a client component.

Dependencies: button, input, kbd, progress, @phosphor-icons/react.

Example

examples/questionnaire.tsx
"use client";​import * as React from "react";​import {  Questionnaire,  QuestionnaireActions,  QuestionnaireChoice,  QuestionnaireChoiceDescription,  QuestionnaireChoices,  QuestionnaireDescription,  QuestionnaireError,  QuestionnaireInput,  QuestionnaireItem,  QuestionnaireNext,  QuestionnairePrevious,  QuestionnaireProgress,  QuestionnaireSkip,  QuestionnaireSubmit,  QuestionnaireTitle,} from "@/components/ui/questionnaire";​const ITEMS = [  { name: "channel", required: true, choices: [{ value: "email" }, { value: "phone" }, { value: "imessage" }] },  { name: "include", multiple: true, choices: [{ value: "sender" }, { value: "summary" }, { value: "attachments" }, { value: "next-step" }] },  { name: "when", required: true, choices: [{ value: "now" }, { value: "hourly" }, { value: "daily" }] },] as const;​function Saved({ answers }: { answers: string }) {  return (    <p role="status" className="rounded-control bg-success-bg px-3.5 py-2.5 text-body text-fg">      Saved. {answers}    </p>  );}​export function Preview() {  const [saved, setSaved] = React.useState<string>();  return (    <div className="mx-auto grid w-full max-w-110 gap-4">      <Questionnaire        items={ITEMS}        onSubmit={(event) => {          event.preventDefault();          const data = new FormData(event.currentTarget);          setSaved(`Channel: ${data.get("channel")}. Include: ${data.getAll("include").join(", ") || "nothing"}. When: ${data.get("when")}.`);        }}      >        <QuestionnaireProgress />        <QuestionnaireItem name="channel" required>          <QuestionnaireTitle>Where should @acme tell you about new mail?</QuestionnaireTitle>          <QuestionnaireDescription>Choose a channel, or type another one.</QuestionnaireDescription>          <QuestionnaireChoices>            <QuestionnaireChoice value="email">              <span className="font-medium">Email</span>              <QuestionnaireChoiceDescription>A note to robert@acme.com</QuestionnaireChoiceDescription>            </QuestionnaireChoice>            <QuestionnaireChoice value="phone">              <span className="font-medium">Phone</span>              <QuestionnaireChoiceDescription>A text from the agent&apos;s number</QuestionnaireChoiceDescription>            </QuestionnaireChoice>            <QuestionnaireChoice value="imessage">              <span className="font-medium">iMessage</span>              <QuestionnaireChoiceDescription>Through the shared iMessage line</QuestionnaireChoiceDescription>            </QuestionnaireChoice>            <QuestionnaireInput aria-label="Another channel" placeholder="Another channel" />          </QuestionnaireChoices>          <QuestionnaireError />        </QuestionnaireItem>        <QuestionnaireItem name="include" multiple>          <QuestionnaireTitle>What should each note include?</QuestionnaireTitle>          <QuestionnaireDescription>Choose any, or skip this question.</QuestionnaireDescription>          <QuestionnaireChoices>            <QuestionnaireChoice value="sender">The sender</QuestionnaireChoice>            <QuestionnaireChoice value="summary">A one-line summary</QuestionnaireChoice>            <QuestionnaireChoice value="attachments">Attachments</QuestionnaireChoice>            <QuestionnaireChoice value="next-step">The suggested next step</QuestionnaireChoice>          </QuestionnaireChoices>          <QuestionnaireError />        </QuestionnaireItem>        <QuestionnaireItem name="when" required>          <QuestionnaireTitle>How often?</QuestionnaireTitle>          <QuestionnaireDescription>Batched notes arrive once per period.</QuestionnaireDescription>          <QuestionnaireChoices>            <QuestionnaireChoice value="now">As each email arrives</QuestionnaireChoice>            <QuestionnaireChoice value="hourly">Once an hour</QuestionnaireChoice>            <QuestionnaireChoice value="daily">Once a day</QuestionnaireChoice>          </QuestionnaireChoices>          <QuestionnaireError />        </QuestionnaireItem>        <QuestionnaireActions>          <QuestionnairePrevious />          <QuestionnaireSkip />          <QuestionnaireNext />          <QuestionnaireSubmit>Save rule</QuestionnaireSubmit>        </QuestionnaireActions>      </Questionnaire>      {saved && <Saved answers={saved} />}    </div>  );}​const PLAN = [{ name: "line", required: true, choices: [{ value: "shared" }, { value: "dedicated" }, { value: "port" }] }] as const;​export function Disabled() {  return (    <Questionnaire items={PLAN} shortcuts="letters" className="mx-auto max-w-110" onSubmit={(event) => event.preventDefault()}>      <QuestionnaireItem name="line" required>        <QuestionnaireTitle>Which iMessage line should the agent use?</QuestionnaireTitle>        <QuestionnaireDescription>No dedicated lines are available to assign right now.</QuestionnaireDescription>        <QuestionnaireChoices>          <QuestionnaireChoice value="shared">            <span className="font-medium">Shared</span>            <QuestionnaireChoiceDescription>Assigned when someone connects</QuestionnaireChoiceDescription>          </QuestionnaireChoice>          <QuestionnaireChoice value="dedicated" disabled>            <span className="font-medium">Dedicated</span>            <QuestionnaireChoiceDescription>None available</QuestionnaireChoiceDescription>          </QuestionnaireChoice>          <QuestionnaireChoice value="port">            <span className="font-medium">Port a number</span>            <QuestionnaireChoiceDescription>Bring an existing number</QuestionnaireChoiceDescription>          </QuestionnaireChoice>        </QuestionnaireChoices>        <QuestionnaireError />      </QuestionnaireItem>      <QuestionnaireActions>        <QuestionnaireSubmit>Assign line</QuestionnaireSubmit>      </QuestionnaireActions>    </Questionnaire>  );}

Source

components/ui/questionnaire.tsx
"use client";​import * as React from "react";import { CheckIcon } from "@phosphor-icons/react/ssr";​import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Kbd } from "@/components/ui/kbd";import { Progress } from "@/components/ui/progress";import { cn } from "@/lib/utils";​// A short set of questions asked one at a time (extrapolated from the console, Figma 427:2715: Create Agent Identity's// choices and the Where does this agent run? picker). A choice is a 44px white card with the field edge and 8px// corners, the brand's radio (a thick pink ring when chosen) or checkbox (purple when checked) 14px in, a 14px ink label// and its number or letter key at the right; a chosen card takes the purple edge. The question is 16px medium ink with// a 14px secondary line under it; progress is a 12px muted count over a thin purple bar. Changing questions is instant:// it is driven by the keyboard as often as by the pointer. The answers submit as one native form (FormData), every// question's inputs included, so the page owns saving and what happens next.​type Choice = { readonly value: string; readonly disabled?: boolean };type ItemConfig = { readonly name: string; readonly required?: boolean; readonly multiple?: boolean; readonly choices?: readonly Choice[] };type Shortcuts = "numbers" | "letters" | "none";​type QuestionnaireContextValue = {  order: string[];  active: string | undefined;  goTo: (name: string) => void;  shortcuts: Shortcuts;  answers: Record<string, string[]>;  other: Record<string, string>;  errors: Record<string, string | undefined>;  configs: React.RefObject<Map<string, ItemConfig>>;  register: (config: ItemConfig) => () => void;  setAnswer: (name: string, value: string, checked: boolean) => void;  setOther: (name: string, text: string) => void;  next: () => void;  previous: () => void;  skip: () => void;};​const QuestionnaireContext = React.createContext<QuestionnaireContextValue | null>(null);const ItemContext = React.createContext<{ name: string; required: boolean; multiple: boolean; id: string } | null>(null);​function useQuestionnaire() {  const context = React.useContext(QuestionnaireContext);  if (!context) throw new Error("Questionnaire parts must be inside a Questionnaire.");  return context;}​function useItem() {  const context = React.useContext(ItemContext);  if (!context) throw new Error("This part must be inside a QuestionnaireItem.");  return context;}​const shortcutKey = (index: number, shortcuts: Shortcuts) => (shortcuts === "none" || index > 8 ? undefined : shortcuts === "letters" ? String.fromCharCode(65 + index) : String(index + 1));​/** * The questions and their answers. Pass `items` (name, required, multiple, choices) so the order, progress and shortcuts * are known on the server; without it the order is the QuestionnaireItems' order. `defaultItem` or a controlled * `item` with `onItemChange` picks the question shown. `shortcuts` puts 1 to 9 (or A to I) on the choices. */function Questionnaire({  items,  defaultItem,  item,  onItemChange,  shortcuts = "numbers",  className,  onSubmit,  onKeyDown,  children,  ...props}: Omit<React.ComponentProps<"form">, "onSubmit"> & {  items?: readonly ItemConfig[];  defaultItem?: string;  item?: string;  onItemChange?: (name: string) => void;  shortcuts?: Shortcuts;  onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void;}) {  const form = React.useRef<HTMLFormElement>(null);  const configs = React.useRef(new Map<string, ItemConfig>(items?.map((i) => [i.name, i])));  const [registered, setRegistered] = React.useState<string[]>([]);  const order = items ? items.map((i) => i.name) : registered;  const [internal, setInternal] = React.useState(defaultItem ?? items?.[0]?.name);  const active = item ?? internal ?? order[0];  const [answers, setAnswers] = React.useState<Record<string, string[]>>({});  const [other, setOtherState] = React.useState<Record<string, string>>({});  const [errors, setErrors] = React.useState<Record<string, string | undefined>>({});​  const focusItem = React.useCallback((name: string) => {    requestAnimationFrame(() => {      const fieldset = form.current?.querySelector<HTMLElement>(`[data-slot=questionnaire-item][data-name="${CSS.escape(name)}"]`);      fieldset?.querySelector<HTMLElement>("input:not(:disabled)")?.focus({ preventScroll: true });    });  }, []);​  const goTo = React.useCallback(    (name: string) => {      if (item === undefined) setInternal(name);      onItemChange?.(name);      focusItem(name);    },    [item, onItemChange, focusItem],  );​  const register = React.useCallback((config: ItemConfig) => {    configs.current.set(config.name, { ...configs.current.get(config.name), ...config });    setRegistered((list) => (list.includes(config.name) ? list : [...list, config.name]));    return () => setRegistered((list) => list.filter((n) => n !== config.name));  }, []);​  const answered = React.useCallback((name: string) => (answers[name]?.length ?? 0) > 0 || (other[name] ?? "").trim() !== "", [answers, other]);​  const validate = React.useCallback(    (name: string) => {      const config = configs.current.get(name);      const message = config?.required && !answered(name) ? (config.multiple ? "Choose at least one answer to continue." : "Choose an answer to continue.") : undefined;      setErrors((e) => ({ ...e, [name]: message }));      return !message;    },    [answered],  );​  const setAnswer = React.useCallback((name: string, value: string, checked: boolean) => {    const multiple = configs.current.get(name)?.multiple;    setAnswers((a) => ({ ...a, [name]: multiple ? (checked ? [...(a[name] ?? []).filter((v) => v !== value), value] : (a[name] ?? []).filter((v) => v !== value)) : checked ? [value] : [] }));    if (!multiple && checked) setOtherState((o) => ({ ...o, [name]: "" }));    setErrors((e) => ({ ...e, [name]: undefined }));  }, []);​  const setOther = React.useCallback((name: string, text: string) => {    setOtherState((o) => ({ ...o, [name]: text }));    if (text && !configs.current.get(name)?.multiple) setAnswers((a) => ({ ...a, [name]: [] }));    setErrors((e) => ({ ...e, [name]: undefined }));  }, []);​  const index = active ? order.indexOf(active) : -1;  const next = React.useCallback(() => {    if (!active || !validate(active)) return focusItem(active ?? "");    const following = order[index + 1];    if (following) goTo(following);  }, [active, validate, focusItem, order, index, goTo]);  const previous = React.useCallback(() => {    const before = order[index - 1];    if (before) goTo(before);  }, [order, index, goTo]);  const skip = React.useCallback(() => {    if (!active) return;    setAnswers((a) => ({ ...a, [active]: [] }));    setOtherState((o) => ({ ...o, [active]: "" }));    setErrors((e) => ({ ...e, [active]: undefined }));    const following = order[index + 1];    if (following) goTo(following);  }, [active, order, index, goTo]);​  const value = React.useMemo<QuestionnaireContextValue>(    () => ({ order, active, goTo, shortcuts, answers, other, errors, configs, register, setAnswer, setOther, next, previous, skip }),    [order, active, goTo, shortcuts, answers, other, errors, register, setAnswer, setOther, next, previous, skip],  );​  return (    <QuestionnaireContext.Provider value={value}>      <form        ref={form}        data-slot="questionnaire"        noValidate        className={cn("flex w-full min-w-0 flex-col gap-4", className)}        onSubmit={(event) => {          // Every required question must be answered; the first one that is not is shown with its message.          const missing = order.find((name) => !validate(name));          if (missing) {            event.preventDefault();            goTo(missing);            return;          }          onSubmit?.(event);        }}        onKeyDown={(event) => {          onKeyDown?.(event);          if (event.defaultPrevented || !active || event.metaKey || event.ctrlKey || event.altKey) return;          const target = event.target as HTMLElement;          const typing = target instanceof HTMLInputElement && target.type === "text";          if (event.key === "Enter" && !(target instanceof HTMLButtonElement) && !(target instanceof HTMLTextAreaElement)) {            event.preventDefault();            if (index < order.length - 1) next();            else form.current?.requestSubmit();            return;          }          if (typing || shortcuts === "none") return;          const key = event.key.toUpperCase();          const at = shortcuts === "letters" ? key.charCodeAt(0) - 65 : Number(key) - 1;          if (key.length !== 1 || Number.isNaN(at) || at < 0 || at > 8) return;          const choice = form.current?.querySelectorAll<HTMLInputElement>(`[data-slot=questionnaire-item][data-name="${CSS.escape(active)}"] [data-slot=questionnaire-choice-input]`)[at];          if (!choice || choice.disabled) return;          event.preventDefault();          choice.focus();          setAnswer(active, choice.value, choice.type === "checkbox" ? !choice.checked : true);        }}        {...props}      >        {children}      </form>    </QuestionnaireContext.Provider>  );}​/** Where the person is: "Question 2 of 3" in 12px muted medium over a 4px purple bar. */function QuestionnaireProgress({ className, children, ...props }: React.ComponentProps<"div">) {  const { order, active } = useQuestionnaire();  const at = active ? order.indexOf(active) + 1 : 0;  const total = order.length;  return (    <div data-slot="questionnaire-progress" className={cn("grid w-full gap-1.5", className)} {...props}>      <span className="font-sans text-caption leading-tight font-medium text-fg-muted tabular-nums" aria-live="polite">        {children ?? (total ? `Question ${at} of ${total}` : " ")}      </span>      <Progress value={total ? (at / total) * 100 : 0} aria-label="Progress" className="h-1" />    </div>  );}​/** One question. Only the current one shows; the others stay in the form so their answers submit with it. */function QuestionnaireItem({ name, required = false, multiple = false, className, children, ...props }: Omit<React.ComponentProps<"fieldset">, "name"> & { name: string; required?: boolean; multiple?: boolean }) {  const { active, register, errors } = useQuestionnaire();  const id = React.useId();  React.useLayoutEffect(() => register({ name, required, multiple }), [register, name, required, multiple]);  const context = React.useMemo(() => ({ name, required, multiple, id }), [name, required, multiple, id]);  const isActive = active === name;  return (    <ItemContext.Provider value={context}>      <fieldset        data-slot="questionnaire-item"        data-name={name}        data-active={isActive || undefined}        hidden={!isActive}        aria-labelledby={`${id}-title`}        aria-describedby={`${id}-description${errors[name] ? ` ${id}-error` : ""}`}        aria-invalid={errors[name] ? true : undefined}        className={cn("m-0 flex min-w-0 flex-col gap-4 border-0 p-0 outline-none", className)}        {...props}      >        {children}      </fieldset>    </ItemContext.Provider>  );}​function QuestionnaireTitle({ className, ...props }: React.ComponentProps<"div">) {  const { id } = useItem();  return <div id={`${id}-title`} data-slot="questionnaire-title" className={cn("font-sans text-body-lg leading-snug font-medium text-pretty text-fg", className)} {...props} />;}​function QuestionnaireDescription({ className, ...props }: React.ComponentProps<"p">) {  const { id } = useItem();  return <p id={`${id}-description`} data-slot="questionnaire-description" className={cn("-mt-3 font-sans text-body leading-body text-pretty text-fg-secondary", className)} {...props} />;}​/** The choices of a question, 8px apart; each direct QuestionnaireChoice gets its shortcut in order. */function QuestionnaireChoices({ className, children, ...props }: React.ComponentProps<"div">) {  const { multiple, id } = useItem();  let index = 0;  return (    <div data-slot="questionnaire-choices" role={multiple ? "group" : "radiogroup"} aria-labelledby={`${id}-title`} className={cn("group/questionnaire-choices grid min-w-0 gap-2", className)} {...props}>      {React.Children.map(children, (child) => (React.isValidElement<{ index?: number }>(child) && child.type === QuestionnaireChoice ? React.cloneElement(child, { index: index++ }) : child))}    </div>  );}​function QuestionnaireChoice({ value, disabled = false, index = 0, className, children, ...props }: Omit<React.ComponentProps<"label">, "children"> & { value: string; disabled?: boolean; index?: number; children?: React.ReactNode }) {  const { answers, setAnswer, shortcuts } = useQuestionnaire();  const { name, multiple } = useItem();  const checked = answers[name]?.includes(value) ?? false;  const key = shortcutKey(index, shortcuts);  return (    <label      data-slot="questionnaire-choice"      data-type={multiple ? "checkbox" : "radio"}      data-checked={checked || undefined}      data-disabled={disabled || undefined}      className={cn(        "group/questionnaire-choice relative flex min-h-11 cursor-pointer items-start gap-2.5 rounded-control border border-line-field bg-surface px-3.5 py-2.5 text-start font-sans text-body leading-snug text-fg select-none transition-[background-color,border-color] duration-150 ease-out hover:bg-subtle has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-focus-ring data-checked:border-line-brand data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 motion-reduce:transition-none",        className,      )}      {...props}    >      <input        data-slot="questionnaire-choice-input"        type={multiple ? "checkbox" : "radio"}        name={name}        value={value}        checked={checked}        disabled={disabled}        onChange={(event) => setAnswer(name, value, event.target.checked)}        className="peer sr-only"      />      <span        aria-hidden        data-slot="questionnaire-choice-indicator"        className={cn(          "pointer-events-none mt-px flex size-4.5 shrink-0 items-center justify-center border-[1.5px] border-line-strong bg-surface text-on-primary transition-[background-color,border-color,border-width] duration-150 ease-out motion-reduce:transition-none",          multiple ? "rounded-xs group-data-checked/questionnaire-choice:border-primary group-data-checked/questionnaire-choice:bg-primary" : "rounded-pill group-data-checked/questionnaire-choice:border-4 group-data-checked/questionnaire-choice:border-pink",        )}      >        {multiple && <CheckIcon weight="bold" className="hidden size-3.5 group-data-checked/questionnaire-choice:block" />}      </span>      <span data-slot="questionnaire-choice-label" className="flex min-w-0 flex-1 flex-col gap-0.5">        {children}      </span>      {key && (        <Kbd data-slot="questionnaire-choice-shortcut" aria-hidden className="ml-auto hidden shrink-0 sm:inline-flex">          {key}        </Kbd>      )}    </label>  );}​/** A choice's second line: 14px muted. */function QuestionnaireChoiceDescription({ className, ...props }: React.ComponentProps<"span">) {  return <span data-slot="questionnaire-choice-description" className={cn("text-fg-muted", className)} {...props} />;}​/** A free answer typed instead of a choice (another answer); typing clears a single choice, choosing clears the text. */function QuestionnaireInput({ className, ...props }: Omit<React.ComponentProps<typeof Input>, "name" | "value" | "onChange">) {  const { other, setOther } = useQuestionnaire();  const { name } = useItem();  return (    <div data-slot="questionnaire-input-wrapper" className="group/questionnaire-input relative w-full min-w-0">      <Input data-slot="questionnaire-input" type="text" name={name} value={other[name] ?? ""} onChange={(event) => setOther(name, event.target.value)} className={cn("h-11", className)} {...props} />    </div>  );}​/** Why the question cannot be passed yet: 12px red, read out when it appears. */function QuestionnaireError({ className, children, ...props }: React.ComponentProps<"p">) {  const { errors } = useQuestionnaire();  const { name, id } = useItem();  const message = children ?? errors[name];  if (!message) return null;  return (    <p id={`${id}-error`} role="alert" data-slot="questionnaire-error" className={cn("-mt-2 font-sans text-caption leading-tight text-danger", className)} {...props}>      {message}    </p>  );}​/** The buttons under the question: Previous at the left, then Skip and Next (or Submit on the last question). */function QuestionnaireActions({ className, ...props }: React.ComponentProps<"div">) {  return <div data-slot="questionnaire-actions" className={cn("grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2", className)} {...props} />;}​type ActionProps = Omit<React.ComponentProps<typeof Button>, "onClick" | "type"> & { onClick?: React.MouseEventHandler<HTMLButtonElement> };​function QuestionnairePrevious({ children, className, variant = "secondary", onClick, ...props }: ActionProps) {  const { order, active, previous } = useQuestionnaire();  const first = !active || order.indexOf(active) <= 0;  return (    <Button      data-slot="questionnaire-previous"      variant={variant}      disabled={first}      className={cn("col-start-1 row-start-1 justify-self-start", first && "invisible", className)}      onClick={(event) => {        onClick?.(event);        if (!event.defaultPrevented) previous();      }}      {...props}    >      {children ?? "Previous"}    </Button>  );}​function QuestionnaireSkip({ children, className, variant = "ghost", onClick, ...props }: ActionProps) {  const { active, configs, order, skip } = useQuestionnaire();  const required = active ? configs.current.get(active)?.required : true;  const last = !active || order.indexOf(active) === order.length - 1;  if (required || last) return null;  return (    <Button      data-slot="questionnaire-skip"      variant={variant}      className={cn("col-start-2 row-start-1 justify-self-end px-4 text-fg-secondary", className)}      onClick={(event) => {        onClick?.(event);        if (!event.defaultPrevented) skip();      }}      {...props}    >      {children ?? "Skip"}    </Button>  );}​function QuestionnaireNext({ children, className, variant = "default", onClick, ...props }: ActionProps) {  const { order, active, next } = useQuestionnaire();  if (!active || order.indexOf(active) >= order.length - 1) return null;  return (    <Button      data-slot="questionnaire-next"      variant={variant}      className={cn("col-start-3 row-start-1 justify-self-end", className)}      onClick={(event) => {        onClick?.(event);        if (!event.defaultPrevented) next();      }}      {...props}    >      {children ?? "Next"}    </Button>  );}​function QuestionnaireSubmit({ children, className, variant = "default", ...props }: Omit<React.ComponentProps<typeof Button>, "type">) {  const { order, active } = useQuestionnaire();  if (!active || order.indexOf(active) !== order.length - 1) return null;  return (    <Button data-slot="questionnaire-submit" type="submit" variant={variant} className={cn("col-start-3 row-start-1 justify-self-end", className)} {...props}>      {children ?? "Submit"}    </Button>  );}​export {  Questionnaire,  QuestionnaireActions,  QuestionnaireChoice,  QuestionnaireChoiceDescription,  QuestionnaireChoices,  QuestionnaireDescription,  QuestionnaireError,  QuestionnaireInput,  QuestionnaireItem,  QuestionnaireNext,  QuestionnairePrevious,  QuestionnaireProgress,  QuestionnaireSkip,  QuestionnaireSubmit,  QuestionnaireTitle,};

Reference: Extrapolated from the Inkbox console (Figma 427:2715): the radio and checks in Create Agent Identity and the Where does this agent run? picker.