All docs

Developer experience

React widget

Drop-in React FAB chat widget — three identity flows (anonymous form, OTP-verified, backend-authenticated), every per-turn agent option exposed, fully themable.

React widget

@platosdev/react-widget is a drop-in floating chat bubble for any React 18+ app — Next.js, Vite, CRA, Remix, anywhere. Three identity flows out of the box, every per-turn agent option exposed, fully themable via CSS variables, and a headless usePlatosChat hook for callers that want their own UI.

What it is

A single React component, <PlatosFab>, that renders:

  • A circular floating-action button anchored to a corner of the page (default: bottom-right)
  • A chat panel that slides in on click
  • An identity form (name + email; optionally OTP-verified) before chat begins, OR a pass-through when your app already knows the user
  • Streaming assistant responses powered by @platosdev/client over Socket.IO

Plus a headless usePlatosChat hook for fully custom layouts.

Why it matters

Self-hosters previously had two options for embedding a Platos agent into a customer-facing app:

  1. @platosdev/embed — iframe-based web component. Works in any HTML page but doesn't integrate with the host app's auth, layout, or styling system.
  2. @platosdev/client — raw SDK. Maximum flexibility, but you build the UI from scratch.

The React widget fills the gap: native React, integrates with the host app's session, themable to match any design system, and ships a sensible default UI that customers can install in five lines of code.

How to use it

Install

npm install @platosdev/react-widget
# or
pnpm add @platosdev/react-widget

Anonymous public agent — minimum setup

"use client";
import "@platosdev/react-widget/styles.css";
import { PlatosFab } from "@platosdev/react-widget";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <PlatosFab
        baseUrl="https://platos.example.com"
        agentId="agt_xxx"
        tokenUrl="/api/platos-session"
      />
    </>
  );
}

Visitor sees a name/email form, submits, the widget POSTs to /api/platos-session (your route), your backend mints a session token signed with your entity's serviceSecret (using @platosdev/token-mint or any JWT lib), the widget streams chat with the resulting JWT.

The visitor's name + email travel as the userMeta claim in the JWT and surface as {{user.name}} / {{user.email}} in the agent's prompt — no code change needed if your agent already references those.

Anonymous + email-verified (OTP gate)

Add verifyEmail and point at customer-side OTP endpoints. The widget orchestrates the form → send-code → verify-code → chat flow; your backend handles the actual code generation, hashing, storage, and email send via Resend (or any other transport).

<PlatosFab
  baseUrl="https://platos.example.com"
  agentId="agt_xxx"
  tokenUrl="/api/platos-session"
  verifyEmail
  otpEndpoints={{
    sendUrl:   "/api/platos-otp/send",
    verifyUrl: "/api/platos-otp/verify",
  }}
/>

The README in packages/platos-react-widget/ ships paste-ready Next.js Route Handlers for the OTP send + verify endpoints using Resend + Redis.

Backend-authenticated — your app already knows the user

Skip the form entirely. Two paths:

// Path A — pass identity, widget still uses tokenUrl
<PlatosFab
  baseUrl="..." agentId="..." tokenUrl="/api/platos-session"
  identityMode="preset"
  identity={{ name: session.user.name, email: session.user.email }}
/>

// Path B — pass a token your server already minted
<PlatosFab
  baseUrl="..." agentId="..."
  sessionToken={mySessionToken}
/>

Path A is preferred for long sessions (the widget auto-refreshes on 401 via tokenUrl). Path B is fine for short-lived embeds.

Per-turn options

Every variable the agent's Socket.IO turn endpoint accepts is exposed via the perTurn prop and passed unchanged on every message:

<PlatosFab
  ...
  perTurn={{
    dynamicBlocks: { product_context: "User is on the pricing page." },
    modelLabel: "fast",
    contextType: "entity",
    contextId: "shopify-store-acme",
    attachmentIds: ["att_123"],
    sessionContextOverride: { entity_ids: ["acme-prod"], role: "manager" },
  }}
/>

systemPromptOverride is HTTP-only on the agent today (not exposed by the streaming WS path). Use the agent's stored systemPrompt and rotate via the dashboard for now.

Theming

Three levels:

  • CSS variables: set --platos-color-primary, --platos-color-bg, --platos-radius, etc. on .platos-widget-root or globally. The widget defines defaults for both light and dark via prefers-color-scheme.
  • themeTokens prop: per-instance overrides without a stylesheet edit.
  • classNames prop: pass-through className slots for fab, panel, header, messages, assistantBubble, userBubble, inputArea, input, sendButton, identityForm. Use with Tailwind / CSS Modules / styled-components.

Headless — bring your own UI

import { usePlatosChat } from "@platosdev/react-widget";

function CustomChat() {
  const { messages, send, status, error } = usePlatosChat({
    baseUrl: "...",
    agentId: "...",
    tokenUrl: "...",
    identity: { name, email },
    perTurn: { modelLabel: "fast" },
  });
  // render however you want
}

Hotkey

⌘K / Ctrl+K toggles the panel; Esc closes. Disable with hotkey={false}.

Common pitfalls

  • No backend for tokenUrl: the widget can't talk directly to Platos with raw entity credentials — the browser must never hold a serviceSecret. The token-mint MUST live on your server. Use @platosdev/token-mint for the JWT signing primitive.
  • Cross-origin: when running the widget on a domain different from your Platos deployment, either add the origin to your connected entity's allowedOrigins, OR set PLATOS_CORS_UNIVERSAL=true on the agent for hosted-demo flexibility.
  • agentId must be public-guest enabled for identityMode="anonymous" to work without a token. For all other identity modes, the agent's visibility doesn't matter — your token-mint backend authorises the call.
  • Avatar images: avatar accepts a URL string OR a React node. URL strings render as <img> and need to be on a CORS-friendly host or same-origin.
  • Markdown rendering: v0.1 renders assistant bubbles as plain text. Wrap with classNames.assistantBubble and your own markdown lib (e.g. react-markdown) if you need rich formatting; v0.2 will ship it built-in.
  • SDKs: the underlying @platosdev/client the widget builds on.
  • Public agents and embed: the iframe-based alternative for non-React contexts.
  • Auth modes: the session-token + userMeta claim the widget uses.
  • Email skill: paired well with the OTP flow — the agent can send the verification email itself.

Talk to Platos

Powered by the Platos runtime

Powered by Platos →