@basegent/react
v0.2.9
Published
React hooks and components for embedding Basegent support chat
Maintainers
Readme
@basegent/react
React hooks and components for embedding Basegent support chat in any React or Next.js app.
Two integration styles, same package:
- Headless — use hooks (
useChat,useLiveSession,useWidget) with your own UI - Drop-in — use pre-built Tailwind v4 + shadcn-pattern components (
BasegentChat,BasegentMessages,BasegentInput,BasegentFAB)
Installation
npm install @basegent/react react react-dom
# or
pnpm add @basegent/react react react-domQuick Start (Drop-in)
With Tailwind v4 (recommended)
Add the package source to your Tailwind input so its utility classes are included:
/* app/globals.css */
@import "tailwindcss";
@source "../../node_modules/@basegent/react/dist";Then wrap your layout:
// app/layout.tsx
import { BasegentProvider, BasegentChat } from "@basegent/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<BasegentProvider tenantId="t_your_tenant_id">
{children}
<BasegentChat title="Support" placeholder="Ask anything…" />
</BasegentProvider>
</body>
</html>
);
}Without Tailwind (pre-built CSS)
// app/layout.tsx
import { BasegentProvider, BasegentChat } from "@basegent/react";
import "@basegent/react/styles.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<BasegentProvider tenantId="t_your_tenant_id">
{children}
<BasegentChat />
</BasegentProvider>
</body>
</html>
);
}BasegentProvider Props
| Prop | Type | Default | Description |
| ---------------------- | ------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| tenantId | string | required | Your Basegent tenant ID |
| apiBase | string | "" | Base URL of your Basegent API (omit for same-origin) |
| customerToken | string | — | Pre-issued signed identity JWT (optional) |
| tokenUrl | string | — | URL of an endpoint returning { token: string }. The provider fetches it on mount and uses the result as customerToken. Ideal for SPAs — point it at any serverless function that signs the JWT. |
| escalationKeywords | string[] | — | Words that trigger the “speak to a human?” offer |
| showEscalationButton | boolean | — | Show a persistent “Talk to a person” button in the widget header |
| persona | object | — | { name, avatar, greeting, tone } overrides |
| collectVisitorInfo | "post" \| "pre" \| false \| CollectVisitorInfoConfig | — | Ask anonymous visitors for their name/email. "post" prompts after the first reply; "pre" prompts before the first message. Use the config object for field and email verification options. |
Client-only / SPA integration
For frameworks without server-side rendering (Vite, CRA, Expo Web, etc.) you cannot use your secret API key in the browser. There are two options:
Option A — tokenUrl prop (recommended)
Deploy a lightweight serverless function that signs the JWT server-side, then
point tokenUrl at it. The provider fetches it on mount automatically:
// main.tsx / App.tsx
import { BasegentProvider, BasegentChat } from "@basegent/react";
export default function App() {
return (
<BasegentProvider
tenantId={import.meta.env.VITE_BASEGENT_TENANT_ID}
apiBase={import.meta.env.VITE_BASEGENT_API_BASE}
tokenUrl="https://your-worker.example.com/basegent-token" // ← serverless fn
>
<YourApp />
<BasegentChat title="Support" />
</BasegentProvider>
);
}Example Cloudflare Worker (signs the token using your secret API key):
// worker.ts
export default {
async fetch(req: Request, env: Env): Promise<Response> {
// CORS — allow your SPA origin
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders(req) });
}
// Authenticate the user however your app does it
// (session cookie, Authorization header, etc.)
const userId = await getUserId(req, env); // your auth logic
if (!userId) return new Response("Unauthorized", { status: 401 });
const res = await fetch(`${env.BASEGENT_API_BASE}/api/v1/auth/customer-token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.BASEGENT_API_KEY}`, // secret — stays in the worker
},
body: JSON.stringify({ userId }),
});
const { data } = await res.json<{ data: { token: string } }>();
return Response.json({ token: data.token }, { headers: corsHeaders(req) });
},
};Option B — useCustomerToken hook
For headless scenarios where you need more control (loading/error states, refetch on login, etc.):
import { BasegentProvider, BasegentChat, useCustomerToken } from "@basegent/react";
function Root() {
const { token, loading, error, refetch } = useCustomerToken(
isLoggedIn ? "/api/basegent-token" : null, // pass null to skip (anonymous)
);
return (
<BasegentProvider tenantId={import.meta.env.VITE_BASEGENT_TENANT_ID} customerToken={token}>
<App />
<BasegentChat />
</BasegentProvider>
);
}Returns: { token, loading, error, refetch }
Option C — Anonymous sessions
If you don’t need identity-linked sessions (no conversation history, no
customer profile in the inbox), just omit customerToken and tokenUrl
entirely. The widget works fully anonymously:
<BasegentProvider tenantId="t_xxx" apiBase="https://...">
<BasegentChat />
</BasegentProvider>Headless Hooks
useChat()
Core AI chat + escalation state.
import { useChat } from "@basegent/react";
function MyChat() {
const { turns, loading, streaming, isEscalated, send, escalate, react, reset } = useChat();
return (
<div>
{turns.map((t, i) => (
<div key={i} style={{ textAlign: t.role === "user" ? "right" : "left" }}>
{t.content}
</div>
))}
{loading && <p>Thinking…</p>}
<button onClick={() => send("Hello!")}>Send</button>
{!isEscalated && <button onClick={() => escalate()}>Talk to a human</button>}
</div>
);
}Returns:
| Value | Type | Description |
| ------------- | ------------------------------------------------------------------------ | -------------------------------------------------- |
| turns | Turn[] | All conversation turns |
| loading | boolean | True while AI response is in flight |
| streaming | string | Partial token stream during SSE |
| sessionId | string \| null | Current session ID (null until first message) |
| isEscalated | boolean | True once handed over to a human agent |
| send | (message: string) => Promise<void> | Send a message — auto-routes to live agent if live |
| escalate | (reason?: string) => Promise<void> | Manually trigger human handover |
| react | (turnIndex: number, reaction: "helpful"\|"unhelpful") => Promise<void> | Submit a thumbs reaction |
| reset | () => void | Clear all turns and start fresh |
useLiveSession()
Live agent message sync (polls every 3 s when escalated).
import { useLiveSession } from "@basegent/react";
function LiveIndicator() {
const { isLive, agentMessages, sendMessage } = useLiveSession();
if (!isLive) return null;
return (
<div>
<p>Connected to a live agent</p>
{agentMessages.map((m) => (
<p key={m.id}>{m.content}</p>
))}
<button onClick={() => sendMessage("Can you help?")}>Reply</button>
</div>
);
}Returns:
| Value | Type | Description |
| --------------- | ------------------------------------ | --------------------------------------------- |
| isLive | boolean | True when session is under live agent control |
| escalationId | string \| null | Active escalation ID |
| liveMessages | LiveMessage[] | All live messages (both directions) |
| agentMessages | LiveMessage[] | Filtered to agent-sent messages only |
| sendMessage | (content: string) => Promise<void> | Send a customer message to the live agent |
useWidget()
Open/close state for the chat panel.
import { useWidget } from "@basegent/react";
function MyFAB() {
const { isOpen, toggle } = useWidget();
return <button onClick={toggle}>{isOpen ? "Close" : "Chat"}</button>;
}Returns: { isOpen, open, close, toggle }
useVisitorIdentity()
Headless hook for visitor self-identification. Reads/writes visitor profile from localStorage and the Basegent API. Only meaningful when collectVisitorInfo is set on BasegentProvider.
import { useVisitorIdentity } from "@basegent/react";
function IdentityForm() {
const {
visitorProfile,
showIdentityForm,
identityFields,
identityDismissed,
verifyPending,
verifyEmailEnabled,
identify,
dismiss,
reopen,
clear,
resendVerification,
} = useVisitorIdentity();
if (!showIdentityForm) {
if (visitorProfile) {
// Already identified — show "Chatting as…" chip
return (
<div>
Chatting as {visitorProfile.name ?? visitorProfile.email}{" "}
<button onClick={clear}>Not you?</button>
</div>
);
}
if (identityDismissed) {
// Show a minimal re-open link
return <button onClick={reopen}>Add contact info</button>;
}
return null;
}
if (verifyPending) {
return (
<div>
<p>Check your inbox to verify your email.</p>
<button onClick={resendVerification}>Resend</button>
</div>
);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
identify({
name: fd.get("name") as string,
email: fd.get("email") as string,
});
}}
>
{identityFields.includes("name") && <input name="name" placeholder="Your name" />}
{identityFields.includes("email") && (
<input name="email" type="email" placeholder="Your email" />
)}
<button type="submit">Save</button>
<button type="button" onClick={dismiss}>
Not now
</button>
</form>
);
}Returns:
| Value | Type | Description |
| -------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------- |
| visitorProfile | { profileId: string; name?: string; email?: string } \| null | Persisted profile (from localStorage + API), null if unknown |
| identityMode | "post" \| "pre" \| null | Active mode derived from collectVisitorInfo config |
| identityFields | ("name" \| "email" \| "phone" \| "company")[] | Fields to show in the identity form (default: ["name","email"]) |
| showIdentityForm | boolean | Whether the form should currently be visible |
| identityDismissed | boolean | Whether the visitor clicked "Not now" |
| verifyPending | boolean | Verification email has been sent, awaiting click |
| verifyEmailEnabled | boolean | Whether email verification is enabled on the config |
| identify | (fields: Record<string, string>) => Promise<void> | Submit visitor info to the API and persist locally |
| dismiss | () => void | Hide the form (can be re-opened with reopen()) |
| reopen | () => void | Show the form again after dismissal |
| clear | () => void | Clear persisted profile ("Not you?" action) |
| resendVerification | () => Promise<void> | Re-send the verification email |
Pre-built Components
All pre-built components accept a className prop for Tailwind overrides.
<BasegentChat />
Full panel — FAB + collapsible chat panel. Renders BasegentFAB, BasegentMessages, and BasegentInput together.
<BasegentChat
title="Support" // panel header text (default: "Support")
placeholder="Ask…" // input placeholder
className="" // extra classes on the panel wrapper
fabClassName="" // extra classes on the FAB button
/><BasegentMessages />
Scrollable message list. Merges AI turns with live agent turns, shows a typing indicator while streaming.
<BasegentMessages className="h-96" /><BasegentInput />
Rounded-pill text input + Send button. Shows "Reply to agent…" placeholder when session is escalated.
<BasegentInput placeholder="Type a message…" className="" /><BasegentFAB />
Fixed floating action button, bottom-right.
<BasegentFAB className="bottom-8 right-8" />Custom UI with Headless Hooks
import { BasegentProvider, useChat, useLiveSession, useWidget } from "@basegent/react";
function MyCustomChat() {
const { turns, send, isEscalated, loading } = useChat();
const { isLive, agentMessages } = useLiveSession();
const { isOpen, toggle } = useWidget();
return (
<div>
<button onClick={toggle}>💬</button>
{isOpen && (
<div className="my-chat-panel">
{turns.map((t, i) => (
<div key={i} className={t.role === "user" ? "user-bubble" : "ai-bubble"}>
{t.content}
</div>
))}
{isLive &&
agentMessages.map((m) => (
<div key={m.id} className="agent-bubble">
{m.content}
</div>
))}
{loading && <span>…</span>}
<input
onKeyDown={(e) => e.key === "Enter" && send(e.currentTarget.value)}
placeholder={isEscalated ? "Reply to agent…" : "Ask anything…"}
/>
</div>
)}
</div>
);
}
export default function App() {
return (
<BasegentProvider tenantId="t_xxx" apiBase="https://your-basegent.example.com">
<MyCustomChat />
</BasegentProvider>
);
}Styling
The package ships with design tokens as CSS custom properties so you can theme the pre-built components without forking them:
/* Override in your global CSS */
:root {
--basegent-primary: #6366f1; /* FAB + user bubble + send button */
--basegent-primary-foreground: #fff;
--basegent-background: #fff;
--basegent-foreground: #0f172a;
--basegent-border: #e2e8f0;
--basegent-muted: #f1f5f9;
--basegent-muted-foreground: #64748b;
--basegent-radius: 0.75rem;
}Exports
// Provider + context
export { BasegentProvider } from "./provider";
// Hooks
export { useChat } from "./hooks/useChat";
export { useLiveSession } from "./hooks/useLiveSession";
export { useWidget } from "./hooks/useWidget";
export { useCustomerToken } from "./hooks/useCustomerToken";
export { useVisitorIdentity } from "./hooks/useVisitorIdentity";
// Pre-built components
export { BasegentChat, BasegentMessages, BasegentInput, BasegentFAB } from "./components";
// Types
export type {
BasegentConfig,
Turn,
LiveMessage,
Source,
BasegentContextValue,
UseCustomerTokenResult,
UseVisitorIdentityReturn,
CollectVisitorInfoConfig,
VisitorProfileResult,
} from "./types";CSS: import "@basegent/react/styles.css" (only needed without Tailwind v4 in the host app)
Build
# JS + types
pnpm build:js
# Pre-built CSS
pnpm build:css
# Both
pnpm buildOutput in dist/: index.js (ESM), index.cjs (CJS), index.d.ts, styles.css.
