Skip to content
Elements

Prompt library

Prompts you saved, searchable, with their variables shown before you insert one.

Review {branch} against the repo conventions. Judge whether it should exist before checking correctness.

{branch}
fig. 01

Installation

Install manually
npx shadcn@latest add "@assistant-ui/elements-prompt-library"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A searchable list of saved prompts that previews the highlighted one, body and variable placeholders included, before you commit to it. With a runtime only the final insert reaches into assistant-ui state; standalone you own selection and insertion too.

Getting started

assistant-ui does not store saved prompts, so there is no library to read from the runtime. The catalog, its storage, and its search are yours, as in the standalone lane. The runtime is involved only when a prompt is inserted.

Wire insert to the composer

With onSelect, onInsert fires on Enter or a double click. Without onSelect, each row is tabbable and a single pointer or keyboard activation inserts it. Write the chosen prompt's body straight into the live composer; the element does not touch variable placeholders like {topic}, so substituting them, if you want that, happens before this call.

components/assistant-ui/elements/prompt-library-panel.tsx
"use client";

import { useAui } from "@assistant-ui/react";
import { useState } from "react";
import {
  PromptLibrary,
  type SavedPrompt,
} from "@/components/assistant-ui/elements/prompt-library";

export function PromptLibraryPanel({
  prompts,
}: {
  prompts: readonly SavedPrompt[];
}) {
  const aui = useAui();
  const [query, setQuery] = useState("");
  const [selectedId, setSelectedId] = useState(prompts[0]?.id ?? "");

  return (
    <PromptLibrary
      prompts={prompts}
      query={query}
      selectedId={selectedId}
      onQueryChange={setQuery}
      onSelect={setSelectedId}
      onInsert={(id) => {
        const prompt = prompts.find((p) => p.id === id);
        if (prompt) aui.composer.setText(prompt.body);
      }}
    />
  );
}

Anatomy

<div data-slot="prompt-library">
  <div>
    <svg /* bookmark icon */ />
    <input role="combobox" aria-expanded aria-controls placeholder="Search prompts" />
  </div>
  <div role="listbox" aria-label="Saved prompts">
    {/* one option row per match, filtered by name */}
  </div>
  {/* "Nothing matches “query”" when there are no matches */}
  {/* preview: body text, then a chip per variable, only for the selected match */}
</div>

Filtering matches prompts against query by substring on name only, case-insensitively; the preview panel at the bottom renders only when selectedId is among the current matches, so a query that filters the selected prompt out of the list hides its preview along with it. With onSelect, arrow keys move the highlighted row within the current matches; if the selected id has been filtered out, ArrowDown starts from the first match and ArrowUp from the last. Enter inserts the highlighted match, the same action a double click on a row performs. With only onInsert, rows are tabbable and insert on one pointer or keyboard activation.

Examples

Formatting the variable count

The chip next to a match, and the chips in the preview, are display only; the element counts and lists variables but never substitutes them into body.

{prompt.variables.length > 0 && (
  <span>{`${prompt.variables.length} ${prompt.variables.length === 1 ? "var" : "vars"}`}</span>
)}

Restyle the list

The search field and the preview panel both use the shared field surface, and every count uses mono, from surfaces.tsx.

<PromptLibrary className="max-w-md" prompts={prompts} query={query} selectedId={selectedId} onQueryChange={setQuery} onSelect={setSelectedId} onInsert={onInsert} />

API reference

Composer action

Selector / callTypeDescription
aui.composer.setText(text)(text: string) => voidWrites text into the active composer, using the client from useAui(). The natural target for onInsert.

There is no selector for a saved-prompt catalog: the list, its storage, and its search all live in your own state.