@standardagents/react
v0.15.3
Published
React composables for AgentBuilder
Readme
@standardagents/react
React hooks and components for Standard Agents - connect to AI agent threads with real-time updates, send messages, manage files, and listen for custom events.
Installation
npm install @standardagents/react
# or
pnpm add @standardagents/react
# or
yarn add @standardagents/reactQuick Start
import {
AgentBuilderProvider,
ThreadProvider,
useThread,
} from "@standardagents/react"
function App() {
return (
<AgentBuilderProvider config={{ endpoint: "https://your-api.com" }}>
<ThreadProvider threadId="thread-123">
<ChatInterface />
</ThreadProvider>
</AgentBuilderProvider>
)
}
function ChatInterface() {
const { messages, sendMessage, status } = useThread()
const handleSend = async (text: string) => {
await sendMessage({ role: "user", content: text })
}
return (
<div>
<p>Status: {status}</p>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
<input onKeyDown={(e) => e.key === "Enter" && handleSend(e.currentTarget.value)} />
</div>
)
}Authentication
The package reads the authentication token from localStorage using the key standardagents_auth_token:
// Set the token before using the hooks
localStorage.setItem("standardagents_auth_token", "your-token-here")All API requests and WebSocket connections will automatically include this token.
API Reference
AgentBuilderProvider
Context provider that configures the Standard Agents client for all child components.
Props:
config.endpoint: string- The API endpoint URL
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
{children}
</AgentBuilderProvider>ThreadProvider
Context provider that establishes a WebSocket connection to a specific thread. Must be nested inside AgentBuilderProvider.
Props:
threadId: string- The thread ID to connect topreload?: boolean- Fetch existing messages on mount (default:true)live?: boolean- Enable WebSocket for real-time updates (default:true)useWorkblocks?: boolean- Transform tool calls into workblocks (default:false)depth?: number- Message depth level for nested conversations (default:0)includeSilent?: boolean- Include silent messages (default:false)endpoint?: string- Override the endpoint from context
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
<ThreadProvider threadId="thread-123" live={true}>
<YourComponents />
</ThreadProvider>
</AgentBuilderProvider>useThread()
Hook to access the full thread context. Must be used within a ThreadProvider.
Returns: ThreadContextValue
threadId: string- The thread IDmessages: Message[]- Array of messagesworkblocks: ThreadMessage[]- Messages transformed to workblocks (ifuseWorkblocksis true)status: ConnectionStatus- WebSocket connection status ("connecting"|"connected"|"disconnected"|"reconnecting")loading: boolean- Whether messages are loading (alias:isLoading)error: Error | null- Any error that occurredoptions: ThreadProviderOptions- Options passed to the providersendMessage: (payload: SendMessagePayload) => Promise<Message>- Send a messagestopExecution: () => Promise<void>- Stop current executiononEvent: <T>(eventType, listener) => () => void- Subscribe to custom events (alias:subscribeToEvent)files: ThreadFile[]- All files (pending + committed)addFiles: (files: File[] | FileList) => void- Upload filesremoveFile: (id: string) => void- Remove a pending filegetFileUrl: (file: ThreadFile) => string- Get file URLgetThumbnailUrl: (file: ThreadFile) => string- Get thumbnail URLgetPreviewUrl: (file: ThreadFile) => string | null- Get preview URL
Example:
function ChatView() {
const {
messages,
sendMessage,
stopExecution,
status,
isLoading,
files,
addFiles,
} = useThread()
return (
<div>
<p>Status: {status}</p>
{isLoading && <p>Loading...</p>}
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
<input
type="file"
multiple
onChange={(e) => e.target.files && addFiles(e.target.files)}
/>
<div>
{files.map((file) => (
<span key={file.id}>{file.name} ({file.status})</span>
))}
</div>
<button onClick={() => sendMessage({ role: "user", content: "Hello!" })}>Send</button>
<button onClick={stopExecution}>Stop</button>
</div>
)
}useThreadEvent<T>(eventType)
Hook to listen for custom events emitted by the agent. Must be used within a ThreadProvider.
Parameters:
eventType: string- The custom event type to listen for
Returns: T | null - The latest event value, or null if no event received yet
Example:
function TodoProgress() {
const todos = useThreadEvent<{ todos: string[]; completed: number }>("todo-updated")
if (!todos) return <div>Waiting for updates...</div>
return (
<div>
<p>Progress: {todos.completed} / {todos.todos.length}</p>
<ul>
{todos.todos.map((todo, i) => (
<li key={i}>{todo}</li>
))}
</ul>
</div>
)
}onThreadEvent<T>(eventType, callback)
Hook to listen for custom events with a callback. Must be used within a ThreadProvider.
Parameters:
eventType: string- The custom event type to listen forcallback: (data: T) => void- Called when event is received
Example:
function Notifications() {
onThreadEvent<{ message: string }>("notification", (data) => {
alert(data.message)
})
return null
}File Management
The useThread() hook provides file management capabilities:
function FileUploader() {
const { files, addFiles, removeFile, getFileUrl, getPreviewUrl } = useThread()
return (
<div>
<input
type="file"
multiple
accept="image/*,.pdf,.txt"
onChange={(e) => e.target.files && addFiles(e.target.files)}
/>
{files.map((file) => (
<div key={file.id}>
{file.isImage && file.status !== 'uploading' && (
<img src={getPreviewUrl(file) || ''} alt={file.name} />
)}
<span>{file.name}</span>
<span>{file.status}</span>
{file.status === 'uploading' && <span>Uploading...</span>}
{file.status === 'error' && <span>Error: {file.error}</span>}
{file.status !== 'committed' && (
<button onClick={() => removeFile(file.id)}>Remove</button>
)}
</div>
))}
</div>
)
}File States
uploading- File is being uploadedready- Upload complete, file ready to attach to messagecommitted- File is attached to a sent messageerror- Upload failed
Types
interface Message {
id: string
role: "user" | "assistant" | "system" | "tool"
content: string | null
created_at: number
attachments?: string // JSON array of AttachmentRef
}
interface SendMessagePayload {
role: "user" | "assistant" | "system"
content: string
silent?: boolean
attachments?: string[] // Paths of files to attach
}
interface ThreadFile {
id: string
name: string
mimeType: string
size: number
isImage: boolean
status: "uploading" | "ready" | "committed" | "error"
error?: string
path?: string
localPreviewUrl: string | null
}
type ConnectionStatus = "connecting" | "connected" | "disconnected" | "reconnecting"Complete Example
import { useState, useEffect } from "react"
import {
AgentBuilderProvider,
ThreadProvider,
useThread,
useThreadEvent,
} from "@standardagents/react"
function App() {
useEffect(() => {
localStorage.setItem("standardagents_auth_token", "your-token")
}, [])
return (
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
<ThreadProvider threadId="thread-123">
<AgentChat />
</ThreadProvider>
</AgentBuilderProvider>
)
}
function AgentChat() {
const [input, setInput] = useState("")
const {
messages,
sendMessage,
stopExecution,
status,
isLoading,
files,
addFiles,
removeFile,
getPreviewUrl,
} = useThread()
const progress = useThreadEvent<{ step: string; percent: number }>("progress")
const handleSend = async () => {
if (!input.trim()) return
await sendMessage({ role: "user", content: input })
setInput("")
}
return (
<div>
{progress && (
<div>
<p>{progress.step}</p>
<progress value={progress.percent} max={100} />
</div>
)}
{isLoading && <p>Loading messages...</p>}
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
<input
type="file"
multiple
onChange={(e) => e.target.files && addFiles(e.target.files)}
/>
{files.filter(f => f.status !== 'committed').map((file) => (
<div key={file.id}>
{file.isImage && getPreviewUrl(file) && (
<img src={getPreviewUrl(file)!} alt={file.name} width={50} />
)}
<span>{file.name} ({file.status})</span>
<button onClick={() => removeFile(file.id)}>x</button>
</div>
))}
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
/>
<button onClick={handleSend}>Send</button>
<button onClick={stopExecution}>Stop</button>
<span>Status: {status}</span>
</div>
</div>
)
}TypeScript Support
The package includes full TypeScript definitions:
import type {
Message,
SendMessagePayload,
ThreadFile,
ConnectionStatus,
ThreadContextValue,
} from "@standardagents/react"