Clients
React SDK
Build a chat UI for an agent with React.
Build a chat UI for an agent with @rivetkit/react. The agent logic lives in one hook, and the UI in one component.
import { type FormEvent, useState } from "react";
import { useAgentChat } from "./useAgentChat";
export function Chat({ userId }: { userId: string }) {
const chat = useAgentChat(["support", userId]);
const [text, setText] = useState("");
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!text.trim()) return;
void chat.send(text);
setText("");
}
return (
<section>
<ul>
{chat.messages.map((message) => (
<li key={message.id}>
<strong>{message.role === "user" ? "You" : "Agent"}:</strong> {message.text}
</li>
))}
{chat.streaming && (
<li>
<strong>Agent:</strong> {chat.streaming}
</li>
)}
</ul>
{chat.tool && <p>Running {chat.tool}…</p>}
<form onSubmit={submit}>
<input value={text} onChange={(event) => setText(event.target.value)} disabled={chat.running} />
{chat.running ? (
<button type="button" onClick={() => void chat.stop()}>
Stop
</button>
) : (
<button type="submit" disabled={!chat.ready}>
Send
</button>
)}
</form>
</section>
);
}
import type { AssistantMessage, UserMessage } from "@earendil-works/pi-ai";
import { createRivetKit } from "@rivetkit/react";
import { useEffect, useState } from "react";
import type { registry } from "../pi/server";
const { useActor } = createRivetKit<typeof registry>();
export type ChatMessage = { id: string; role: "user" | "assistant"; text: string };
export function useAgentChat(key: string[]) {
const agent = useActor({ name: "agent", key });
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [streaming, setStreaming] = useState("");
const [tool, setTool] = useState<string | null>(null);
const [running, setRunning] = useState(false);
useEffect(() => {
agent.connection?.getMessages().then((history) => setMessages(history.flatMap(toChatMessage)));
}, [agent.connection]);
agent.useEvent("event", (event) => {
if (event.type === "agent_start") setRunning(true);
if (event.type === "agent_settled") setRunning(false);
if (event.type === "tool_execution_start") setTool(event.toolName);
if (event.type === "tool_execution_end") setTool(null);
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
const { delta } = event.assistantMessageEvent;
setStreaming((text) => text + delta);
}
if (event.type === "message_end") {
setStreaming("");
setMessages((current) => [...current, ...toChatMessage(event.message)]);
}
});
return {
messages,
streaming,
tool,
running,
ready: agent.connection !== null,
send: (text: string) => agent.connection?.prompt(text),
stop: () => agent.connection?.abort(),
};
}
function toChatMessage(message: { role: string }): ChatMessage[] {
if (message.role !== "user" && message.role !== "assistant") return [];
const { role, content } = message as UserMessage | AssistantMessage;
const text =
typeof content === "string"
? content
: content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
return text ? [{ id: crypto.randomUUID(), role, text }] : [];
}
useActorconnects to the agent by name and key. Once the connection opens, the hook loads the conversation withgetMessages.message_updatestreams the reply as text deltas, andmessage_endadds each finished message, including your own prompt. See Session Lifecycle for every event.agent_settledmarks the agent idle once it has finished, after any retry.Chatswitches its button from Send to Stop while the agent works.
Next steps
- To steer a run in progress, see Session Lifecycle. To handle errors, see Client SDK.
- To send a login token, add
paramstouseActor. - To switch models from the UI, call
setModelon the connection with one of the agent’sscopedModels.
See React in the Actors docs for everything @rivetkit/react can do.