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.
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 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
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.
"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 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 / 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. | |
onInsert | (id: string) => void | Called on double click or Enter, to drop the prompt into the composer. | |
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.