Elements

Elements · Composer

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

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

With a runtime, there is no saved-prompt library to read: assistant-ui has no concept of prompts you bookmarked for reuse. That catalog, its storage, and its search are entirely yours, exactly as in the standalone lane. The one place a runtime is actually involved is the moment a prompt gets used.

Wire insert to the composer

onInsert fires on Enter or a double click. 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 role="combobox" aria-expanded aria-controls>
    <svg /* bookmark icon */ />
    <input 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. Arrow keys move the highlighted row within the current matches; if the previously selected id has been filtered out, the first press starts from the edge the direction implies rather than jumping from a phantom position. Enter inserts the highlighted match, the same action a double click on a row performs.

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