Prompt Input Agent
Agent composer: agent chip, @ mention list and send button.
import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from "@/components/ai/prompt-input";import { PromptInputAgent, PromptInputAgentMentions, type PromptInputAgentOption, useAgentMention,} from "@/components/ai/prompt-input-agent";import { Button } from "@/components/ui/button";import { useState } from "react";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const agents: PromptInputAgentOption[] = [ { id: "keeper", name: "Keeper", scope: "trading", color: "chart-3" }, { id: "writer", name: "Writer", scope: "feed", color: "chart-4" }, { id: "analyst", name: "Analyst", scope: "research", color: "chart-2" },];
export default function Demo() { const [agent, setAgent] = useState(agents[0] as PromptInputAgentOption); const [value, setValue] = useState("@"); const mention = useAgentMention({ agents, value, onValueChange: setValue, onAgentSelect: setAgent, });
return ( <div className="w-full max-w-xl pt-36"> <div className="relative"> {mention.open && <PromptInputAgentMentions {...mention.mentions} />} <PromptInput onSubmit={() => setValue("")} className="[&_[data-slot=input-group]]:bg-muted" > <PromptInputHeader> <PromptInputAgent agent={agent} /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea value={value} onChange={(e) => setValue(e.currentTarget.value)} onKeyDown={mention.onKeyDown} placeholder={`Ask ${agent.name} anything about your index… (type @ to switch)`} className="min-h-20" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <Button aria-label="Attach" size="icon-sm" type="button" variant="ghost" > <IconPlaceholder lucide="PlusIcon" tabler="IconPlus" hugeicons="Add01Icon" phosphor="PlusIcon" remixicon="RiAddLine" className="size-4" /> </Button> </PromptInputTools> <PromptInputSubmit size="icon-sm" className="size-10" variant="secondary" disabled={!value} /> </PromptInputFooter> </PromptInput> </div> </div> );}<script setup lang="ts">import { PlusIcon } from "@lucide/vue";import { ref } from "vue";import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools, createPromptInputState, providePromptInput,} from "@/components/ai/prompt-input";import { PromptInputAgent, PromptInputAgentMentions, type PromptInputAgentOption, useAgentMention,} from "@/components/ai/prompt-input-agent";import { Button } from "@/components/ui/button";
const agents: PromptInputAgentOption[] = [ { id: "keeper", name: "Keeper", scope: "trading", color: "chart-3" }, { id: "writer", name: "Writer", scope: "feed", color: "chart-4" }, { id: "analyst", name: "Analyst", scope: "research", color: "chart-2" },];
const agent = ref<PromptInputAgentOption>(agents[0] as PromptInputAgentOption);// Lift the composer state so the mention list can read the text.const state = providePromptInput(createPromptInputState("@"));const mention = useAgentMention({ agents, value: state.textInput, onValueChange: state.setTextInput, onAgentSelect: (a) => { agent.value = a; },});</script>
<template> <div class="w-full max-w-xl pt-36"> <div class="relative"> <PromptInputAgentMentions v-if="mention.open.value" :agents="mention.items.value" :active-index="mention.activeIndex.value" @select="mention.select" /> <PromptInput class="[&_[data-slot=input-group]]:bg-muted"> <PromptInputHeader> <PromptInputAgent :agent="agent" /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea :placeholder="`Ask ${agent.name} anything about your index… (type @ to switch)`" class="min-h-20" @keydown.capture="mention.onKeydown" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <Button aria-label="Attach" size="icon-sm" type="button" variant="ghost"> <PlusIcon class="size-4" /> </Button> </PromptInputTools> <PromptInputSubmit class="size-10" size="icon-sm" variant="secondary" /> </PromptInputFooter> </PromptInput> </div> </div></template><script lang="ts"> import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import { PromptInput, PromptInputBody, PromptInputController, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools, setPromptInput, } from "$lib/components/ai/prompt-input"; import { PromptInputAgent, PromptInputAgentMentions, type PromptInputAgentOption, useAgentMention, } from "$lib/components/ai/prompt-input-agent"; import { Button } from "$lib/components/ui/button";
const agents: PromptInputAgentOption[] = [ { id: "keeper", name: "Keeper", scope: "trading", color: "chart-3" }, { id: "writer", name: "Writer", scope: "feed", color: "chart-4" }, { id: "analyst", name: "Analyst", scope: "research", color: "chart-2" }, ];
let agent = $state<PromptInputAgentOption>(agents[0] as PromptInputAgentOption); // Lift the composer state so the mention list can read the text. const controller = setPromptInput(new PromptInputController("@")); const mention = useAgentMention({ agents, value: () => controller.textInput, onValueChange: controller.setTextInput, onAgentSelect: (a) => (agent = a), });</script>
<div class="w-full max-w-xl pt-36"> <div class="relative"> {#if mention.open} <PromptInputAgentMentions agents={mention.items} activeIndex={mention.activeIndex} onSelect={mention.select} /> {/if} <PromptInput onSubmit={() => {}} class="[&_[data-slot=input-group]]:bg-muted"> <PromptInputHeader> <PromptInputAgent {agent} /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea placeholder="Ask {agent.name} anything about your index… (type @ to switch)" class="min-h-20" onkeydown={mention.onkeydown} /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <Button aria-label="Attach" size="icon-sm" type="button" variant="ghost"> <IconPlaceholder lucide="PlusIcon" tabler="IconPlus" hugeicons="Add01Icon" phosphor="PlusIcon" remixicon="RiAddLine" class="size-4" /> </Button> </PromptInputTools> <PromptInputSubmit class="size-10" size="icon-sm" variant="secondary" /> </PromptInputFooter> </PromptInput> </div></div>Edmi ✦ addition. Built on ai-prompt-input, ai-agent-avatar and button.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-prompt-input-agentpnpm dlx shadcn@latest add @edmi-ui/ai-prompt-input-agentyarn dlx shadcn@latest add @edmi-ui/ai-prompt-input-agentbunx shadcn@latest add @edmi-ui/ai-prompt-input-agentPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
ai-agent-avatar,button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-prompt-input-agentpnpm dlx shadcn-vue@latest add @edmi-ui/ai-prompt-input-agentyarn dlx shadcn-vue@latest add @edmi-ui/ai-prompt-input-agentbunx shadcn-vue@latest add @edmi-ui/ai-prompt-input-agentPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
ai-agent-avatar,button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-prompt-input-agent.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-prompt-input-agent.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-prompt-input-agent.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-prompt-input-agent.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
ai-agent-avatar,button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { PromptInput, PromptInputBody, PromptInputHeader, PromptInputTextarea,} from "@/components/ai/prompt-input";import { PromptInputAgent } from "@/components/ai/prompt-input-agent";
<div className="w-full max-w-xl pt-36"> <div className="relative"> <PromptInput className="[&_[data-slot=input-group]]:bg-muted" > <PromptInputHeader> <PromptInputAgent /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea className="min-h-20" /> </PromptInputBody> {/* … */} </PromptInput> </div></div><script setup lang="ts">import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from "@/components/ai/prompt-input";import { PromptInputAgent, PromptInputAgentMentions,} from "@/components/ai/prompt-input-agent";import { Button } from "@/components/ui/button";</script>
<template> <div class="w-full max-w-xl pt-36"> <div class="relative"> <PromptInputAgentMentions /> <PromptInput class="[&_[data-slot=input-group]]:bg-muted"> <PromptInputHeader> <PromptInputAgent /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea :placeholder="`Ask $ anything about your index… (type @ to switch)`" class="min-h-20" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <Button aria-label="Attach" size="icon-sm" type="button" variant="ghost"> <PlusIcon class="size-4" /> </Button> </PromptInputTools> <PromptInputSubmit class="size-10" size="icon-sm" variant="secondary" /> </PromptInputFooter> </PromptInput> </div> </div></template><script lang="ts"> import { PromptInputAgentMentions, } from "$lib/components/ai/prompt-input-agent";</script>
<div class="w-full max-w-xl pt-36"> <div class="relative"> {#if mention.open} <PromptInputAgentMentions /> {/if} <!-- … --> </div></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps, KeyboardEvent } from "react";import { useEffect, useMemo, useState } from "react";import { AgentAvatar } from "@/registry/edmi/components/ai/agent-avatar";import { Button } from "@/registry/edmi/ui/button";import type { Elevation } from "@/registry/edmi/ui/elevation";
export type PromptInputAgentOption = { id: string; name: string; /** Short scope shown on the right of the mention list (`trading`). */ scope?: string; /** `chart-1` to `chart-5` or any CSS color for the identicon. */ color?: string;};
export type PromptInputAgentProps = Omit< ComponentProps<typeof Button>, "children" | "elevation"> & { agent: PromptInputAgentOption; /** ✦ depth of the chip: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;};
/** Chip in the composer header: who you are talking to. */export const PromptInputAgent = ({ agent, elevation, className, ...props}: PromptInputAgentProps) => ( <Button data-slot="ai-prompt-input-agent" variant="outline" size="sm" elevation={elevation} type="button" className={cn("gap-[7px] bg-card", className)} {...props} > <AgentAvatar seed={agent.id} color={agent.color} size={19} tile={false} /> {agent.name} </Button>);
export type PromptInputAgentMentionsProps = Omit< ComponentProps<"div">, "onSelect"> & { agents: PromptInputAgentOption[]; activeIndex?: number; onSelect?: (agent: PromptInputAgentOption) => void; label?: string;};
/** * The @ mention list. It positions itself above the nearest `relative` ancestor, so render it next to * (not inside) `PromptInput`, whose group clips overflow. */export const PromptInputAgentMentions = ({ agents, activeIndex = 0, onSelect, label = "Agents", className, ...props}: PromptInputAgentMentionsProps) => ( <div data-slot="ai-prompt-input-agent-mentions" role="listbox" aria-label={label} className={cn( "absolute bottom-full left-0 z-50 mb-2 w-70 rounded-xl border border-border bg-popover p-1.5 text-popover-foreground", className, )} {...props} > <div className="px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground"> {label} </div> {agents.map((agent, i) => ( <div key={agent.id} role="option" aria-selected={i === activeIndex} tabIndex={-1} data-active={i === activeIndex ? "" : undefined} className="flex h-8 cursor-default items-center gap-[9px] rounded-[7px] px-2 text-[13.5px] whitespace-nowrap data-[active]:bg-accent data-[active]:text-accent-foreground" // keep focus in the textarea while picking onMouseDown={(e) => e.preventDefault()} onClick={() => onSelect?.(agent)} onKeyDown={(e) => { if (e.key === "Enter") onSelect?.(agent); }} > <AgentAvatar seed={agent.id} color={agent.color} size={19} tile={false} /> <span>{agent.name}</span> {agent.scope && ( <span className="ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground"> {agent.scope} </span> )} </div> ))} </div>);
const MENTION = /(^|\s)@(\w*)$/;
export type UseAgentMentionOptions = { agents: PromptInputAgentOption[]; /** Controlled textarea value. */ value: string; onValueChange: (value: string) => void; /** Called with the chosen agent; the `@query` token is removed from the text. */ onAgentSelect: (agent: PromptInputAgentOption) => void;};
/** * Opens the mention list when the text ends in `@query`. Wire `onKeyDown` to the textarea and spread * `mentions` on `PromptInputAgentMentions` while `open`. */export function useAgentMention({ agents, value, onValueChange, onAgentSelect,}: UseAgentMentionOptions) { const [activeIndex, setActiveIndex] = useState(0); const [dismissed, setDismissed] = useState(false); const query = MENTION.exec(value)?.[2]; const items = useMemo( () => query === undefined ? [] : agents.filter((a) => a.name.toLowerCase().includes(query.toLowerCase()), ), [agents, query], ); const open = query !== undefined && items.length > 0 && !dismissed;
// A new query reopens the list and resets the highlight. // biome-ignore lint/correctness/useExhaustiveDependencies: reset on every query change useEffect(() => { setDismissed(false); setActiveIndex(0); }, [query]);
const select = (agent: PromptInputAgentOption) => { onValueChange(value.replace(MENTION, "$1")); onAgentSelect(agent); };
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { if (!open) return; if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((i) => (i + 1) % items.length); } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((i) => (i - 1 + items.length) % items.length); } else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); const agent = items[activeIndex]; if (agent) select(agent); } else if (e.key === "Escape") { setDismissed(true); } };
return { open, onKeyDown, mentions: { agents: items, activeIndex, onSelect: select }, };}<script setup lang="ts">import type { HTMLAttributes } from "vue"import type { PromptInputAgentOption } from "./types"import { AgentAvatar } from "@/registry/edmi/components/ai/agent-avatar"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import type { Elevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] agent: PromptInputAgentOption /** ✦ depth of the chip: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation}>(), { elevation: undefined,})</script>
<template> <!-- Chip in the composer header: who you are talking to. --> <Button data-slot="ai-prompt-input-agent" variant="outline" size="sm" :elevation="elevation" type="button" :class="cn('gap-[7px] bg-card', props.class)" > <AgentAvatar :seed="agent.id" :color="agent.color" :size="19" :tile="false" /> {{ agent.name }} </Button></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import type { PromptInputAgentOption } from "./types"import { AgentAvatar } from "@/registry/edmi/components/ai/agent-avatar"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] agents: PromptInputAgentOption[] activeIndex?: number label?: string}>(), { activeIndex: 0, label: "Agents",})
const emit = defineEmits<{ (e: "select", agent: PromptInputAgentOption): void}>()</script>
<template> <!-- The @ mention list. It positions itself above the nearest `relative` ancestor, so render it next to (not inside) `PromptInput`, whose group clips overflow. --> <div data-slot="ai-prompt-input-agent-mentions" role="listbox" :aria-label="label" :class="cn('absolute bottom-full left-0 z-50 mb-2 w-70 rounded-xl border border-border bg-popover p-1.5 text-popover-foreground', props.class)" > <div class="px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground"> {{ label }} </div> <!-- keep focus in the textarea while picking --> <div v-for="(agent, i) in agents" :key="agent.id" role="option" :aria-selected="i === activeIndex" tabindex="-1" :data-active="i === activeIndex ? '' : undefined" class="flex h-8 cursor-default items-center gap-[9px] rounded-[7px] px-2 text-[13.5px] whitespace-nowrap data-[active]:bg-accent data-[active]:text-accent-foreground" @mousedown.prevent @click="emit('select', agent)" @keydown.enter="emit('select', agent)" > <AgentAvatar :seed="agent.id" :color="agent.color" :size="19" :tile="false" /> <span>{{ agent.name }}</span> <span v-if="agent.scope" class="ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground"> {{ agent.scope }} </span> </div> </div></template>export type PromptInputAgentOption = { id: string; name: string; /** Short scope shown on the right of the mention list (`trading`). */ scope?: string; /** `chart-1` to `chart-5` or any CSS color for the identicon. */ color?: string;};import type { MaybeRefOrGetter } from "vue";import { computed, ref, toValue, watch } from "vue";import type { PromptInputAgentOption } from "./types";
const MENTION = /(^|\s)@(\w*)$/;
export type UseAgentMentionOptions = { agents: PromptInputAgentOption[]; /** Textarea value (ref or getter), for example the lifted composer `textInput`. */ value: MaybeRefOrGetter<string>; onValueChange: (value: string) => void; /** Called with the chosen agent; the `@query` token is removed from the text. */ onAgentSelect: (agent: PromptInputAgentOption) => void;};
/** * Opens the mention list when the text ends in `@query`. Attach `onKeydown` to the textarea with the * capture modifier (`@keydown.capture`) so it runs before the composer's own Enter handling. */export function useAgentMention({ agents, value, onValueChange, onAgentSelect,}: UseAgentMentionOptions) { const activeIndex = ref(0); const dismissed = ref(false); const query = computed(() => MENTION.exec(toValue(value))?.[2]); const items = computed(() => query.value === undefined ? [] : agents.filter((a) => a.name.toLowerCase().includes((query.value as string).toLowerCase()), ), ); const open = computed( () => query.value !== undefined && items.value.length > 0 && !dismissed.value, );
// A new query reopens the list and resets the highlight. watch(query, () => { dismissed.value = false; activeIndex.value = 0; });
const select = (agent: PromptInputAgentOption) => { onValueChange(toValue(value).replace(MENTION, "$1")); onAgentSelect(agent); };
const onKeydown = (e: KeyboardEvent) => { if (!open.value) return; const count = items.value.length; if (e.key === "ArrowDown") { e.preventDefault(); activeIndex.value = (activeIndex.value + 1) % count; } else if (e.key === "ArrowUp") { e.preventDefault(); activeIndex.value = (activeIndex.value - 1 + count) % count; } else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); // stop the composer's own Enter handler (submit) e.stopImmediatePropagation(); const agent = items.value[activeIndex.value]; if (agent) select(agent); } else if (e.key === "Escape") { dismissed.value = true; } };
return { open, items, activeIndex, onKeydown, select };}export { default as PromptInputAgent } from "./PromptInputAgent.vue";export { default as PromptInputAgentMentions } from "./PromptInputAgentMentions.vue";export * from "./types";export * from "./useAgentMention";import PromptInputAgent from "./prompt-input-agent.svelte";import PromptInputAgentMentions from "./prompt-input-agent-mentions.svelte";
export * from "./types.js";export * from "./use-agent-mention.svelte.js";export { PromptInputAgent, PromptInputAgentMentions };<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import { AgentAvatar } from "../agent-avatar/index.js"; import type { PromptInputAgentOption } from "./types.js";
let { ref = $bindable(null), agents, activeIndex = 0, onSelect, label = "Agents", class: className, ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, "children" | "onselect">, HTMLDivElement> & { agents: PromptInputAgentOption[]; activeIndex?: number; onSelect?: (agent: PromptInputAgentOption) => void; label?: string; } = $props();</script>
<!-- The @ mention list. It positions itself above the nearest `relative` ancestor, so render it next to (not inside) `PromptInput`, whose group clips overflow.--><div bind:this={ref} data-slot="ai-prompt-input-agent-mentions" role="listbox" aria-label={label} class={cn( "absolute bottom-full left-0 z-50 mb-2 w-70 rounded-xl border border-border bg-popover p-1.5 text-popover-foreground", className )} {...restProps}> <div class="px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground">{label}</div> {#each agents as agent, i (agent.id)} <!-- keep focus in the textarea while picking --> <div role="option" aria-selected={i === activeIndex} tabindex="-1" data-active={i === activeIndex ? "" : undefined} class="flex h-8 cursor-default items-center gap-[9px] rounded-[7px] px-2 text-[13.5px] whitespace-nowrap data-[active]:bg-accent data-[active]:text-accent-foreground" onmousedown={(e) => e.preventDefault()} onclick={() => onSelect?.(agent)} onkeydown={(e) => { if (e.key === "Enter") onSelect?.(agent); }} > <AgentAvatar seed={agent.id} color={agent.color} size={19} tile={false} /> <span>{agent.name}</span> {#if agent.scope} <span class="ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground"> {agent.scope} </span> {/if} </div> {/each}</div><script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { cn } from "$lib/utils.js"; import { AgentAvatar } from "../agent-avatar/index.js"; import type { PromptInputAgentOption } from "./types.js";
let { ref = $bindable(null), agent, elevation = "auto", class: className, ...restProps }: Omit<ButtonProps, "href" | "children" | "elevation"> & { agent: PromptInputAgentOption; /** ✦ depth of the chip: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $props();</script>
<!-- Chip in the composer header: who you are talking to. --><Button bind:ref {elevation} data-slot="ai-prompt-input-agent" variant="outline" size="sm" type="button" class={cn("gap-[7px] bg-card", className)} {...restProps}> <AgentAvatar seed={agent.id} color={agent.color} size={19} tile={false} /> {agent.name}</Button>export type PromptInputAgentOption = { id: string; name: string; /** Short scope shown on the right of the mention list (`trading`). */ scope?: string; /** `chart-1` to `chart-5` or any CSS color for the identicon. */ color?: string;};import type { PromptInputAgentOption } from "./types.js";
const MENTION = /(^|\s)@(\w*)$/;
export type UseAgentMentionOptions = { agents: PromptInputAgentOption[]; /** Getter for the textarea value, for example `() => controller.textInput`. */ value: () => string; onValueChange: (value: string) => void; /** Called with the chosen agent; the `@query` token is removed from the text. */ onAgentSelect: (agent: PromptInputAgentOption) => void;};
/** * Opens the mention list when the text ends in `@query`. Pass `onkeydown` to `PromptInputTextarea` * and render `PromptInputAgentMentions` with `mentions` while `open`. Call during component init. */export function useAgentMention(options: UseAgentMentionOptions) { let activeIndex = $state(0); let dismissed = $state(false); const query = $derived(MENTION.exec(options.value())?.[2]); const items = $derived( query === undefined ? [] : options.agents.filter((a) => a.name.toLowerCase().includes(query.toLowerCase()), ), ); const open = $derived(query !== undefined && items.length > 0 && !dismissed);
// A new query reopens the list and resets the highlight. $effect(() => { void query; dismissed = false; activeIndex = 0; });
const select = (agent: PromptInputAgentOption) => { options.onValueChange(options.value().replace(MENTION, "$1")); options.onAgentSelect(agent); };
const onkeydown = (event: KeyboardEvent) => { if (!open) return; const count = items.length; if (event.key === "ArrowDown") { event.preventDefault(); activeIndex = (activeIndex + 1) % count; } else if (event.key === "ArrowUp") { event.preventDefault(); activeIndex = (activeIndex - 1 + count) % count; } else if (event.key === "Enter" || event.key === "Tab") { event.preventDefault(); const agent = items[activeIndex]; if (agent) select(agent); } else if (event.key === "Escape") { dismissed = true; } };
return { get open() { return open; }, get items() { return items; }, get activeIndex() { return activeIndex; }, onkeydown, select, };}Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
The chip (PromptInputAgent) is an outline Button: raised and floating raise it, flat and sunken keep it flat, auto follows the scope. The composer plate is an overlay surface: pass elevation to PromptInput for it.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from "@/components/ai/prompt-input";import { PromptInputAgent } from "@/components/ai/prompt-input-agent";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex w-full max-w-2xl flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <PromptInput elevation={value} onSubmit={() => {}} className="max-w-xl [&_[data-slot=input-group]]:bg-muted" > <PromptInputHeader> <PromptInputAgent agent={{ id: "keeper", name: "Keeper", color: "chart-3" }} /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea defaultValue="Rebalance MAG4 and draft a post" className="min-h-14" placeholder="Ask Keeper anything about your index…" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools /> <PromptInputSubmit size="icon-sm" variant="secondary" /> </PromptInputFooter> </PromptInput> </div> ))} </div> );}<script setup lang="ts">import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from "@/components/ai/prompt-input";import { PromptInputAgent } from "@/components/ai/prompt-input-agent";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex w-full max-w-2xl flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <PromptInput :elevation="level.value" class="max-w-xl [&_[data-slot=input-group]]:bg-muted"> <PromptInputHeader> <PromptInputAgent :agent="{ id: 'keeper', name: 'Keeper', color: 'chart-3' }" /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea class="min-h-14" placeholder="Ask Keeper anything about your index…" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools /> <PromptInputSubmit size="icon-sm" variant="secondary" /> </PromptInputFooter> </PromptInput> </div> </div></template><script lang="ts"> import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools, } from "$lib/components/ai/prompt-input"; import { PromptInputAgent } from "$lib/components/ai/prompt-input-agent";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex w-full max-w-2xl flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <PromptInput elevation={level.value} class="max-w-xl [&_[data-slot=input-group]]:bg-muted" onSubmit={() => {}}> <PromptInputHeader> <PromptInputAgent agent={{ id: "keeper", name: "Keeper", color: "chart-3" }} /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea class="min-h-14" placeholder="Ask Keeper anything about your index…" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools /> <PromptInputSubmit size="icon-sm" variant="secondary" /> </PromptInputFooter> </PromptInput> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| PromptInputAgent | agent |
{ id, name, scope?, color? } |
|
| PromptInputAgent | elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
| PromptInputAgentMentions | agents, activeIndex, onSelect, label (Vue: select event) |
list that opens above the nearest relative ancestor |
|
| useAgentMention | agents, value, onValueChange, onAgentSelect |
returns { open, onKeyDown, mentions } |
✦ Edmi additions
Section titled “✦ Edmi additions”- Put
PromptInputAgentinPromptInputHeader. Typing@opens the mention list (arrows, Enter or Tab pick, Escape closes); render it next toPromptInput, not inside, because the input group clips overflow. - Pass
elevationtoPromptInputfor the plate depth; the muted shell is a class on the form ([&_[data-slot=input-group]]:bg-muted).