> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Prompt – Code

> The AgentPrompt presents a clarifying question with single-select, multi-select, and free-text answers.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="ai-kit-agent-prompt" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { useState } from "react";
      import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

      function App() {
        const [value, setValue] = useState<string | null>(null);

        return (
          <div style={{ width: 380 }}>
            <AgentPrompt
              question="Question created by the agent, asking the user how to proceed."
              options={[
                { value: "a", label: "Choice A" },
                { value: "b", label: "Choice B" },
                { value: "c", label: "Choice C" },
              ]}
              value={value}
              onChange={setValue}
              allowOther={false}
              onClose={() => {}}
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    To use the `AgentPrompt`, pass a `question`, an `options` array, controlled `value` / `onChange`, and a required `onClose` handler. The close button is always shown and remains available while the prompt is disabled.

    ```tsx theme={null}
    import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

    function ExampleComponent() {
      const [value, setValue] = useState<string | null>(null);

      return (
        <AgentPrompt
          question="How should I proceed?"
          options={[
            { value: "a", label: "Choice A" },
            { value: "b", label: "Choice B" },
          ]}
          value={value}
          onChange={setValue}
          onSend={(answer) => send(answer)}
          onClose={() => dismiss()}
        />
      );
    }
    ```

    ### Selection mode

    Single-select is the default. Each row is the control; the selected row is outlined. Set `selectionMode="multi"` for checkbox semantics and a `string[]` value.

    <LiveCode showCode example="ai-kit-agent-prompt-single-select" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { useState } from "react";
      import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

      const options = [
        { value: "choc", label: "Chocolate" },
        { value: "van", label: "Vanilla" },
        { value: "straw", label: "Strawberry" },
        { value: "cookie", label: "Cookie Dough" },
      ];

      function App() {
        const [value, setValue] = useState<string | null>(null);
        const [other, setOther] = useState("");

        return (
          <div style={{ width: 380 }}>
            <AgentPrompt
              question="Which flavor would you like?"
              options={options}
              value={value}
              onChange={setValue}
              otherValue={other}
              onOtherChange={setOther}
              onClose={() => {}}
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    <LiveCode showCode example="ai-kit-agent-prompt-multi-select" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { useState } from "react";
      import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

      const options = [
        { value: "choc", label: "Chocolate" },
        { value: "van", label: "Vanilla" },
        { value: "straw", label: "Strawberry" },
        { value: "cookie", label: "Cookie Dough" },
      ];

      function App() {
        const [value, setValue] = useState<string[]>([]);
        const [other, setOther] = useState("");

        return (
          <div style={{ width: 380 }}>
            <AgentPrompt
              selectionMode="multi"
              question="Pick any flavors you like"
              options={options}
              value={value}
              onChange={setValue}
              otherValue={other}
              onOtherChange={setOther}
              onClose={() => {}}
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Free text

    Wiring `onOtherChange` shows the "Something else…" row. In single-select, free text is mutually exclusive with a selected row; the text is retained if the user switches back. In multi-select, free text is independent and is appended to the selected values on send.

    <LiveCode showCode example="ai-kit-agent-prompt-free-text" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { useState } from "react";
      import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

      const options = [
        { value: "choc", label: "Chocolate" },
        { value: "van", label: "Vanilla" },
        { value: "straw", label: "Strawberry" },
      ];

      function App() {
        const [value, setValue] = useState<string | null>(null);
        const [other, setOther] = useState("Pistachio");

        return (
          <div style={{ width: 380 }}>
            <AgentPrompt
              question="Which flavor would you like?"
              options={options}
              value={value}
              onChange={setValue}
              otherValue={other}
              onOtherChange={setOther}
              onClose={() => {}}
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    Pass `allowOther={false}` to hide the row even when a handler exists. Pass `true` without `onOtherChange` only in development as a misconfiguration.

    ### Multi-step

    Pass `step={{ current, total }}` to replace the single **Send** with a stepper. Wire `onNext` to advance; without it, Next validates but does not move. `onBack` is never validated.

    <LiveCode showCode example="ai-kit-agent-prompt-multi-step" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

      const options = [
        { value: "choc", label: "Chocolate" },
        { value: "van", label: "Vanilla" },
        { value: "straw", label: "Strawberry" },
      ];

      const noop = () => {};

      function App() {
        return (
          <div
            style={{
              display: "flex",
              gap: 24,
              alignItems: "flex-start",
              flexWrap: "wrap",
            }}
          >
            {/* First step — Next only */}
            <div style={{ width: 280 }}>
              <AgentPrompt
                question="Which flavor?"
                options={options}
                value="choc"
                onChange={noop}
                allowOther={false}
                step={{ current: 1, total: 3 }}
                onNext={noop}
                onClose={noop}
              />
            </div>

            {/* Middle step — Back + Next */}
            <div style={{ width: 280 }}>
              <AgentPrompt
                question="Which topping?"
                options={options}
                value="straw"
                onChange={noop}
                allowOther={false}
                step={{ current: 2, total: 3 }}
                onBack={noop}
                onNext={noop}
                onClose={noop}
              />
            </div>

            {/* Last step — Back + Send */}
            <div style={{ width: 280 }}>
              <AgentPrompt
                question="Which size?"
                options={[
                  { value: "sm", label: "Small" },
                  { value: "md", label: "Medium" },
                  { value: "lg", label: "Large" },
                ]}
                value="md"
                onChange={noop}
                allowOther={false}
                step={{ current: 3, total: 3 }}
                onBack={noop}
                onSend={noop}
                onClose={noop}
              />
            </div>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Empty answer

    Send and Next stay enabled. Submitting with no selection and no free text shows the empty-answer error instead of calling `onSend` or `onNext`.

    <LiveCode showCode example="ai-kit-agent-prompt-empty-answer-error" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { AgentPrompt } from "@servicetitan/anvil2-ai-kit";

      const options = [
        { value: "choc", label: "Chocolate" },
        { value: "van", label: "🍨 Vanilla" },
        { value: "straw", label: "🍓 Strawberry" },
      ];

      function App() {
        return (
          <div style={{ width: 380 }}>
            <AgentPrompt
              question="Which flavor would you like?"
              options={options}
              value={null}
              onChange={() => {}}
              allowOther={false}
              onClose={() => {}}
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    `onSend` and `onNext` receive a resolved answer: the selected option in single-select, or the checked values with non-empty free text appended in multi-select. Free-text strings pass through verbatim.

    ## Internationalization

    `AgentPrompt` owns these localized strings:

    * `ai-kit.agentPrompt.close`
    * `ai-kit.agentPrompt.otherPlaceholder`
    * `ai-kit.agentPrompt.send`
    * `ai-kit.agentPrompt.back`
    * `ai-kit.agentPrompt.next`
    * `ai-kit.agentPrompt.emptyError`
    * `ai-kit.agentPrompt.step`

    Localize `question` and option labels before passing them. Override `otherPlaceholder` or `emptyErrorMessage` on the component, or mount `AiKitIntlProvider` inside `CartoTheme`. `step` receives `{current}` and `{total}`.

    ## React Accessibility

    * The question labels the options group through `aria-labelledby`.
    * Single-select uses radio semantics. Multi-select uses checkbox semantics.
    * `headingLevel` sets the question's heading tag independently of visual size.
    * Close remains available while `disabled` is true.
  </Tab>

  <Tab title="AgentPrompt Props">
    ```tsx theme={null}
    <AgentPrompt
      question="How should I proceed?"
      options={options}
      value={value}
      onChange={setValue}
      onSend={send}
      onClose={dismiss}
    />
    ```

    ## `AgentPrompt` Props

    The `AgentPrompt` accepts the following props. It is a discriminated union on `selectionMode`. `value` and `onChange` match that mode.

    <ParamField path="onChange" type="(value: string | null) => void | (value: string[]) => void" required>
      Fired when the selection changes. Signature matches `selectionMode`.
    </ParamField>

    <ParamField path="onClose" type="() => void" required>
      Fired when the close button is pressed. Always required.
    </ParamField>

    <ParamField path="options" type="AgentPromptOption[]" required>
      Selectable options, rendered in order.
    </ParamField>

    <ParamField path="question" type="string" required>
      Question shown in the header and used as the options group's accessible
      name.
    </ParamField>

    <ParamField path="value" type="string | null | string[]" required>
      Controlled selection. `string | null` in single-select; `string[]` in
      multi-select.
    </ParamField>

    <ParamField path="allowOther" type="boolean">
      Whether to show the free-text row. Defaults to whether `onOtherChange` is
      provided.
    </ParamField>

    <ParamField path="className" type="string">
      Class merged onto the root element.
    </ParamField>

    <ParamField path="disabled" type="boolean">
      Disables the prompt. Close remains available.
    </ParamField>

    <ParamField path="emptyErrorMessage" type="string">
      Override for the localized empty-answer error.
    </ParamField>

    <ParamField path="headingLevel" type={`"h2" | "h3" | "h4" | "h5" | "h6"`} default="h2">
      Semantic heading level for the question.
    </ParamField>

    <ParamField path="onBack" type="() => void">
      Fired when Back is pressed. Never validated.
    </ParamField>

    <ParamField path="onNext" type="(answer: string | null) => void | (answer: string[]) => void">
      Fired when Next commits a valid answer. Required for the stepper to
      advance.
    </ParamField>

    <ParamField path="onOtherChange" type="(value: string) => void">
      Fired on every free-text edit. Wiring it shows the row by default.
    </ParamField>

    <ParamField path="onSend" type="(answer: string | null) => void | (answer: string[]) => void">
      Fired when Send commits a valid answer. Receives the resolved answer.
    </ParamField>

    <ParamField path="otherPlaceholder" type="string">
      Placeholder for the free-text row. Defaults to the localized "Something
      else…".
    </ParamField>

    <ParamField path="otherValue" type="string">
      Controlled free-text value.
    </ParamField>

    <ParamField path="selectionMode" type={`"single" | "multi"`} default="single">
      Selection semantics. Co-varies with `value` and `onChange`.
    </ParamField>

    <ParamField path="step" type="{ current: number; total: number }">
      Multi-step position. Omit for a single Send action.
    </ParamField>
  </Tab>

  <Tab title="AgentPromptOption">
    ```tsx theme={null}
    const option: AgentPromptOption = {
      value: "choc",
      label: "Chocolate",
    };
    ```

    ## `AgentPromptOption`

    <ParamField path="label" type="ReactNode" required>
      Display content for the option.
    </ParamField>

    <ParamField path="value" type="string" required>
      Stable identity used as the selection value and React key.
    </ParamField>

    <ParamField path="isDisabled" type="boolean">
      Disables this option while leaving the rest interactive.
    </ParamField>
  </Tab>
</Tabs>
