> ## 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.

# PromptBar – Design

> Prompt bars let users compose and submit prompts to an agent.

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>;
  }
};

export const CodePreviewPlaceholder = ({double, fullWidth}) => {
  const single = <div style={{
    width: fullWidth ? "100%" : "50%",
    borderRadius: "1rem",
    display: "flex",
    padding: "1rem",
    flexDirection: "column",
    gap: "0.5rem",
    height: "10rem",
    marginBlockEnd: "1rem"
  }} className="border-width-default border-color-subdued">
      <div className="bg-strong border-radius-large" style={{
    width: "100%",
    flexGrow: "1"
  }} />
      <div className="bg-strong border-radius-large" style={{
    width: "100%",
    flexGrow: "1"
  }} />
    </div>;
  return double ? <div style={{
    display: "flex",
    gap: "1rem"
  }}>
      {single}
      {single}
    </div> : single;
};

<LiveCode example="carto-prompt-bar" screenshot fullWidth>
  ```tsx lines theme={null}
  import { useState } from "react";
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    const [value, setValue] = useState("");

    return (
      <PromptBar value={value} onChange={setValue} placeholder="Ask anything…" />
    );
  }

  export default App;
  ```
</LiveCode>

## Options

PromptBar is controlled — the parent owns `value`, `attachments`, and the `transcription` feed — and derives its mode from them. It supports an attachment row, a voice-transcription entry point, and a customizable disclaimer line.

### With Text

<LiveCode example="carto-prompt-bar-with-text" screenshot fullWidth>
  ```tsx lines theme={null}
  import { useState } from "react";
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    const [value, setValue] = useState(
      "What's the weather like in San Francisco today?",
    );

    return <PromptBar value={value} onChange={setValue} onSend={() => {}} />;
  }

  export default App;
  ```
</LiveCode>

Once there's text, the editor rises to its own row above the controls, and the primary action switches from disabled to send.

### Attachments

<LiveCode example="carto-prompt-bar-attachments" screenshot fullWidth>
  ```tsx lines theme={null}
  import { useState } from "react";
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    const [value, setValue] = useState("");
    const [attachments, setAttachments] = useState([
      { id: "1", name: "invoice.pdf" },
      { id: "2", name: "site-photo.jpg" },
    ]);

    return (
      <PromptBar
        value={value}
        onChange={setValue}
        attachments={attachments}
        onAddAttachment={() => {}}
        onRemoveAttachment={(id) =>
          setAttachments((current) =>
            current.filter((attachment) => attachment.id !== id),
          )
        }
      />
    );
  }

  export default App;
  ```
</LiveCode>

Attachments render as thumbnail tiles in a row above the editor; adding one expands the row and reflows the bar. A tile without a `thumbnailUrl` falls back to showing the file name.

### Transcribing

<LiveCode example="carto-prompt-bar-transcribing" screenshot fullWidth>
  ```tsx lines theme={null}
  import { useState } from "react";
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    const [value, setValue] = useState("");
    const [draftValue, setDraftValue] = useState(
      "What's the weather like in San Francisco today?",
    );

    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
        {/* Active transcription with a live waveform */}
        <PromptBar
          value={value}
          onChange={setValue}
          transcription={{
            active: true,
            source: { type: "preset", name: "speaking" },
          }}
          onCancelTranscription={() => {}}
          onConfirmTranscription={() => {}}
        />

        {/* Text in the prompt — no transcription active */}
        <PromptBar
          value={draftValue}
          onChange={setDraftValue}
          onSend={() => {}}
        />

        {/* Transcription error */}
        <PromptBar
          value={value}
          onChange={setValue}
          transcription={{ active: true, error: "Unable to access microphone" }}
          onCancelTranscription={() => {}}
          onConfirmTranscription={() => {}}
        />
      </div>
    );
  }

  export default App;
  ```
</LiveCode>

`transcription.active` switches the bar into a distinct mode: the attach and mic controls are replaced by a Cancel button, an animated waveform, and a Done primary action. Prefer wiring real audio into `transcription.source` (a `MediaStream`, an `AnalyserNode`, or a computed amplitude); fall back to a named preset (`ambient` / `speaking` / `thinking`) only when none is available. Setting `transcription.error` instead swaps in an error layout — an error icon, the message, and Done dismisses the error and exits.

### Disclaimer

<LiveCode example="carto-prompt-bar-disclaimer" screenshot fullWidth>
  ```tsx lines theme={null}
  import { useState } from "react";
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    const [value, setValue] = useState("");

    return (
      <PromptBar
        value={value}
        onChange={setValue}
        disclaimer={{ linkHref: "https://example.com" }}
      />
    );
  }

  export default App;
  ```
</LiveCode>

A disclaimer line sits below the bar, with a "Learn more" link out to more information.

### Disabled

<LiveCode example="carto-prompt-bar-disabled" screenshot fullWidth>
  ```tsx lines theme={null}
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    return (
      <PromptBar
        value="What's the weather like in San Francisco today?"
        onChange={() => {}}
        disabled
      />
    );
  }

  export default App;
  ```
</LiveCode>

`disabled` dims the whole bar and blocks the editor, attachments, and every action.

## Behavior

The editor grows with its content, and the bar reflows as attachments and text come and go.

### Text Wrapping

<LiveCode example="carto-prompt-bar-draft-reflow" screenshot fullWidth>
  ```tsx lines theme={null}
  import { useState } from "react";
  import { PromptBar } from "@servicetitan/carto-react-kit";

  function App() {
    const [value, setValue] = useState(
      "Can you help me draft a detailed message to the customer explaining the delay in their service appointment, the reason for the delay, and what we're doing to make it right? Please keep the tone apologetic but professional.",
    );

    return <PromptBar value={value} onChange={setValue} onSend={() => {}} />;
  }

  export default App;
  ```
</LiveCode>

Once there's text, the editor lifts to its own row above the controls and wraps across multiple lines, growing up to 8 rows before it scrolls internally — it never truncates or clips.

## Keyboard Interaction

Users can compose, send, and control transcription using standard keyboard controls.

| Key                        | Description                                                                 |
| -------------------------- | --------------------------------------------------------------------------- |
| Enter                      | Sends the draft (draft mode) or confirms the transcript (transcribing mode) |
| Shift / Ctrl / Cmd + Enter | Inserts a newline in the editor instead of sending                          |
| Escape                     | Cancels transcription and returns to the editor                             |
| Tab                        | Moves focus among the attach, editor, mic, and primary controls             |
