Use this file to discover all available pages before exploring further.
assistant-ui is a headless React UI framework for AI chat. It provides a full runtime layer—thread management, message branching, attachment handling—that connects to useStream via the useExternalStoreRuntime adapter.
Clone and run the full assistant-ui example to see a Claude-style chat interface wired to a LangChain agent with useExternalStoreRuntime.
The useExternalStoreRuntime adapter bridges stream.messages into the assistant-ui runtime. Pass it to AssistantRuntimeProvider and render any thread component:
import { useCallback, useMemo } from "react";import { AssistantRuntimeProvider, useExternalStoreRuntime, type AppendMessage, type ThreadMessageLike,} from "@assistant-ui/react";import { useStream } from "@langchain/react";import { Thread } from "@assistant-ui/react";export function Chat() { const stream = useStream({ apiUrl: "http://localhost:2024", assistantId: "agent", }); const onNew = useCallback( async (message: AppendMessage) => { const text = message.content .filter((c) => c.type === "text") .map((c) => c.text) .join(""); await stream.submit({ messages: [{ type: "human", content: text }] }); }, [stream], ); // Convert LangChain messages to assistant-ui's ThreadMessageLike format const messages = useMemo( () => toThreadMessages(stream.messages), [stream.messages], ); const runtime = useExternalStoreRuntime<ThreadMessageLike>({ messages, onNew, onCancel: () => stream.stop(), convertMessage: (m) => m, }); return ( <AssistantRuntimeProvider runtime={runtime}> <Thread /> </AssistantRuntimeProvider> );}
toThreadMessages maps LangChain BaseMessage[] to the ThreadMessageLike[] format assistant-ui expects. Handle each message type — human, AI, and tool — and convert content blocks, tool calls, and reasoning tokens:
import { AIMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";import type { ThreadMessageLike } from "@assistant-ui/react";export function toThreadMessages(messages: BaseMessage[]): ThreadMessageLike[] { const result: ThreadMessageLike[] = []; for (const msg of messages) { if (HumanMessage.isInstance(msg)) { result.push({ role: "user", content: [{ type: "text", text: getTextContent(msg.content) }], }); } else if (AIMessage.isInstance(msg)) { const parts: ThreadMessageLike["content"] = []; // Reasoning tokens const reasoning = getReasoningText(msg); if (reasoning) parts.push({ type: "reasoning", reasoning }); // Tool calls for (const tc of msg.tool_calls ?? []) { parts.push({ type: "tool-call", toolCallId: tc.id ?? "", toolName: tc.name, args: tc.args, }); } // Text response const text = getTextContent(msg.content); if (text) parts.push({ type: "text", text }); result.push({ role: "assistant", content: parts }); } else if (ToolMessage.isInstance(msg)) { // Attach tool results to the preceding assistant message const last = result[result.length - 1]; if (last?.role === "assistant") { for (const part of last.content) { if ( part.type === "tool-call" && part.toolCallId === msg.tool_call_id ) { (part as { result?: string }).result = getTextContent(msg.content); } } } } } return result;}
<Thread /> ships a complete default thread UI including message list, composer, and scroll management. Customise individual parts by overriding component slots: