Skip to navigation

Embedding the chat widget (React)

Add the Runtype chat widget to a React or Next.js application as a ChatWidget component that you generate from your chat Surface.

Before you begin

Complete the following prerequisites:

Use a client token instead of an API key in a chat widget. Client tokens use the ct_live_ or ct_test_ prefix and are designed for client-side code. Restrict each token to your site’s origins to help protect it from reuse on other sites.

Install the package

Install the Persona package in your application with the following command:

Install Persona
npm install @runtypelabs/persona

Get the component code

Copy the generated component from your chat Surface. The steps depend on which dashboard view you use:

  1. Open the chat Surface and select Embed.
  2. Select React.
  3. Select a client token in the token list, or select Create token to create one.
  4. If you selected a live token, paste the token that you saved when you created it into Saved live token. The dashboard stores only a hint of a live token, so it can’t show the full value again.
  5. Click Copy.

To have a coding agent add the widget for you, open Embed in the workbench, select your tool under Build it with a coding agent, and click Copy prompt. Paste the prompt into Claude Code, Cursor, or Codex.

Generate the component from the CLI

The runtype persona init command creates a new Agent and a client token for it, sets the token’s allowed origins, and prints a React component that connects to that Agent. Use it to start from a new Agent instead of an existing chat Surface:

Generate a React component
runtype persona init --format react --environment test --origins https://example.com --print-snippet

Coding agents connected to the Runtype MCP server can generate the React component with the generate_persona_embed_code tool.

Add the component to your app

  1. Save the generated code as ChatWidget.tsx in your application.
  2. If your application doesn’t use the Next.js App Router, delete the 'use client' line at the top of the file. Vite and Create React App don’t need it.
  3. Render <ChatWidget /> once, near the root of your application.

The generated component imports @runtypelabs/persona/widget.css and initializes the widget with your client token and theme. It mounts the widget to the document body, renders nothing itself, and calls handle.destroy() when React unmounts it. Customize the component for your application after you add it.

Verify the widget

Open your application in a browser. The chat launcher appears on the page. Open it and send a message to confirm that the Surface replies.

In the workbench, Origins seen in Embed lists your site’s origin as Allowed after the widget connects.

Control the widget from your code

initAgentWidget returns a handle with the following methods:

  • open() and close(): show or hide the chat panel.
  • submitMessage(text): send a message as the visitor.
  • destroy(): remove the widget.
  • on(event, callback): listen for widget events, such as user:message, assistant:message, assistant:complete, widget:opened, and widget:closed.

The generated component keeps the handle inside useEffect. To call it from elsewhere in your component, store it in a ref. The following example opens the widget from a button:

ChatWidget.tsx
import { useEffect, useRef } from 'react'
import '@runtypelabs/persona/widget.css'
import { initAgentWidget } from '@runtypelabs/persona'
import type { AgentWidgetInitHandle } from '@runtypelabs/persona'
export function ChatWidget() {
const handleRef = useRef<AgentWidgetInitHandle | null>(null)
useEffect(() => {
const handle = initAgentWidget({
target: 'body',
config: { apiUrl: 'https://api.runtype.com', clientToken: 'YOUR_CLIENT_TOKEN' },
})
handleRef.current = handle
handle.on('assistant:complete', (message) => console.log(message))
return () => {
handle.destroy()
handleRef.current = null
}
}, [])
return <button onClick={() => handleRef.current?.open()}>Chat with us</button>
}

Keep the config object from your generated code rather than the shortened one in this example.

Troubleshooting

The widget appears but messages fail. A common cause is that your site’s origin isn’t in the token’s allowed origins. In the workbench, find the origin in Origins seen and click Allow, or add it in Access & keys.

The code contains YOUR_CLIENT_TOKEN. In the classic dashboard, you copied the code with a placeholder selected. Replace YOUR_CLIENT_TOKEN with your client token, or copy the code again with a token selected.

The Copy button is disabled in the workbench. No client token is selected, or you selected a live token and didn’t paste its full value. Select a token, or paste the full live token that you saved when you created it into Saved live token. If you no longer have the live token, create a new token.

Build a custom voice interface

For a voice interface built from your own React components, use useVoiceClient from @runtypelabs/voice/react. It manages microphone capture, playback, transcripts, and the Agent’s Interruption Mode setting, which controls whether a caller can interrupt a spoken reply.

Install the voice package with the following command:

Install the voice package
npm install @runtypelabs/voice

The following component starts and ends a call with an Agent:

VoiceCall.tsx
import { useVoiceClient } from '@runtypelabs/voice/react'
function VoiceCall({ agentId, clientToken }) {
const voice = useVoiceClient({ agentId, clientToken })
return (
<div>
<p role="status">{voice.error ?? voice.status}</p>
<button onClick={() => void voice.startCall()}>Start call</button>
<button onClick={voice.endCall}>End call</button>
{voice.canCancel && <button onClick={voice.cancelResponse}>Stop response</button>}
</div>
)
}

Use a client token that can access the Agent. Start calls from a user click on an HTTPS page or on localhost. The hook releases the microphone when the component unmounts. For plain JavaScript usage and the Persona widget adapter, see the @runtypelabs/voice package.

Connect ahead on mic hover

A call’s first connection and first reply are slower when the browser still has to open the voice connection. To remove part of that delay, spread getMicButtonPrewarmProps() onto the button that starts the call:

<button {...voice.getMicButtonPrewarmProps()} onClick={() => void voice.startCall()}>
Start call
</button>

When the pointer enters the button, the button receives focus, or the user presses it, the client opens the voice connection and authenticates before the click. startCall() then reuses the open connection.

  • An early connection doesn’t request microphone access, create a conversation, bill, reserve quota, or record usage. The conversation is created when the call starts.
  • The client keeps one early connection and closes it after 30 seconds if no call starts. voice.attached is true while one is ready.
  • If the early connection drops, the call still starts normally over a new connection. Connecting early never throws.
  • In a browser without WebSocket, the client sends one request to POST /v1/client/agents/{agentId}/voice/prewarm instead, at most once every 25 seconds. Runtype limits these requests for each client token and Agent.

For a page where voice is the main interaction, pass keepWarm: true:

const voice = useVoiceClient({ agentId, clientToken, keepWarm: true })

With keepWarm, the hook connects when it mounts, and Runtype holds the connection for up to 10 minutes. After a drop, the client reconnects on the next mic hover or when the page becomes visible again. It never reconnects while the page is hidden.

Without React, call voice.attach() or voice.prewarm() on a VoiceClient, and call voice.dispose() when the view is removed. For a Persona widget, call bindVoicePrewarmToElement(provider, micButton) from @runtypelabs/voice/persona. This function returns a function that removes the listeners.

Next steps