Agentoria

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 haveUsePackage
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):

PropDefault
position"bottom-right"or "bottom-left"
title"Chat"accessible name for the launcher + dialog
headertitlecustom header node; null hides the header
defaultOpen / open / onOpenChangecloseduncontrolled or controlled
launcherLabela chat glyphFAB content
unreadMax9badge 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
greetingseed one assistant message on first open
headerExtrayour DOM node in the header (a status strip, actions…)
toolLabelactivity 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 })
labelslocalize 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.

On this page