Assistant

<Assistant> is a chat-style AI sidebar with pluggable LLM transport and tool-call support. The component handles the conversation UI, message streaming, and tool execution; you bring the AI provider via the transport prop (OpenAI, Anthropic, Vercel AI SDK, custom backend — your choice).

Installation

pnpm dlx shadcn@latest add @shadmin/assistant

Usage

import { Admin, Assistant, echoTransport } from "@/components/admin";
 
const App = () => (
  <Admin dataProvider={dp}>
    <Resource name="products" />
    <Assistant
      transport={echoTransport}
      tools={{
        setFilter: {
          description: "Apply a filter on the current list",
          parameters: { field: "string", value: "string" },
          handler: ({ field, value }) => {
            // apply filter logic here
          },
        },
      }}
    />
  </Admin>
);

Built-in echoTransport

A no-op transport that simply echoes the user's last message. Use it for demos, snapshots, or when you haven't wired up a real LLM yet.

import { Assistant, echoTransport } from "@/components/admin";
 
<Assistant transport={echoTransport} />;

Writing a real transport

import { type AssistantTransport } from "@/components/admin";
import OpenAI from "openai";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_KEY! });
 
const openaiTransport: AssistantTransport = {
  async *send(messages, tools) {
    const stream = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      stream: true,
      messages: messages.map((m) =>
        m.role === "tool"
          ? {
              role: "tool",
              tool_call_id: m.toolName,
              content: JSON.stringify(m.result),
            }
          : { role: m.role, content: m.content },
      ),
      tools: Object.entries(tools).map(([name, def]) => ({
        type: "function",
        function: {
          name,
          description: def.description,
          parameters: { type: "object", properties: def.parameters },
        },
      })),
    });
    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta?.content;
      if (delta) yield { type: "text", delta };
      const toolCalls = chunk.choices[0]?.delta?.tool_calls;
      if (toolCalls) {
        for (const tc of toolCalls) {
          if (tc.function?.name) {
            yield {
              type: "tool-call",
              toolName: tc.function.name,
              args: JSON.parse(tc.function.arguments ?? "{}"),
            };
          }
        }
      }
    }
    yield { type: "done" };
  },
};

Props

PropTypeDefault
transportAssistantTransportrequired
toolsRecord<string, ToolDefinition>{}
placement"bottom-right" | "bottom-left" | "sidebar""bottom-right"
triggerLabelstringtranslated Ask
welcomeMessagestringtranslated

Tool definitions

interface ToolDefinition {
  description: string;
  parameters: Record<string, string>; // keys are param names, values are types
  handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
}

When the LLM returns a tool-call chunk, the registered handler is invoked and its result is appended to the conversation as a tool message, then re-sent to the LLM on the next exchange.

placement

Controls where the floating trigger button and chat panel appear:

  • "bottom-right" (default) — trigger and panel in the bottom-right corner
  • "bottom-left" — trigger and panel in the bottom-left corner
  • "sidebar" — trigger and panel anchored to the top-right (suitable for sidebar layouts)

i18n

KeyDefault
ra.assistant.triggerAsk
ra.assistant.welcomeHi! Ask me anything.
ra.assistant.thinkingThinking…
ra.assistant.placeholderAsk…