@anthive/webchat-react
v0.16.0
Published
Anthive webchat widget — embeddable chat powered by anthive workers
Readme
@anthive/webchat-react
Embeddable chat widget powered by anthive AI agents. Drop it into a React app as a component, or embed it in any website with a single <script> tag — no React required.
This package is the React component. Framework-agnostic sites should use the script tag instead — it needs no framework at all.
Getting a channel ID
Before using the widget you need a channel_id. This identifies which of your anthive agents answers the chat — it's a public identifier, safe to expose in client-side code (same as a Stripe publishable key or an Intercom app ID).
To create one:
- Log in to anthive.work.
- Go to Canales de Comunicación.
- Click Agregar canal and select Web Chat.
- Copy the
channel_idshown for the new channel.
Option 1: Script tag (any website)
No build step, no framework required.
<script
src="https://anthive.work/widget/widget.js"
data-channel-id="YOUR_CHANNEL_ID"
data-primary-color="#7C3AED"
data-background-color="#ffffff"
defer
></script>Paste this before the closing </body> tag. The widget mounts itself as a floating chat bubble in the bottom-right corner. data-primary-color and data-background-color are optional.
Option 2: React component
npm install @anthive/webchat-reactimport { AnthiveWebchat } from '@anthive/webchat-react';
function App() {
return <AnthiveWebchat channelId="YOUR_CHANNEL_ID" primaryColor="#7C3AED" backgroundColor="#ffffff" />;
}React and React DOM (>=18) are peer dependencies — bring your own, they aren't bundled with this build.
Props
| Prop | Type | Required | Description |
| ------------------------ | -------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| channelId | string | Yes | The channel_id of your Web Chat channel. |
| primaryColor | string | No | Hex color (e.g. #7C3AED) for the accent — bubble, send button, user messages. Defaults to anthive's lime. Icon/text contrast on top of it is computed automatically. |
| backgroundColor | string | No | Hex color for the panel background. Surface shades and text color are derived automatically for readable contrast. Defaults to anthive's dark theme. |
| layout | 'floating' \| 'sidenav' | No | floating (default) is the bubble + popover. sidenav docks the panel full-height against one edge. See Layout. |
| side | 'right' \| 'left' | No | Which edge the widget lives on. Defaults to right. Moves both the panel and the bubble. |
| variant | 'detached' \| 'flush' | No | Only with layout="sidenav". detached (default) leaves a gap around the panel; flush runs it edge to edge. Ignored when floating. |
| behavior | 'overlay' \| 'push' | No | Only with layout="sidenav". overlay (default) floats above your page; push moves your content aside. See Push. |
| allowLayoutToggle | boolean | No | Shows a button in the panel header that flips floating ↔ sidenav and remembers the choice. false by default. |
| allowNewChat | boolean | No | Muestra un botón "+" en el header que, con confirmación, empieza una conversación nueva y limpia el contexto visible. false por defecto. |
| showToolActivity | boolean | No | Muestra bajo el header las herramientas que ejecuta el agente. false por defecto. Ver Actividad de herramientas. |
| showContextMeter | boolean | No | Muestra en el header la píldora de uso de contexto (tokens / turnos, con opción de comprimir o reiniciar). true por defecto; ponlo en false para ocultarla. |
| hideBranding | boolean | No | Saca el footer "Powered by anthive.work" del panel. false por defecto. Ver Whitelabel. |
| brandingText | string | No | Reemplaza el texto del footer por la marca propia. Ver Whitelabel. |
| brandingUrl | string | No | Destino del footer. Sólo se usa junto a brandingText, y sólo si es http(s). Ver Whitelabel. |
| onToolActivity | (event: ToolExecutionEvent) => void | No | Cada ejecución de herramienta del agente, en vivo. Ver Actividad de herramientas. |
| onConsentDecision | (decision: ConsentDecision) => void | No | Cada permiso que el usuario aprueba o rechaza. Ver Permisos de herramientas. |
| allowPersistentConsent | boolean | No | Deprecada, sin efecto desde 0.16.0. El backend ya no ofrece "Permitir siempre". Ver Permisos. |
| debug | boolean | No | Abre el inspector de desarrollo en el header. false por defecto. Ver Inspector. |
| onReady | (event: WebchatReadyEvent) => void | No | El chat quedó creado y el backend lo confirmó, una vez por sesión. Ver Eventos. |
| onOpen | (event: WebchatOpenEvent) => void | No | El panel pasó a visible. Ver Eventos. |
| onClose | (event: WebchatCloseEvent) => void | No | El panel pasó a oculto. Ver Eventos. |
| onMessage | (event: WebchatMessageEvent) => void | No | El usuario envió, el agente cerró un bloque, o un operador escribió. Ver Eventos. |
| onError | (event: WebchatErrorEvent) => void | No | Falló la creación del chat o el agente. Ver Eventos. |
If channelId is omitted or empty, the widget renders nothing. The agent's display name is set per-channel in anthive.work (Canales de Comunicación) and shown in the widget header automatically.
Layout
By default the widget renders as an overlay. It mounts its own <div> on <body> at position: fixed; z-index: 9999 and never writes to your :root, mutates your <body>, or reflows your page — that holds for sidenav just as much as for floating.
The single exception is behavior="push", which exists precisely to move your content aside; it is opt-in and covered in Push.
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" layout="sidenav" side="left" variant="flush" /><script
src="https://anthive.work/widget/widget.js"
data-channel-id="YOUR_CHANNEL_ID"
data-layout="sidenav"
data-side="left"
data-variant="flush"
data-behavior="push"
data-allow-layout-toggle
data-allow-new-chat
defer
></script>Unrecognised values fall back to the default rather than breaking the widget. The boolean attributes — data-allow-layout-toggle, data-allow-new-chat, data-show-tool-activity, data-hide-branding and data-debug — count as on by their mere presence; only ="false" opts out of each. data-show-context-meter is the exception: the context-usage pill ships on, so only data-show-context-meter="false" matters — it hides it.
Notes on sidenav:
- Width is the
--aw-sidenav-widthCSS variable (default400px), not a prop — override it from your own stylesheet:.anthive-webchat-panel--sidenav { --aw-sidenav-width: 480px; } - The bubble hides while the panel is open, since a full-height rail covers the corner it sits in. Closing happens from the header's ✕.
- On screens ≤639px the panel goes full screen regardless of
sideandvariant. A 400px rail doesn't fit a 375px viewport. This is not configurable. - No keyboard or focus listeners. The widget does not bind
Escapeor trap focus, because both require listening ondocument— exactly the kind of thing that interferes with the host page.
layout defaults to floating, so existing embeds are unaffected by this addition.
Letting the user switch layouts
allowLayoutToggle puts a button in the panel header that flips floating ↔ sidenav:
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" layout="sidenav" allowLayoutToggle />It is off by default on purpose: a host that deliberately picked a layout shouldn't have end users overriding it. When on, the choice is stored in localStorage under anthive-webchat-layout and takes precedence over the layout prop on later visits. Clearing that key restores your default.
Push: moving your content aside
behavior="push" is the only mode that touches your page. Instead of covering the content, the widget adds a margin to <body> while the sidenav is open and removes it on close:
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" layout="sidenav" behavior="push" />The margin matches the panel's real width, including the detached gap and any --aw-sidenav-width you set, so it stays correct without extra configuration. Before turning it on, know that:
- Your
position: fixedelements do not move. Sticky headers, cookie bars and your own floating buttons are positioned against the viewport, not the body, so the sidenav will overlap them. If you have any, either add the margin to them yourself whilebody.anthive-webchat-pushed--right/--leftis present, or stay onoverlay. - Full-bleed
100vwelements will overflow, because the viewport stays the same width while the body narrows. Usewidth: 100%for those. - It reflows your page on open. That is the whole point of the mode, but it means layout-sensitive widgets in your page re-run their measurements.
- Below 640px it does nothing. The panel is full screen there, so pushing would shove your content out of view.
overlay remains the default and leaves every one of these concerns untouched.
Whitelabel
El panel cierra con un footer que dice "Powered by anthive.work" y enlaza a nuestro sitio. Podés sacarlo o reemplazarlo por tu marca.
Sacarlo. hideBranding no renderiza el footer; el espacio se lo queda la
lista de mensajes.
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" hideBranding />Reemplazarlo. brandingText y brandingUrl ponen tu marca en su lugar.
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" brandingText="Powered by ACME" brandingUrl="https://acme.com" /><script
src="https://anthive.work/widget/widget.js"
data-channel-id="YOUR_CHANNEL_ID"
data-branding-text="Powered by ACME"
data-branding-url="https://acme.com"
defer
></script>Las tres props se resuelven con estas reglas:
hideBrandinggana sobre las otras dos. Entrueno hay footer aunque pases texto o url.- El texto y la url son un par.
brandingTextsolo deja el footer sin link, porque enlazar "Powered by ACME" a anthive.work sería engañoso.brandingUrlsolo, sin texto propio, se ignora. - La url tiene que ser
http:ohttps:. Cualquier otro protocolo, o algo que no parsee como url, deja el footer sin link en vez de escribirlo en el DOM.
Permisos de herramientas
Algunas herramientas están detrás de una compuerta de consentimiento: antes de usarlas el agente suspende el turno y pide permiso. Mientras nadie responda, el turno queda parado — no es un cuelgue, es una pregunta esperando respuesta.
El widget muestra esa pregunta en un panel fijo justo encima del composer, con el nombre de la herramienta, qué agente la pide y (desplegable) los parámetros con los que correría. No hace falta configurar nada: si el backend pide permiso, el widget lo muestra.
- Varias solicitudes a la vez. Un worker que delega en cinco sub-agentes abre cinco permisos sobre el mismo chat. Los que piden lo mismo se agrupan en una sola tarjeta: un toque responde a todos.
- Sobrevive a un reload. Los permisos sin responder se guardan en
localStoragey se restauran al recargar la página, así que una recarga no vuelve a dejar el turno colgado. - No existe "Permitir siempre". El backend ofrece tres opciones —
approve_once,approve_session(todo el chat, hasta que termine la sesión) ydeny— y ninguna es permanente. El permiso durable es configuración del admin (por agente, o política de equipo), no algo que se conceda desde un chat. El widget dibuja exactamente losoptionsque manda el backend, así que no hay chip que prometa un permiso que no se da.allowPersistentConsentquedó deprecada y no hace nada. - El botón principal es siempre el permiso más chico que el backend ofrezca, y dice cuál alcance concede ("Permitir", "Permitir en esta conversación"). Los alcances más amplios quedan en una fila aparte, bajo la frase que los explica.
- Los argumentos se muestran como filas (
to,subject, …), no como JSON: quien decide no es necesariamente quien escribió la herramienta. El payload crudo queda a un clic. - Aviso de escritura. Cuando el nombre de la acción contiene un verbo que modifica
algo (
send,create,delete, …) la tarjeta muestra un chip ámbar que lo dice con palabras. Es deliberadamente unilateral: avisamos cuando reconocemos un verbo de escritura y nos quedamos callados si no. Nunca afirmamos "solo lectura" — equivocarse en esa dirección sí tiene costo.
<AnthiveWebchat
channelId="YOUR_CHANNEL_ID"
onConsentDecision={(decision) => {
console.log(decision.toolKey, decision.value); // 'gmail.send', 'approve_once'
}}
/>Inspector de desarrollo
Un panel dentro del widget para ver qué está pasando: estado de la sesión y del socket, permisos aprobados/rechazados, y el log completo de frames que entran y salen (expandibles, con un botón para copiar todo a un reporte de bug).
Se activa de dos formas:
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" debug />// O sobre un embed ya desplegado, desde la consola del navegador:
localStorage.setItem('anthive-webchat-debug', '1');Con data-debug en el script tag también funciona. El token del chat se registra
redactado. Mantenelo apagado en producción: muestra frames crudos.
Preguntas sugeridas
Sobre una conversación recién empezada, el panel puede ofrecer atajos: botones con una pregunta típica que, al tocarlos, mandan un mensaje ya escrito.
No se configuran desde el widget. Son los examples del agente, definidos en
anthive; el widget sólo los muestra. No hay prop para pasarlos ni para editarlos:
si querés cambiarlos, se cambian en el agente y todos los canales los heredan a la
vez — el panel del dashboard usa exactamente los mismos.
Cada sugerencia tiene esta forma, y la distinción entre los dos primeros campos importa:
interface SuggestedMessage {
title: string; // lo que se lee en el botón
content: string; // lo que se manda al tocarlo
category: string; // lo manda el backend; el widget no lo usa
}Es decir: el botón puede decir "Ver mis agentes" y mandar "Mostrame la lista
completa de agentes de mi equipo". El tipo se exporta (import type { SuggestedMessage })
por si necesitás tiparlas de tu lado.
Cuándo aparecen y cuándo no:
- Sólo en un hilo fresco: cero mensajes tuyos y, como mucho, un mensaje del agente. Un saludo inicial no las bloquea; tu primer mensaje sí.
- Se van con el primer mensaje que mandás, sea desde el composer o tocando una sugerencia, y no vuelven en esa conversación.
- Vuelven al empezar una conversación nueva (el botón
+, verallowNewChat). - No reaparecen si se corta y vuelve la conexión a mitad de conversación.
Llegan por el frame connected del WebSocket, así que funcionan igual en modo
público y en modo autenticado, sin que tu backend tenga que reenviarlas. Un backend
que todavía no las mande simplemente no muestra atajos: no es un error.
El frame connected
Vale conocerlo si integrás en modo autenticado. Es el primer frame que manda el backend al aceptar el socket, y describe el estado del agente al conectar: su nombre, y las sugerencias de arriba.
Dos propiedades que conviene tener presentes:
- Tu backend no lo intermedia. El WebSocket va directo contra anthive; vos
proxeás la sesión, no el socket. Por eso lo que describe al agente viaja por acá
y no por
sessionProvider: cambiarlo no te obliga a tocar tu backend. - Llega de nuevo en cada reconexión, así que todo lo que transporta es estado declarativo, nunca un evento. El widget lo aplica de forma idempotente: volver a recibirlo no repone algo que ya no corresponde.
Lo que decide el agente viaja en este frame; lo que decide la página que embebe (layout, lado, colores, whitelabel del footer) son props del componente.
Detener y reintentar
Mientras el agente responde, el botón de enviar se convierte en un botón de detener. Detener corta la espera del lado del cliente: el backend no expone un frame de cancelación, así que el turno sigue corriendo en el servidor y su resultado se descarta. Por eso, justo debajo aparece "Volver a intentar", que reenvía tu último mensaje tal cual.
Actividad de herramientas
El agente puede ejecutar herramientas para responder (consultar una API, buscar en una base). El widget expone esas ejecuciones de dos formas independientes: un callback para tu código y un indicador opcional en el panel.
El callback
<AnthiveWebchat
channelId="YOUR_CHANNEL_ID"
onToolActivity={(event) => {
if (event.phase === 'completed' && event.action === 'crear_ticket') {
refrescarTickets();
}
}}
/>Dispara una vez por frame, en vivo. El objeto es el frame tal como llega por el socket:
{
type: 'tool_execution',
phase: 'started' | 'completed' | 'failed',
execution_id: string, // correlaciona started con su cierre
tool_key: string, // el proveedor, p.ej. 'levannta'
action: string, // la operación, p.ej. 'companies_list_tool'
agent: { id: string, name: string },
sub_chat_id: string | null,
timestamp: string,
params?: Record<string, unknown>, // solo en started
params_truncated?: boolean,
ok?: boolean, // completed / failed
status_code?: number,
error?: string,
duration_ms?: number,
seq?: number,
replayed?: boolean,
}Dos cosas a tener en cuenta:
- Cada
(execution_id, phase)te llega una sola vez. Si el socket se reconecta y el backend reenvía frames, el widget los descarta. Podés disparar efectos sin preocuparte por duplicados. replayed: truesignifica que eso ya pasó, no que esté pasando ahora: llega al reconectar, para que puedas reconstruir historial. Si tu callback dispara acciones, filtralo.
El callback existe solo en el componente React. El embed por <script> no lo
recibe.
El indicador
showToolActivity agrega una franja bajo el header con un chip por ejecución:
◌ companies_list_tool mientras corre, ✓ companies_list_tool · 412 ms al
terminar, ✕ companies_list_tool · 401 si falla. Los chips cerrados se van solos
a los 3 segundos y se muestran como máximo 3 a la vez (+N para el resto).
<AnthiveWebchat channelId="YOUR_CHANNEL_ID" showToolActivity /><script
src="https://anthive.work/widget/widget.js"
data-channel-id="YOUR_CHANNEL_ID"
data-show-tool-activity
defer
></script>Está apagado por defecto: muestra el nombre técnico de la herramienta, que no
siempre es algo que quieras enseñarle al usuario final. Los argumentos
(params), el agente que ejecuta y el sub-chat nunca se pintan — solo viajan en
el callback.
Eventos
El widget publica cinco eventos de ciclo de vida. Ambas superficies disparan
en los dos modos: el componente React recibe las props y, a la vez, dispara el
CustomEvent en window — no solo en el embed por <script>. Si tu app React
también tiene un listener global de analytics en window, vas a contar cada
evento dos veces; usá una sola superficie o dedupealo por chatId/messageId.
onReady solo dispara después de la primera apertura del chat, no al
cargar la página: la sesión se crea recién cuando el usuario (o vos, via
control imperativo) abre el panel por primera vez. El
orden es siempre open → ready. Un onReady={() => chat.current?.open()}
nunca se ejecutaría, porque ready no llega hasta que ya abriste.
Solo ready lleva chatId en su detail. Si tu página tiene dos widgets
montados, un listener de window no puede saber a cuál instancia pertenece un
open, close o message — para eso necesitás las props de React, una por
instancia.
| Evento | Evento DOM | detail | Cuándo |
| ----------- | ------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| onReady | anthive-webchat:ready | { chatId, agentName } | El chat quedó creado y el backend lo confirmó, tras la primera apertura. Una vez por sesión. |
| onOpen | anthive-webchat:open | { reason: 'bubble' \| 'api' } | El panel pasó a visible. |
| onClose | anthive-webchat:close | { reason: 'bubble' \| 'panel-button' \| 'api' } | El panel pasó a oculto. |
| onMessage | anthive-webchat:message | { role, content, messageId, kind? } | El usuario envió, el agente cerró un bloque, o un operador escribió. |
| onError | anthive-webchat:error | { error, phase: 'init' \| 'agent' \| 'refresh' } | Falló la creación del chat (init), el agente (agent), o se agotaron los reintentos de renovación de sesión (refresh, solo modo autenticado). |
<AnthiveWebchat
channelId="YOUR_CHANNEL_ID"
onOpen={({ reason }) => analytics.track('chat_opened', { reason })}
onClose={({ reason }) => analytics.track('chat_closed', { reason })}
onMessage={({ role, content }) => console.log(role, content)}
/><script>
window.addEventListener('anthive-webchat:open', (e) => {
gtag('event', 'chat_opened', { reason: e.detail.reason });
});
</script>onMessage con role: 'agent' dispara varias veces por turno. El agente
manda un acuse rápido antes del trabajo pesado y después la respuesta final; cada
uno es un mensaje con su propio messageId. El acuse llega con
kind: 'quick_ack', así que filtralo si querés un solo evento por respuesta:
onMessage={({ role, kind, content }) => {
if (role === 'agent' && kind === 'quick_ack') return;
// …
}}messageId es el id del backend para role: 'agent' y role: 'human'. Para
role: 'user' es un id local del widget, porque el evento se emite en el momento
del envío: sirve para correlacionar dentro de la sesión, no contra la base.
Una desconexión no emite onError — el widget reconecta solo con backoff.
Control imperativo
Para abrir el chat desde tu propio UI (un botón "¿Necesitás ayuda?", el final de un formulario, un timer).
import { useRef } from 'react';
import { AnthiveWebchat, type AnthiveWebchatHandle } from '@anthive/webchat-react';
function Page() {
const chat = useRef<AnthiveWebchatHandle>(null);
return (
<>
<button onClick={() => chat.current?.open()}>¿Necesitás ayuda?</button>
<AnthiveWebchat ref={chat} channelId="YOUR_CHANNEL_ID" />
</>
);
}En el embed por <script> la misma API vive en window:
<button onclick="window.AnthiveWebchat.open()">¿Necesitás ayuda?</button>window.AnthiveWebchat existe desde que el script se evalúa, antes de que el
widget monte, así que una llamada temprana no se pierde: se aplica al montar.
Las tres acciones emiten sus eventos con reason: 'api', para distinguir una
apertura que provocaste vos de una del usuario.
Authenticated mode (embed in your own app)
Use this when the chat runs inside your product, tied to your logged-in user, and you want the agent's structured output back in your code — for example, translating "traeme todos los créditos aprobados del mes" into a JSON of report filters your app applies to its own report.
The public mode above identifies your agent with a channelId that is safe to expose. Authenticated mode is different: it is bound to your anthive API key, a specific agent, and a specific end user. The API key must never reach the browser, so the widget never sees it. Instead your backend exchanges the key for a short-lived (15 min) token, and the widget connects with that.
1. Your backend: exchange the API key for a session token
Add one endpoint to your backend. It authenticates your own user (your session, your rules), then calls anthive with your API key from the server environment. Route on whether the widget is asking for a fresh conversation (newSession) or its first/renewed session:
POST /api/anthive/session (your backend, your route name)
body: { newSession?: boolean }
1. authenticate your logged-in user
2. POST https://anthive.work/api/public/v1/embed/sessions/new if body.newSession
POST https://anthive.work/api/public/v1/embed/sessions otherwise
X-API-Key: <ANTHIVE_API_KEY> // from your server env, never the client
Content-Type: application/json
{ "agent_id": "<your-agent-slug>", "end_user_id": "<your user's id>" }
3. return { token, chatId, expiresAt, initialMessages, resumed } to your frontendanthive validates the key, checks that the agent belongs to your team, creates or resumes the chat, and responds:
{
"token": "<15-min JWT>",
"chat_id": "...",
"agent_name": "...",
"expires_at": "...",
"messages": [
/* only when resuming */
],
"resumed": true
}/embed/sessions resumes the end user's most recent conversation by default, returning its visible history in messages; /embed/sessions/new always starts a fresh one. Map the response to the widget's WebchatSession shape — chat_id → chatId, expires_at → expiresAt, messages → initialMessages — when you return it. Forwarding messages is an optimization, not a requirement: a provider that returns only { token, chatId } still gets the full thread, because the widget fetches the history itself with the chat token.
This endpoint needs an API key with the embed:sessions permission — and nothing else. Follow Crear una API key to create one, checking Crear sesiones de embed.
Revoking the key immediately stops new sessions; tokens already issued expire on their own within 15 minutes.
2. Your frontend: mount the widget in authenticated mode
import { AnthiveWebchat } from '@anthive/webchat-react';
function Report() {
return (
<AnthiveWebchat
mode="authenticated"
sessionProvider={async (opts) => {
const r = await fetch('/api/anthive/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(opts ?? {}), // forwards { newSession } straight through
});
return r.json(); // { token, chatId, expiresAt?, initialMessages?, resumed? }
}}
onResult={(filters) => {
// filters is the agent's structured output, already validated
// against your agent's schema. Apply it to your report.
applyReportFilters(filters);
}}
/>
);
}sessionProvider is called when the chat opens, again whenever the widget starts a new conversation via the allowNewChat prop below (with { newSession: true }), and — if you return expiresAt — again on every automatic token renewal, with no arguments at all, indistinguishable on your end from an initial open. Make sure your implementation is safe to call repeatedly: if it's rate-limited, audited, or logs "user opened chat", a renewal will trigger that too. The token travels to anthive as the first WebSocket frame, never in a URL.
Automatic renewal itself is opt-in and driven by what you return:
expiresAtis what enables automatic renewal. Return the ISO 8601 timestamp your backend already gets from anthive (expires_at) and the widget schedules a renewal itself — 60 seconds before the token expires, retrying at 2s and 8s if the first attempt fails. WithoutexpiresAtthe widget renews nothing: it never inspects the JWT or guesses an expiry, so an existing provider that ignores this field keeps behaving exactly as before.- You write no scheduling code. The widget owns the timer, the retries, and re-authenticating the live socket with the new token — a turn that's mid-stream isn't interrupted, and the socket only reconnects if the renewed session comes back with a different
chatId. initialMessagespaints the resumed thread — and is optional./embed/sessionsresumes the end user's most recent conversation by default; forward themessagesarray your backend already receives and the widget renders it immediately, with no extra round trip. Omit it and the widget asks for the history itself (GET /v1/embed/messages, authenticated with the same chat token), so a provider that returns only{ token, chatId }no longer loses the thread.resumedis purely informational if you want to log or display it.- If every retry fails, the widget doesn't crash — it goes read-only.
onErrorfires withphase: 'refresh', the composer disables, the message history stays visible, and a "Reconectar" button in the panel retries the renewal from scratch.
Authenticated-mode props
| Prop | Type | Required | Description |
| ------------------- | -------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| mode | 'authenticated' | Yes | Selects authenticated mode. Omitting mode (or 'public') keeps the channelId behavior above. |
| sessionProvider | (opts?: { newSession?: boolean }) => Promise<WebchatSession> | Yes | Resolves a session from your backend. Called on open, again with { newSession: true } from allowNewChat, and again with no arguments on every automatic renewal — must be safe to call repeatedly. See below for the refresh contract. |
| onResult | (result: unknown) => void | No | Fires with the agent's structured output (e.g. the filters JSON). Wire it into your app. |
| primaryColor | string | No | Same as public mode. |
| backgroundColor | string | No | Same as public mode. |
| layout | 'floating' \| 'sidenav' | No | Same as public mode. See Layout. |
| side | 'right' \| 'left' | No | Same as public mode. |
| variant | 'detached' \| 'flush' | No | Same as public mode. |
| behavior | 'overlay' \| 'push' | No | Same as public mode. |
| allowLayoutToggle | boolean | No | Same as public mode. |
| allowNewChat | boolean | No | Same as public mode. Calls sessionProvider({ newSession: true }) instead of minting a local id. |
| showToolActivity | boolean | No | Same as public mode. |
| showContextMeter | boolean | No | Same as public mode. true by default; set false to hide the context-usage pill. |
| hideBranding | boolean | No | Same as public mode. See Whitelabel. |
| brandingText | string | No | Same as public mode. See Whitelabel. |
| brandingUrl | string | No | Same as public mode. See Whitelabel. |
| onToolActivity | (event: ToolExecutionEvent) => void | No | Same as public mode. See Actividad de herramientas. |
| onReady | (event: WebchatReadyEvent) => void | No | Same as public mode. See Eventos. |
| onOpen | (event: WebchatOpenEvent) => void | No | Same as public mode. See Eventos. |
| onClose | (event: WebchatCloseEvent) => void | No | Same as public mode. See Eventos. |
| onMessage | (event: WebchatMessageEvent) => void | No | Same as public mode. See Eventos. |
| onError | (event: WebchatErrorEvent) => void | No | Same as public mode. See Eventos. |
The shape of result is defined by the agent's configuration in anthive (its filter schema), not by this package — the widget passes it through untouched.
Publishing (maintainers)
The latest version on npm is 0.6.0 (0.3.0, 0.4.0, 0.5.0 and 0.5.1 are also published). This source tree is versioned 0.7.0 — session refresh, resumed-thread history and the new-chat button — and publishes on the next merge to main.
release.yml runs changesets/action on every push to main: with pending changesets it opens a "Version Packages" PR, and with none left it publishes the current version to npm. ci.yml is separate — it only builds the standalone bundle that anthive.work/widget/widget.js serves, which is what the script-tag embed uses, so deploying the webapp updates that bundle but leaves the npm package untouched.
Publishing by hand from a maintainer's machine still works if CI is unavailable:
npm publish --workspace=packages/webchat # prepublishOnly builds; npm prompts for your OTPThe README users see on npmjs.com is the one bundled in the published tarball, so documentation changes only reach them on the next publish.
Still pending: the package was renamed from @anthive/webchat to free the un-suffixed name for a future non-React build, but the old name is not deprecated yet — npm deprecate @anthive/webchat "Renamed to @anthive/webchat-react".
License
MIT
