Floating chat
A launcher-plus-popover chat — a batteries-included React component, a headless controller, or a zero-build vanilla widget.
A floating chat is the corner bubble (FAB) that opens a docked chat popover. agentoria ships it at three levels, so you pick the one that fits your app:
| You have | Use | Package |
|---|---|---|
| A React app, want it to just work | <FloatingChat> | @agentoria/ui |
| A React app, but a custom shell (drawer / modal / sidebar) | useFloatingChat | @agentoria/react |
| No build step / strict CSP (static, SSR, Workers) | AgentoriaWidget | @agentoria/embed |
All three share the same behavior — open/close, Escape, outside-click, focus management, and an unread badge for replies that land while closed. It's the same "headless logic + concrete adapters" split as useAgentChat → <ChatPanel>.
FloatingChat (React)
The 90% case: a corner FAB that opens a popover wrapping <ChatPanel>. Every ChatPanel prop is forwarded, so you keep the full panel API (markdown, tool trace, metadata, feedback, generative UI):
'use client';
import { FloatingChat, AgentoriaStyles } from '@agentoria/ui';
export default function App() {
return (
<>
<AgentoriaStyles />
<FloatingChat endpoint="/api/agent" title="Assistant" />
</>
);
}Floating-specific props (everything else is forwarded to ChatPanel):
| Prop | Default | |
|---|---|---|
position | "bottom-right" | or "bottom-left" |
title | "Chat" | accessible name for the launcher + dialog |
header | title | custom header node; null hides the header |
defaultOpen / open / onOpenChange | closed | uncontrolled or controlled |
launcherLabel | a chat glyph | FAB content |
unreadMax | 9 | badge clamp (9 → "9+") |
Drive it with your own useAgentChat instance (via the chat prop) when the app needs the live transcript — e.g. to persist it or react to tool events.
useFloatingChat (headless)
When the popover shape isn't enough — a slide-over drawer, a modal, a docked sidebar, or a panel with its own chrome (a plan bar, a save bar) — drop to the headless controller and render your own markup. It owns behavior, not styles:
'use client';
import { useFloatingChat, useAgentChat, ChatPanel } from '@agentoria/ui';
export function Assistant() {
const chat = useAgentChat({ endpoint: '/api/agent' });
const replies = chat.messages.filter((m) => m.role === 'assistant').length;
const fc = useFloatingChat({ activityCount: replies }); // unread derives from this
return (
<div className="fab-root">
{fc.open && (
<div {...fc.panelProps} className="panel">
<header>
<button onClick={fc.close}>✕</button>
</header>
<ChatPanel chat={chat} />
</div>
)}
<button {...fc.launcherProps} className="fab">
💬{fc.unread > 0 && <span className="badge">{fc.unread}</span>}
</button>
</div>
);
}useFloatingChat(options) returns { open, setOpen, toggle, close, unread, launcherProps, panelProps }. Spread launcherProps onto your button and panelProps onto your panel container. Options: defaultOpen, open / onOpenChange (controlled), closeOnEscape, closeOnOutsideClick, manageFocus, and activityCount (feed it a monotonic count — e.g. assistant replies — and it tracks how many arrived while closed as unread).
AgentoriaWidget (zero-build)
For apps that can't run React — static sites, server-rendered pages, a strict default-src 'self' CSP. A dependency-free vanilla widget that renders the agent turn as richly as ChatPanel, in plain DOM: live Markdown, an expandable tool-call trace (args · result · ok/err · ms), per-message token metadata (latency · tokens · cost), and feedback (👍/👎/free-text) on each answer and each tool call.
Load the SSE client + the widget + its stylesheet, all same-origin:
<link rel="stylesheet" href="/assets/css/agentoria-widget.css" />
<script src="/assets/js/agentoria-chat.global.js"></script> <!-- AgentoriaChat.connectAgent -->
<script src="/assets/js/agentoria-widget.global.js"></script>
<script>
AgentoriaWidget.mount({
endpoint: '/api/agent',
title: 'Assistant',
greeting: 'Hi! Ask me anything.',
onFeedback: (fb) => navigator.sendBeacon('/api/feedback', JSON.stringify(fb)),
});
</script>Vendor the two assets + the stylesheet from the package's browser/ folder into your static assets. mount(opts) returns a handle — { el, sessionId, open(), close(), toggle(), destroy(), addMessage(role, text), setLocked(bool) }.
The widget is a reusable base; apps layer their own chrome on top via hooks:
| Option | |
|---|---|
greeting | seed one assistant message on first open |
headerExtra | your DOM node in the header (a status strip, actions…) |
toolLabel | activity label while a tool runs |
onOpen(api, firstOpen) | lazy-load state on first open |
onError(msg, status, errorEl) | augment/handle a failed turn (e.g. a 402 upgrade button) |
onFeedback(fb) | persist ratings — { kind, rating, note?, sessionId, turn, toolId?, toolName?, text? } |
buildBody(message, history) | shape the POST body (default { message, history, sessionId }) |
labels | localize the args/result/feedback strings |
Every mount gets a sessionId (on the handle and in the default request body) so feedback — and any server-side action — can be tied back to the conversation.
Theming
No inline styles are set, so the stylesheet loads under a strict CSP. The --ao-* design tokens are declared on the widget root and inherit down, so a single rule re-themes the whole widget:
[data-agentoria="floating"] {
--ao-accent: #8a5a3b;
--ao-bg: #fff;
--ao-radius: 16px;
/* map to your app's tokens, e.g. --ao-accent: var(--brand); */
}AgentoriaWidget.renderMarkdown(el, src) is also exported on its own if you want the safe Markdown→DOM renderer elsewhere.