@bernierllc/admin-agent-chat-ui
v0.1.0
Published
React chat UI for the admin agent: provider, trigger, panel, message list, tool-result and change-summary cards, and the useAdminAgentChat SSE hook.
Downloads
279
Readme
@bernierllc/admin-agent-chat-ui
React components for an embedded admin LLM agent: floating trigger, slide-in chat panel, streaming message bubbles, tool-result cards, and change summary.
Overview
This package provides a complete frontend UI for the admin agent chat system. Drop <AdminAgentProvider> + <AdminAgentTrigger> + <AdminAgentPanel> into any React app and get a fully functional floating chat widget that streams responses from an SSE endpoint.
Architecture:
- All business logic lives in the
useAdminAgentChathook (headless) - Styled components are thin wrappers you can use as-is or replace with your own UI
- Consumes newline-delimited JSON events from the
admin-agent-nextjsSSE endpoint
Installation
npm install @bernierllc/admin-agent-chat-ui react react-domQuick Start
// app/(admin)/layout.tsx
import {
AdminAgentProvider,
AdminAgentTrigger,
AdminAgentPanel,
} from '@bernierllc/admin-agent-chat-ui';
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<AdminAgentProvider url="/api/chat/admin">
{children}
<AdminAgentTrigger position="bottom-right" />
<AdminAgentPanel />
</AdminAgentProvider>
);
}This renders a Sparkles button fixed at the bottom-right. Clicking it opens a slide-in panel with a full streaming chat interface.
Headless Usage
Use only the hook and build your own UI:
import { AdminAgentProvider, useAdminAgentChat } from '@bernierllc/admin-agent-chat-ui';
function MyChat() {
const { messages, status, sendMessage, isOpen, toggle } = useAdminAgentChat();
if (!isOpen) return null;
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}</strong>: {msg.content}
{msg.toolResults?.map((tr) => (
<div
key={tr.toolCallId}
style={{ color: tr.success ? 'green' : 'red' }}
>
[{tr.toolName}] {tr.summary}
</div>
))}
{msg.changeSummary && (
<pre>{msg.changeSummary.markdown}</pre>
)}
</div>
))}
<p>Status: {status}</p>
<button onClick={() => void sendMessage('Hello!')}>Say hello</button>
</div>
);
}
export default function App() {
return (
<AdminAgentProvider url="/api/chat/admin">
<MyChat />
</AdminAgentProvider>
);
}SSE Stream Protocol
The provider POSTs to url with body { message: string } and reads newline-delimited JSON events:
// Each line is one JSON object followed by \n
{"type":"text-delta","delta":"Hello "}
{"type":"text-delta","delta":"world"}
{"type":"tool-call-start","toolCallId":"tc1","toolName":"addService"}
{"type":"tool-call-result","toolCallId":"tc1","success":true,"summary":"Added Haircut at $45","data":{...}}
{"type":"change-summary","summary":{...RollupSummary}}
{"type":"done"}
// Error path:
{"type":"error","message":"Something went wrong"}Components
<AdminAgentProvider>
Context provider. Must wrap all other components and useAdminAgentChat calls.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| url | string | Yes | — | SSE POST endpoint (e.g. /api/chat/admin) |
| headers | Record<string, string> | No | {} | Extra request headers (e.g. auth token) |
| children | React.ReactNode | Yes | — | Your app tree |
| fetchFn | typeof fetch | No | globalThis.fetch | Override fetch (useful for testing) |
<AdminAgentTrigger>
Floating action button that toggles the panel open/closed.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| position | 'bottom-right' \| 'bottom-left' | No | 'bottom-right' | Corner to position the button |
| icon | React.ReactNode | No | Sparkles SVG | Override the button icon |
| className | string | No | — | Extra CSS class |
<AdminAgentTrigger position="bottom-right" /><AdminAgentPanel>
Slide-in panel from the right edge. Renders nothing when closed.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| width | number \| string | No | 380 | Panel width (px number or CSS string) |
| className | string | No | — | Extra CSS class |
<AdminAgentPanel width={420} /><AdminAgentPanelHeader>
Header bar inside the panel with title and close button.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| title | string | No | 'Admin Agent' | Panel heading text |
| className | string | No | — | Extra CSS class |
<MessageList>
Scrollable list of message bubbles. Auto-scrolls to the latest message.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| messages | UIMessage[] | Yes | — | Messages to render |
| className | string | No | — | Extra CSS class |
<MessageBubble>
Single chat bubble. Delegates to <UserMessageContent> or <AssistantMessageContent> based on role, and renders <ToolResultCard> and <ChangeSummaryCard> overlays.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| message | UIMessage | Yes | The message to render |
| className | string | No | Extra CSS class |
<AssistantMessageContent>
Text content area styled for assistant messages.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| content | string | Yes | Text content (rendered with pre-wrap) |
| className | string | No | Extra CSS class |
<UserMessageContent>
Text content area styled for user messages (white text on indigo background).
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| content | string | Yes | Text content |
| className | string | No | Extra CSS class |
<AdminAgentInput>
Text input + send button for composing messages. Submits on Enter (Shift+Enter for newline) and disables while the agent is busy.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| placeholder | string | No | 'Message the admin agent…' | Input placeholder |
| disabled | boolean | No | false | Force-disable the input |
| className | string | No | — | Extra CSS class |
<AgentStatusBar>
Shows contextual status beneath the message list. Returns null when status is 'idle'.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| status | AgentStatus | Yes | — | Current agent status |
| executingTool | string | No | — | Tool name shown during 'executing' state |
| className | string | No | — | Extra CSS class |
Status labels:
| Status | Label |
|--------|-------|
| idle | (hidden) |
| thinking | Thinking… |
| executing | Executing <toolName>… |
| responding | Responding… |
| error | Error — retry? |
<ToolResultCard>
Green (success) or red (failure) card rendered per tool call. Expandable to show raw data.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| toolName | string | Yes | — | Name of the tool that ran |
| success | boolean | Yes | — | Whether the tool call succeeded |
| summary | string | Yes | — | One-line description of the change |
| data | unknown | No | — | Raw data to show when expanded |
| expanded | boolean | No | false | Start expanded |
<ToolResultCard
toolName="addService"
success={true}
summary="Added 'Haircut' at $45"
data={{ id: 42, name: 'Haircut', price: 45 }}
/><ChangeSummaryCard>
End-of-conversation rollup card. Collapsible. Renders RollupSummary.markdown from @bernierllc/agent-changeset.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| summary | RollupSummary | Yes | — | Rollup from agent-changeset |
| defaultExpanded | boolean | No | false | Start expanded |
Hook: useAdminAgentChat
import { useAdminAgentChat } from '@bernierllc/admin-agent-chat-ui';
const {
messages, // UIMessage[] — full conversation history
status, // AgentStatus — current state machine state
isOpen, // boolean — whether the panel is open
currentToolName, // string | null — tool executing right now
sendMessage, // (text: string) => Promise<void>
abort, // () => void — cancel in-flight request
toggle, // () => void — toggle open/closed
open, // () => void — open the panel
close, // () => void — close the panel
} = useAdminAgentChat();Must be called inside <AdminAgentProvider>. Throws AdminAgentUIError with code NO_PROVIDER otherwise.
Types
type AgentStatus = 'idle' | 'thinking' | 'executing' | 'responding' | 'error';
interface UIMessage {
id: string;
role: 'user' | 'assistant';
content: string;
toolResults?: UIToolResult[];
changeSummary?: RollupSummary; // from @bernierllc/agent-changeset
timestamp: string; // ISO 8601
}
interface UIToolResult {
toolName: string;
toolCallId: string;
success: boolean;
summary: string;
data?: unknown;
}Errors
import { AdminAgentUIError, AdminAgentStreamError } from '@bernierllc/admin-agent-chat-ui';
// Base error
class AdminAgentUIError extends Error {
readonly code: string; // e.g. 'UI_ERROR', 'NO_PROVIDER'
}
// Stream-specific error (extends AdminAgentUIError, code = 'STREAM_ERROR')
class AdminAgentStreamError extends AdminAgentUIError {}Both errors use Error.cause chaining for full stack traces.
State Machine
idle ──sendMessage──► thinking
thinking ────────────► executing (on tool-call-start)
executing ───────────► thinking (on tool-call-result, loop continues)
thinking ────────────► responding (on text-delta)
responding ──────────► idle (on done)
any ─────────────────► error (on error event or non-ok response)License
Copyright (c) 2025 Bernier LLC. Licensed to the client under a limited-use license.
