React SDK
Build AI-powered React applications with hooks for chat, agents, workflows, and vector search. Full TypeScript support with streaming and real-time updates.
Installation
npm install @tenzro/react tenzro# orpnpm add @tenzro/react tenzro
Overview
The React SDK provides:
- TenzroProvider: Context provider for configuration
- useChat: Build chat interfaces with streaming
- useAgent: Interact with deployed agents
- useWorkflow: Execute and monitor workflows
- useVec: Vector search operations
- useTenzro: Direct access to the Tenzro client
- Pre-built components: ChatWindow, MessageList, AgentChat
Quick Start
'use client';import { TenzroProvider, useChat } from '@tenzro/react';// 1. Wrap your app with TenzroProviderfunction App() {return (<TenzroProviderapiKey={process.env.NEXT_PUBLIC_TENZRO_API_KEY!}projectId={process.env.NEXT_PUBLIC_TENZRO_PROJECT_ID}><Chat /></TenzroProvider>);}// 2. Use hooks in your componentsfunction Chat() {const { messages, send, loading } = useChat({model: 'gemini-2.5-flash',systemPrompt: 'You are a helpful assistant.',});return (<div>{messages.map((msg) => (<div key={msg.id} className={msg.role}>{msg.content}</div>))}<button onClick={() => send('Hello!')} disabled={loading}>Send</button></div>);}
TenzroProvider
The provider makes Tenzro services available throughout your app:
import { TenzroProvider } from '@tenzro/react';// Basic setup<TenzroProviderapiKey={process.env.NEXT_PUBLIC_TENZRO_API_KEY!}projectId={process.env.NEXT_PUBLIC_TENZRO_PROJECT_ID}>{children}</TenzroProvider>// Full configuration<TenzroProviderapiKey={process.env.NEXT_PUBLIC_TENZRO_API_KEY!}projectId="your-project-id"baseURL="https://api.cloud.tenzro.com"defaultModel="gemini-2.5-flash"defaultSystemPrompt="You are a helpful assistant."onError={(error) => console.error('Tenzro error:', error)}>{children}</TenzroProvider>
useChat Hook
Build chat interfaces with streaming support:
'use client';import { useChat, type ChatMessage } from '@tenzro/react';import { useState } from 'react';export function ChatInterface() {const [input, setInput] = useState('');const {messages, // ChatMessage[] - conversation historysend, // (message: string) => Promise<void>loading, // boolean - is response generatingerror, // Error | nullreset, // () => void - clear conversationsetMessages, // (messages: ChatMessage[]) => void} = useChat({model: 'gemini-2.5-flash',systemPrompt: 'You are a helpful assistant.',stream: true,temperature: 0.7,maxTokens: 1000,onToken: (token) => {// Called for each streamed tokenconsole.log('Token:', token);},onComplete: (message) => {// Called when response is completeconsole.log('Complete:', message);},onError: (error) => {// Handle errorsconsole.error('Chat error:', error);},});const handleSend = async () => {if (!input.trim() || loading) return;await send(input);setInput('');};return (<div className="flex flex-col h-full"><div className="flex-1 overflow-y-auto p-4 space-y-4">{messages.map((msg) => (<divkey={msg.id}className={`p-3 rounded-lg ${msg.role === 'user'? 'bg-blue-500 ml-auto': 'bg-gray-700'}`}>{msg.content}</div>))}{loading && (<div className="text-gray-400">Thinking...</div>)}</div>{error && (<div className="text-red-500 p-2">{error.message}</div>)}<div className="flex gap-2 p-4 border-t border-gray-700"><inputvalue={input}onChange={(e) => setInput(e.target.value)}onKeyPress={(e) => e.key === 'Enter' && handleSend()}placeholder="Type a message..."className="flex-1 px-4 py-2 bg-gray-800 rounded"disabled={loading}/><buttononClick={handleSend}disabled={loading || !input.trim()}className="px-4 py-2 bg-blue-500 rounded disabled:opacity-50">Send</button><buttononClick={reset}className="px-4 py-2 bg-gray-700 rounded">Clear</button></div></div>);}
useChat Options
interface UseChatOptions {model?: AIModel; // Default: 'gemini-2.5-flash'systemPrompt?: string; // System message for the AIstream?: boolean; // Enable streaming (default: true)temperature?: number; // 0-2, default: 0.7maxTokens?: number; // Max response lengthinitialMessages?: ChatMessage[]; // Pre-populate conversationonToken?: (token: string) => void;onComplete?: (message: ChatMessage) => void;onError?: (error: Error) => void;}
useAgent Hook
Interact with deployed AI agents that have tools and memory:
'use client';import { useAgent } from '@tenzro/react';export function CustomerSupport() {const {messages, // Message historyexecute, // Execute agent with messagechat, // Simple chat (no tool execution)loading, // Is executingerror, // Error stateconversationId, // Current conversation IDtoolCalls, // Array of tool calls madereset, // Reset conversation} = useAgent('agent-id', {autoConnect: true,onToolCall: (toolName, args) => {console.log(`Agent called tool: ${toolName}`, args);},onToolResult: (toolName, result) => {console.log(`Tool result: ${toolName}`, result);},onMessage: (message) => {console.log('New message:', message);},});return (<div><div className="messages">{messages.map((msg) => (<div key={msg.id} className={msg.role}>{msg.content}{msg.toolCalls && (<div className="tool-calls">{msg.toolCalls.map((tc) => (<div key={tc.id} className="tool-call"><span className="tool-name">{tc.name}</span><span className="tool-status">{tc.status}</span></div>))}</div>)}</div>))}</div><div className="actions"><buttononClick={() => execute('Check my order status for #12345')}disabled={loading}>Check Order</button><buttononClick={() => execute('Process a refund for my last purchase')}disabled={loading}>Request Refund</button></div>{toolCalls.length > 0 && (<div className="tool-history"><h4>Tools Used:</h4>{toolCalls.map((tc, i) => (<div key={i}>{tc.name}: {tc.status}</div>))}</div>)}</div>);}
useAgent Options
interface UseAgentOptions {autoConnect?: boolean; // Auto-connect on mountconversationId?: string; // Resume existing conversationcontext?: Record<string, any>; // Additional context for agentonToolCall?: (toolName: string, args: any) => void;onToolResult?: (toolName: string, result: any) => void;onMessage?: (message: AgentMessage) => void;onError?: (error: Error) => void;}
useWorkflow Hook
Execute and monitor visual workflows:
'use client';import { useWorkflow } from '@tenzro/react';export function ContentPipeline() {const {execute, // Start workflow executionstatus, // 'idle' | 'running' | 'completed' | 'failed'progress, // { current: number, total: number }nodeStatuses, // Status of each nodeoutput, // Final output dataerror, // Error if failedcancel, // Cancel running executionexecutionId, // Current execution ID} = useWorkflow('workflow-id', {onNodeStart: (nodeId, nodeName) => {console.log(`Starting node: ${nodeName}`);},onNodeComplete: (nodeId, output) => {console.log(`Node completed: ${nodeId}`, output);},onComplete: (output) => {console.log('Workflow completed:', output);},});const handleExecute = async () => {await execute({topic: 'AI in Healthcare',format: 'blog_post',wordCount: 1000,});};return (<div><h2>Content Generation Pipeline</h2><buttononClick={handleExecute}disabled={status === 'running'}>{status === 'running' ? 'Generating...' : 'Generate Content'}</button>{status === 'running' && (<div className="progress"><divclassName="progress-bar"style={{ width: `${(progress.current / progress.total) * 100}%` }}/><span>Step {progress.current} of {progress.total}</span></div>)}{nodeStatuses && (<div className="node-statuses">{Object.entries(nodeStatuses).map(([nodeId, status]) => (<div key={nodeId} className={`node ${status}`}>{nodeId}: {status}</div>))}</div>)}{status === 'running' && (<button onClick={cancel} className="cancel-btn">Cancel</button>)}{status === 'completed' && output && (<div className="output"><h3>Generated Content:</h3><div className="content">{output.content}</div></div>)}{status === 'failed' && error && (<div className="error">Error: {error.message}</div>)}</div>);}
useVec Hook
Perform vector similarity search:
'use client';import { useVec } from '@tenzro/react';import { useState } from 'react';export function SemanticSearch() {const [query, setQuery] = useState('');const {search, // Perform vector searchresults, // Search resultsloading, // Is searchingerror, // Error stateinsert, // Insert vectorsstats, // Database statistics} = useVec('vec-db-id');const handleSearch = async () => {await search({query, // Will auto-generate embeddingtopK: 10,filter: {category: 'documentation',},includeMetadata: true,});};return (<div><div className="search-box"><inputvalue={query}onChange={(e) => setQuery(e.target.value)}placeholder="Search documentation..."onKeyPress={(e) => e.key === 'Enter' && handleSearch()}/><button onClick={handleSearch} disabled={loading}>{loading ? 'Searching...' : 'Search'}</button></div>{results && (<div className="results">{results.map((result) => (<div key={result.id} className="result"><h3>{result.metadata?.title}</h3><p>{result.metadata?.content}</p><span className="score">Score: {(result.score * 100).toFixed(1)}%</span></div>))}</div>)}{error && (<div className="error">{error.message}</div>)}</div>);}
useTenzro Hook
Access the full Tenzro client for any operation:
'use client';import { useTenzro } from '@tenzro/react';import { useEffect, useState } from 'react';export function Dashboard() {const tenzro = useTenzro();const [stats, setStats] = useState(null);useEffect(() => {async function loadStats() {const [agents, workflows, servers] = await Promise.all([tenzro.agents.list('project-id'),tenzro.workflows.list('project-id'),tenzro.servers.list('project-id'),]);setStats({agents: agents.data.length,workflows: workflows.data.length,servers: servers.data.length,});}loadStats();}, [tenzro]);// Use any Tenzro serviceconst createAgent = async () => {const agent = await tenzro.agents.create({projectId: 'project-id',agentName: 'new-agent',systemPrompt: 'You are helpful.',aiModel: 'gemini-2.5-flash',});console.log('Created agent:', agent.agent_id);};return (<div>{stats && (<div className="stats"><div>Agents: {stats.agents}</div><div>Workflows: {stats.workflows}</div><div>Servers: {stats.servers}</div></div>)}<button onClick={createAgent}>Create Agent</button></div>);}
Pre-built Components
Ready-to-use UI components for common patterns:
import {ChatWindow,MessageList,MessageInput,AgentChat,WorkflowStatus,SearchBox,} from '@tenzro/react/components';// Full chat interface with built-in styling<ChatWindowmodel="gemini-2.5-flash"systemPrompt="You are a helpful assistant."placeholder="Ask me anything..."className="h-[500px] rounded-lg"theme="dark"/>// Agent chat with tool call visualization<AgentChatagentId="your-agent-id"showToolCalls={true}className="h-full"/>// Individual components for custom layouts<div className="flex flex-col h-full"><MessageListmessages={messages}renderMessage={(msg) => (<CustomMessage message={msg} />)}/><MessageInputonSend={handleSend}loading={loading}placeholder="Type a message..."autoFocus/></div>// Workflow execution status<WorkflowStatusworkflowId="workflow-id"executionId={executionId}showNodes={true}/>// Vector search box<SearchBoxvecDbId="vec-db-id"placeholder="Search..."onResults={(results) => console.log(results)}/>
Next.js Integration
App Router Setup
// app/layout.tsximport { TenzroProvider } from '@tenzro/react';export default function RootLayout({children,}: {children: React.ReactNode;}) {return (<html lang="en"><body><TenzroProviderapiKey={process.env.NEXT_PUBLIC_TENZRO_API_KEY!}projectId={process.env.NEXT_PUBLIC_TENZRO_PROJECT_ID}>{children}</TenzroProvider></body></html>);}// app/chat/page.tsx'use client';import { ChatWindow } from '@tenzro/react/components';export default function ChatPage() {return (<div className="container mx-auto p-4 h-screen"><ChatWindowmodel="gemini-2.5-flash"systemPrompt="You are a helpful assistant."className="h-full"/></div>);}
Server Components + Client Hooks
// app/agents/page.tsx (Server Component)import { Tenzro } from 'tenzro';import { AgentList } from './agent-list';const tenzro = new Tenzro({apiKey: process.env.TENZRO_API_KEY!,});export default async function AgentsPage() {// Fetch on serverconst { data: agents } = await tenzro.agents.list('project-id');return (<div><h1>Agents</h1>{/* Pass to client component */}<AgentList initialAgents={agents} /></div>);}// app/agents/agent-list.tsx (Client Component)'use client';import { useAgent } from '@tenzro/react';import { useState } from 'react';export function AgentList({ initialAgents }) {const [selectedAgent, setSelectedAgent] = useState(null);return (<div className="grid grid-cols-2 gap-4"><div className="agent-list">{initialAgents.map((agent) => (<divkey={agent.agent_id}onClick={() => setSelectedAgent(agent.agent_id)}className="cursor-pointer p-4 border rounded">{agent.agent_name}</div>))}</div>{selectedAgent && (<AgentChatPanel agentId={selectedAgent} />)}</div>);}function AgentChatPanel({ agentId }) {const { messages, execute, loading } = useAgent(agentId);return (<div className="chat-panel">{messages.map((msg) => (<div key={msg.id}>{msg.content}</div>))}<button onClick={() => execute('Hello!')}>Chat</button></div>);}
API Routes
// app/api/chat/route.tsimport { Tenzro } from 'tenzro';const tenzro = new Tenzro({apiKey: process.env.TENZRO_API_KEY!,});export async function POST(request: Request) {const { message, conversationId } = await request.json();const response = await tenzro.ai.chat({model: 'gemini-2.5-flash',messages: [{ role: 'user', content: message },],});return Response.json({message: response.responseText,});}// Streaming responseexport async function POST(request: Request) {const { messages } = await request.json();const stream = tenzro.ai.streamChat({model: 'gemini-2.5-flash',messages,});return new Response(new ReadableStream({async start(controller) {for await (const chunk of stream) {controller.enqueue(new TextEncoder().encode(chunk.content));}controller.close();},}),{headers: { 'Content-Type': 'text/event-stream' },});}
TypeScript Types
import type {// Hook typesUseChatOptions,UseChatReturn,UseAgentOptions,UseAgentReturn,UseWorkflowOptions,UseWorkflowReturn,UseVecOptions,UseVecReturn,// Message typesChatMessage,AgentMessage,ToolCall,// Provider typesTenzroProviderProps,TenzroContextValue,// Component propsChatWindowProps,MessageListProps,MessageInputProps,} from '@tenzro/react';
Utilities
import {cn, // Classname utility (clsx + tailwind-merge)formatMessageTime, // Format message timestampstruncateText, // Truncate long text with ellipsisparseStreamedResponse, // Parse SSE stream} from '@tenzro/react/utils';// Usageconst className = cn('base-class',condition && 'conditional-class',{ 'object-class': true });const time = formatMessageTime(new Date());// "2:30 PM"const truncated = truncateText(longText, 100);// "Long text that gets cut off after 100..."// Parse streaming responsefor await (const chunk of parseStreamedResponse(response)) {console.log(chunk);}
Best Practices
- Use client components: All hooks require
'use client'directive - Server-side fetching: Use the main SDK in Server Components, hooks in Client Components
- Error boundaries: Wrap chat components in error boundaries for better UX
- Loading states: Always show loading indicators during AI operations
- Conversation persistence: Store conversationId for resuming agent chats
- Streaming: Enable streaming for better perceived performance