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
# or
pnpm 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 TenzroProvider
function App() {
return (
<TenzroProvider
apiKey={process.env.NEXT_PUBLIC_TENZRO_API_KEY!}
projectId={process.env.NEXT_PUBLIC_TENZRO_PROJECT_ID}
>
<Chat />
</TenzroProvider>
);
}
// 2. Use hooks in your components
function 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
<TenzroProvider
apiKey={process.env.NEXT_PUBLIC_TENZRO_API_KEY!}
projectId={process.env.NEXT_PUBLIC_TENZRO_PROJECT_ID}
>
{children}
</TenzroProvider>
// Full configuration
<TenzroProvider
apiKey={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 history
send, // (message: string) => Promise<void>
loading, // boolean - is response generating
error, // Error | null
reset, // () => void - clear conversation
setMessages, // (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 token
console.log('Token:', token);
},
onComplete: (message) => {
// Called when response is complete
console.log('Complete:', message);
},
onError: (error) => {
// Handle errors
console.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) => (
<div
key={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">
<input
value={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}
/>
<button
onClick={handleSend}
disabled={loading || !input.trim()}
className="px-4 py-2 bg-blue-500 rounded disabled:opacity-50"
>
Send
</button>
<button
onClick={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 AI
stream?: boolean; // Enable streaming (default: true)
temperature?: number; // 0-2, default: 0.7
maxTokens?: number; // Max response length
initialMessages?: ChatMessage[]; // Pre-populate conversation
onToken?: (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 history
execute, // Execute agent with message
chat, // Simple chat (no tool execution)
loading, // Is executing
error, // Error state
conversationId, // Current conversation ID
toolCalls, // Array of tool calls made
reset, // 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">
<button
onClick={() => execute('Check my order status for #12345')}
disabled={loading}
>
Check Order
</button>
<button
onClick={() => 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 mount
conversationId?: string; // Resume existing conversation
context?: Record<string, any>; // Additional context for agent
onToolCall?: (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 execution
status, // 'idle' | 'running' | 'completed' | 'failed'
progress, // { current: number, total: number }
nodeStatuses, // Status of each node
output, // Final output data
error, // Error if failed
cancel, // Cancel running execution
executionId, // 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>
<button
onClick={handleExecute}
disabled={status === 'running'}
>
{status === 'running' ? 'Generating...' : 'Generate Content'}
</button>
{status === 'running' && (
<div className="progress">
<div
className="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 search
results, // Search results
loading, // Is searching
error, // Error state
insert, // Insert vectors
stats, // Database statistics
} = useVec('vec-db-id');
const handleSearch = async () => {
await search({
query, // Will auto-generate embedding
topK: 10,
filter: {
category: 'documentation',
},
includeMetadata: true,
});
};
return (
<div>
<div className="search-box">
<input
value={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 service
const 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
<ChatWindow
model="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
<AgentChat
agentId="your-agent-id"
showToolCalls={true}
className="h-full"
/>
// Individual components for custom layouts
<div className="flex flex-col h-full">
<MessageList
messages={messages}
renderMessage={(msg) => (
<CustomMessage message={msg} />
)}
/>
<MessageInput
onSend={handleSend}
loading={loading}
placeholder="Type a message..."
autoFocus
/>
</div>
// Workflow execution status
<WorkflowStatus
workflowId="workflow-id"
executionId={executionId}
showNodes={true}
/>
// Vector search box
<SearchBox
vecDbId="vec-db-id"
placeholder="Search..."
onResults={(results) => console.log(results)}
/>

Next.js Integration

App Router Setup

// app/layout.tsx
import { TenzroProvider } from '@tenzro/react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<TenzroProvider
apiKey={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">
<ChatWindow
model="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 server
const { 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) => (
<div
key={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.ts
import { 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 response
export 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 types
UseChatOptions,
UseChatReturn,
UseAgentOptions,
UseAgentReturn,
UseWorkflowOptions,
UseWorkflowReturn,
UseVecOptions,
UseVecReturn,
// Message types
ChatMessage,
AgentMessage,
ToolCall,
// Provider types
TenzroProviderProps,
TenzroContextValue,
// Component props
ChatWindowProps,
MessageListProps,
MessageInputProps,
} from '@tenzro/react';

Utilities

import {
cn, // Classname utility (clsx + tailwind-merge)
formatMessageTime, // Format message timestamps
truncateText, // Truncate long text with ellipsis
parseStreamedResponse, // Parse SSE stream
} from '@tenzro/react/utils';
// Usage
const 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 response
for 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