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.
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 initThen 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.
npx shadcn@latest add "@assistant-ui/elements-prompt-library"Props-driven: no runtime or provider required.
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.
"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);
}}
/>
);
}Standalone, PromptLibrary filters, previews, and handles its own arrow-key and Enter navigation; you hold the list, the query, and the selection, and decide what onInsert actually does.
Hold the list, the query, and the selection
"use client";
import { useState } from "react";
import { PromptLibrary } from "@/components/assistant-ui/elements/prompt-library";
const prompts = [
{
id: "bug-report",
name: "Bug report",
body: "Describe the bug, the steps to reproduce it, and the expected behavior.",
variables: [],
},
{
id: "summarize",
name: "Summarize {topic}",
body: "Summarize {topic} in three sentences for a non-technical reader.",
variables: ["topic"],
},
];
export function PromptLibraryPanel({
onInsert,
}: {
onInsert: (text: string) => void;
}) {
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) onInsert(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 / call | Type | Description |
|---|---|---|
aui.composer.setText(text) | (text: string) => void | Writes 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.
PromptLibrary
| Prop | Type | Default | Description |
|---|---|---|---|
prompts | readonly SavedPrompt[] | required | Everything saved. Filtering happens inside against the name. |
query | string | required | Current search text. |
selectedId | string | required | Which prompt is previewed below the list. |
onQueryChange | (query: string) => void | Called as the search is typed. | |
onSelect | (id: string) => void | Called when a prompt is highlighted. Without it, rows insert directly when onInsert is supplied and are non-interactive otherwise. | |
onInsert | (id: string) => void | Called on double click or Enter with onSelect; without onSelect, called on a single pointer or keyboard activation. | |
className | string | Merged onto the root. |
SavedPrompt
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Stable identity, compared against selectedId. |
name | string | required | Prompt name, and the only field the search filters on. |
body | string | required | The prompt text, shown in the preview below the list. |
variables | readonly string[] | required | Placeholder names in the body, listed so you know what the prompt will ask for. |
All other div props are forwarded to the root.