Prompt Templates

Galerie de modeles de prompts consultable avec filtre par categorie, formulaire de remplissage de variables et onSelect. Installez Prompt Templates depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Quand utiliser ceci dans une application IA

Use Prompt Templates to give users a curated, fillable prompt library instead of a blank box. Variable fields turn a saved prompt into a quick form.

Parcourir tous les composants pour agents IA

Galerie de modeles de prompts consultable avec filtre par categorie, formulaire de remplissage de variables et onSelect. Fait partie de la famille ai dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez Prompt Templates a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/prompt-templates.json

Source

"use client"; import { type ChangeEvent, type ComponentPropsWithoutRef, type ReactNode, useCallback, useId, useMemo, useState, } from "react"; import { Search, Sparkles } from "lucide-react"; import { cn } from "../../lib/utils"; import { Badge } from "../badge/badge"; import { Button } from "../button/button"; import { Input } from "../input/input"; const VARIABLE_PATTERN = /{{\s*([\w-]+)\s*}}/g; const ALL_CATEGORY_VALUE = "__all__"; /** * One prompt template entry. * * @public */ export type PromptTemplate = { /** Optional category (matched against {@link PromptTemplateCategory.name}). */ category?: string; /** Sub-headline shown under the title. */ description?: ReactNode; /** Stable identifier. */ id: string; /** * Raw template body with `{{variable}}` placeholders. Placeholders are * detected automatically; the explicit `variables` array overrides * detection (useful when the same placeholder appears more than once). */ template: string; /** Display title. */ title: ReactNode; /** Override for detected variable names. */ variables?: string[]; }; /** * Category chip for filtering. * * @public */ export type PromptTemplateCategory = { /** Optional icon rendered next to the name. */ icon?: ReactNode; /** Category display name (matched against {@link PromptTemplate.category}). */ name: string; }; /** * Localizable strings. * * @public */ export type PromptTemplatesLabels = { /** Caption for the all-categories chip. Defaults to `"All"`. */ allCategory?: string; /** Caption for the cancel button on the variable form. Defaults to `"Cancel"`. */ cancel?: string; /** Empty-state heading when no templates match. Defaults to `"No prompts found."`. */ empty?: string; /** Caption for the use button when filling in variables. Defaults to `"Insert"`. */ insert?: string; /** Search input placeholder. Defaults to `"Search prompts…"`. */ searchPlaceholder?: string; /** Caption for the use button on a card. Defaults to `"Use template"`. */ use?: string; /** Caption above the variable form. Defaults to `"Fill in the placeholders"`. */ variablesHeading?: string; }; const DEFAULT_LABELS = { allCategory: "All", cancel: "Cancel", empty: "No prompts found.", insert: "Insert", searchPlaceholder: "Search prompts…", use: "Use template", variablesHeading: "Fill in the placeholders", } as const satisfies Required<PromptTemplatesLabels>; /** * Props for {@link PromptTemplates}. * * @public */ export type PromptTemplatesProps = { /** Optional list of categories to render as filter chips. */ categories?: PromptTemplateCategory[]; /** Localizable strings. */ labels?: PromptTemplatesLabels; /** * Fires with the resolved template body. When the template has variables, * the user fills them in first; otherwise this fires on click. */ onSelect?: (resolved: string, template: PromptTemplate) => void; /** The template list. */ templates: PromptTemplate[]; } & ComponentPropsWithoutRef<"section">; function detectVariables(template: PromptTemplate): string[] { if (template.variables) return template.variables; const matches = [...template.template.matchAll(VARIABLE_PATTERN)]; const seen = new Set<string>(); return matches.reduce<string[]>((accumulator, match) => { const name = match[1]; if (name && !seen.has(name)) { seen.add(name); accumulator.push(name); } return accumulator; }, []); } function fillTemplate( template: string, values: Record<string, string>, ): string { return template.replaceAll( VARIABLE_PATTERN, (_match: string, name: string) => values[name] ?? `{{${name}}}`, ); } function matchesCategory(template: PromptTemplate, selected: string): boolean { if (selected === ALL_CATEGORY_VALUE) return true; return template.category === selected; } function matchesQuery(template: PromptTemplate, query: string): boolean { if (!query) return true; const lowered = query.toLowerCase(); const title = typeof template.title === "string" ? template.title.toLowerCase() : ""; const description = typeof template.description === "string" ? template.description.toLowerCase() : ""; return title.includes(lowered) || description.includes(lowered); } type FilterBarProps = { categories: PromptTemplateCategory[]; labels: Required<PromptTemplatesLabels>; onCategoryChange: (value: string) => void; onQueryChange: (value: string) => void; query: string; searchId: string; selectedCategory: string; }; function FilterBar({ categories, labels, onCategoryChange, onQueryChange, query, searchId, selectedCategory, }: FilterBarProps): ReactNode { const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => { onQueryChange(event.target.value); }; return ( <div className="flex flex-col gap-3"> <div className="relative"> <Search aria-hidden="true" className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> <Input aria-label={labels.searchPlaceholder} className="pl-9" id={searchId} onChange={handleSearch} placeholder={labels.searchPlaceholder} type="search" value={query} /> </div> {categories.length > 0 ? ( <div className="flex flex-wrap gap-1.5"> <CategoryChip active={selectedCategory === ALL_CATEGORY_VALUE} label={labels.allCategory} onClick={() => { onCategoryChange(ALL_CATEGORY_VALUE); }} value={ALL_CATEGORY_VALUE} /> {categories.map((category) => ( <CategoryChip active={selectedCategory === category.name} icon={category.icon} key={category.name} label={category.name} onClick={() => { onCategoryChange(category.name); }} value={category.name} /> ))} </div> ) : null} </div> ); } type CategoryChipProps = { active: boolean; icon?: ReactNode; label: string; onClick: () => void; value: string; }; function CategoryChip({ active, icon, label, onClick, value, }: CategoryChipProps): ReactNode { return ( <button aria-pressed={active} className={cn( "inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", active ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent", )} data-value={value} onClick={onClick} type="button" > {icon ? ( <span aria-hidden="true" className="[&>svg]:h-3.5 [&>svg]:w-3.5"> {icon} </span> ) : null} {label} </button> ); } type CardProps = { active: boolean; labels: Required<PromptTemplatesLabels>; onActivate: (template: PromptTemplate) => void; onCancel: () => void; onResolve: (resolved: string, template: PromptTemplate) => void; onValueChange: (name: string, value: string) => void; template: PromptTemplate; values: Record<string, string>; }; type CardHeaderProps = { category?: string; description?: ReactNode; title: ReactNode; }; function CardHeader({ category, description, title, }: CardHeaderProps): ReactNode { return ( <header className="flex flex-col gap-1"> <h4 className="text-sm font-semibold tracking-tight text-foreground"> {title} </h4> {description ? ( <p className="text-xs text-muted-foreground">{description}</p> ) : null} {category ? ( <Badge className="self-start" variant="outline"> {category} </Badge> ) : null} </header> ); } type CardActionsProps = { onUse: () => void; useLabel: string; variableCount: number; }; function CardActions({ onUse, useLabel, variableCount, }: CardActionsProps): ReactNode { return ( <div className="flex items-center justify-between gap-2"> {variableCount > 0 ? ( <span className="text-xs text-muted-foreground"> {variableCount.toString()} variable{variableCount === 1 ? "" : "s"} </span> ) : ( <span aria-hidden="true" /> )} <Button onClick={onUse} size="sm" type="button" variant={variableCount > 0 ? "outline" : "default"} > <Sparkles aria-hidden="true" className="mr-2 size-3.5" /> {useLabel} </Button> </div> ); } function PromptTemplateCard({ active, labels, onActivate, onCancel, onResolve, onValueChange, template, values, }: CardProps): ReactNode { const variables = detectVariables(template); const handleUse = useCallback(() => { if (variables.length === 0) { onResolve(template.template, template); return; } if (active) { onResolve(fillTemplate(template.template, values), template); return; } onActivate(template); }, [active, onActivate, onResolve, template, values, variables.length]); return ( <article className="flex flex-col gap-3 rounded-xl border border-border bg-background p-4 shadow-sm" data-template-id={template.id} > <CardHeader category={template.category} description={template.description} title={template.title} /> {active && variables.length > 0 ? ( <VariableForm fieldIdPrefix={template.id} labels={labels} onCancel={onCancel} onSubmit={handleUse} onValueChange={onValueChange} values={values} variables={variables} /> ) : ( <CardActions onUse={handleUse} useLabel={labels.use} variableCount={variables.length} /> )} </article> ); } type VariableFormProps = { fieldIdPrefix: string; labels: Required<PromptTemplatesLabels>; onCancel: () => void; onSubmit: () => void; onValueChange: (name: string, value: string) => void; values: Record<string, string>; variables: string[]; }; type VariableFieldProps = { fieldId: string; name: string; onValueChange: (name: string, value: string) => void; value: string; }; function VariableField({ fieldId, name, onValueChange, value, }: VariableFieldProps): ReactNode { const handleVariableValueChange = useCallback( (event: ChangeEvent<HTMLInputElement>) => { onValueChange(name, event.target.value); }, [name, onValueChange], ); return ( <div className="flex flex-col gap-1 text-xs"> <label className="font-medium text-foreground" htmlFor={fieldId}> {name} </label> <Input id={fieldId} onChange={handleVariableValueChange} placeholder={`Value for ${name}`} value={value} /> </div> ); } function VariableForm({ fieldIdPrefix, labels, onCancel, onSubmit, onValueChange, values, variables, }: VariableFormProps): ReactNode { return ( <div className="flex flex-col gap-2 rounded-lg border border-dashed border-border bg-muted/30 p-3"> <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> {labels.variablesHeading} </p> <div className="flex flex-col gap-2"> {variables.map((name) => ( <VariableField fieldId={`${fieldIdPrefix}-${name}`} key={name} name={name} onValueChange={onValueChange} value={values[name] ?? ""} /> ))} </div> <div className="mt-1 flex justify-end gap-2"> <Button onClick={onCancel} size="sm" type="button" variant="ghost"> {labels.cancel} </Button> <Button onClick={onSubmit} size="sm" type="button"> {labels.insert} </Button> </div> </div> ); } /** * Library / gallery of saved prompt templates with search, category filter, * and a built-in fill-in form for `{{variable}}` placeholders. Composes * existing {@link Input}, {@link Button}, and {@link Badge} primitives. * * @example * ```tsx * <PromptTemplates * templates={[ * { * id: "code-review", * title: "Code Review", * description: "Review code for bugs and improvements", * template: "Review this {{language}} code:\n\n{{code}}", * category: "Code", * }, * ]} * categories={[{ name: "Code" }, { name: "Writing" }]} * onSelect={(resolved) => insertIntoComposer(resolved)} * /> * ``` * * @public */ type ControllerState = { activeId: null | string; filtered: PromptTemplate[]; handleActivate: (template: PromptTemplate) => void; handleCancel: () => void; handleCategoryChange: (value: string) => void; handleQueryChange: (value: string) => void; handleResolve: (resolved: string, template: PromptTemplate) => void; handleValueChange: (name: string, value: string) => void; query: string; searchId: string; selectedCategory: string; values: Record<string, string>; }; function usePromptTemplatesController( templates: PromptTemplate[], onSelect: PromptTemplatesProps["onSelect"], ): ControllerState { const searchId = useId(); const [query, setQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORY_VALUE); const [activeId, setActiveId] = useState<null | string>(null); const [values, setValues] = useState<Record<string, string>>({}); const filtered = useMemo( () => templates.filter( (template) => matchesCategory(template, selectedCategory) && matchesQuery(template, query), ), [query, selectedCategory, templates], ); const handleActivate = useCallback((template: PromptTemplate) => { setActiveId(template.id); setValues({}); }, []); const handleCancel = useCallback(() => { setActiveId(null); setValues({}); }, []); const handleResolve = useCallback( (resolved: string, template: PromptTemplate) => { onSelect?.(resolved, template); setActiveId(null); setValues({}); }, [onSelect], ); const handleValueChange = useCallback((name: string, value: string) => { setValues((current) => ({ ...current, [name]: value })); }, []); return { activeId, filtered, handleActivate, handleCancel, handleCategoryChange: setSelectedCategory, handleQueryChange: setQuery, handleResolve, handleValueChange, query, searchId, selectedCategory, values, }; } type GridProps = { controller: ControllerState; labels: Required<PromptTemplatesLabels>; }; function TemplateGrid({ controller, labels }: GridProps): ReactNode { if (controller.filtered.length === 0) { return ( <p className="rounded-lg border border-dashed border-border p-6 text-center text-sm text-muted-foreground"> {labels.empty} </p> ); } return ( <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3"> {controller.filtered.map((template) => ( <PromptTemplateCard active={controller.activeId === template.id} key={template.id} labels={labels} onActivate={controller.handleActivate} onCancel={controller.handleCancel} onResolve={controller.handleResolve} onValueChange={controller.handleValueChange} template={template} values={controller.values} /> ))} </div> ); } export const PromptTemplates = ({ ref, ...props }: PromptTemplatesProps & { ref?: React.Ref<HTMLElement> }) => { const { categories, className, labels, onSelect, templates, ...rest } = props; const resolvedLabels = useMemo( () => ({ ...DEFAULT_LABELS, ...labels }), [labels], ); const controller = usePromptTemplatesController(templates, onSelect); return ( <section className={cn( "flex flex-col gap-4 rounded-2xl border bg-background p-4", className, )} ref={ref} {...rest} > <FilterBar categories={categories ?? []} labels={resolvedLabels} onCategoryChange={controller.handleCategoryChange} onQueryChange={controller.handleQueryChange} query={controller.query} searchId={controller.searchId} selectedCategory={controller.selectedCategory} /> <TemplateGrid controller={controller} labels={resolvedLabels} /> </section> ); }; PromptTemplates.displayName = "PromptTemplates";

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0
  • lucide-react