UI Components
The @erdoai/ui package provides React components for rendering agent results, including charts, tables, and formatted content.
Installation
Peer Dependencies:
npm install react react-dom
ErdoProvider
Wrap your app with ErdoProvider to configure the SDK:
import { ErdoProvider } from '@erdoai/ui';
function App() {
return (
<ErdoProvider
config={{
baseUrl: 'https://api.erdo.ai',
// Use an ephemeral token for authenticated data fetching
// Token should be obtained from your backend via createToken()
token: ephemeralToken,
}}
>
{children}
</ErdoProvider>
);
}
Never expose API keys to the browser. For streaming invocations, use ephemeral tokens or proxy streaming. See Integration Patterns for secure patterns.
Config Options:
| Option | Type | Description |
|---|
baseUrl | string | API base URL (or your proxy URL) |
token | string | Ephemeral token from createToken() for authenticated data fetching |
dataFetcher | DataFetcher | Optional custom data fetcher (see Custom Data Fetching) |
ContentComponent | React.ComponentType | Optional component for rendering nested content |
threadId | string | Thread ID for dataset operations |
ContentComponent
The ContentComponent config option allows you to provide a default component for rendering nested content in invocation components (like InvocationEvents, Output, StepInvocation):
import { ErdoProvider, Content } from '@erdoai/ui';
<ErdoProvider
config={{
baseUrl: 'https://api.erdo.ai',
ContentComponent: Content, // Use SDK's default Content
}}
>
{children}
</ErdoProvider>
This is useful when:
- You want to customize how content is rendered across all invocation components
- You have your own Content component with custom renderers
- You want to avoid passing
ContentComponent as a prop to every component
Components will automatically use the provider’s ContentComponent as a default, but you can still override it via props if needed.
Custom Data Fetching
The SDK uses plain React state for data fetching (no React Query required). You can provide a custom DataFetcher to:
- Proxy requests through your own backend (keeping API keys server-side)
- Add custom authentication headers or logic
- Transform data before it reaches components
- Use your own API instead of Erdo’s REST endpoints
import { ErdoProvider, type DataFetcher } from '@erdoai/ui';
const customFetcher: DataFetcher = {
// Required: fetch dataset contents for charts/tables
fetchDatasetContents: async (slug, invocationId) => {
const response = await fetch(`/api/datasets/${slug}?invocationId=${invocationId}`);
return response.json();
},
// Optional: get dataset details for download buttons
getDatasetDetails: async (slug, threadId) => {
const response = await fetch(`/api/datasets/${slug}/details?threadId=${threadId}`);
if (!response.ok) return null;
return response.json(); // { id: string, name: string }
},
// Optional: download dataset as file
downloadDataset: async (datasetId) => {
const response = await fetch(`/api/datasets/${datasetId}/download`);
return response.blob();
},
};
function App() {
return (
<ErdoProvider
config={{
baseUrl: 'https://api.erdo.ai',
dataFetcher: customFetcher,
}}
>
{children}
</ErdoProvider>
);
}
DataFetcher Interface:
| Method | Required | Description |
|---|
fetchDatasetContents(slug, invocationId) | Yes | Fetch dataset rows for charts/tables |
getDatasetDetails(slug, threadId) | No | Get dataset ID and name for download buttons |
downloadDataset(datasetId) | No | Download dataset file as Blob |
If no dataFetcher is provided, the SDK falls back to the REST API using baseUrl and token/authToken for authentication.
Hooks
useThread
Send messages to threads with streaming support:
import { useThread, Content } from '@erdoai/ui';
function ChatInterface() {
const { isStreaming, streamingContents, error, sendMessage } = useThread({
botKey: 'data-analyst',
onFinish: () => console.log('Done'),
onError: (error) => console.error('Error:', error),
});
return (
<div>
<button onClick={() => sendMessage('Analyze data')}>
{isStreaming ? 'Analyzing...' : 'Analyze'}
</button>
{error && <div className="error">{error.message}</div>}
{streamingContents.map((content) => (
<Content key={content.id} content={content} />
))}
</div>
);
}
Options:
| Option | Type | Description |
|---|
botKey | string | Bot key for the thread (e.g., 'data-analyst') |
threadId | string | Optional existing thread ID to resume |
onFinish | () => void | Called when message streaming completes |
onError | (error: Error) => void | Called on error |
Returns:
| Property | Type | Description |
|---|
streamingContents | ContentItem[] | Visible content items ready for rendering |
isStreaming | boolean | Whether currently streaming |
error | Error | null | Most recent error |
sendMessage | (content: string) => Promise<void> | Send a message |
thread | Thread | null | Current thread object |
activeMessages | MessageWithContents[] | Raw messages (for advanced use) |
setThread | (thread: Thread) => void | Set thread manually |
useDatasetContents
Fetch dataset contents using plain React state (no React Query required):
import { useDatasetContents } from '@erdoai/ui';
function DataView({ datasetSlug, invocationId }) {
const { data, isLoading, error, refetch } = useDatasetContents(datasetSlug, invocationId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={refetch}>Refresh</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
The hook uses the DataFetcher from the provider if available, otherwise falls back to the REST API.
useMultipleDatasetContents
Fetch multiple datasets in parallel:
import { useMultipleDatasetContents } from '@erdoai/ui';
function MultiDataView({ invocationId }) {
const results = useMultipleDatasetContents(
['dataset-1', 'dataset-2'],
invocationId
);
const isLoading = results.some(r => r.isLoading);
const allData = results.map(r => r.data || []);
if (isLoading) return <div>Loading...</div>;
return <pre>{JSON.stringify(allData, null, 2)}</pre>;
}
Content Renderers
Content
Auto-routes to the appropriate renderer based on content type:
import { Content } from '@erdoai/ui';
function ResultView({ content }) {
return <Content content={content} />;
}
Custom Renderers
Override how specific content types are rendered using the components prop:
import { Content } from '@erdoai/ui';
// Custom component receives { content, className } props
function MyBotInvocationRenderer({ content, className }) {
return (
<div className={className}>
<h3>Bot: {content.content.bot_name}</h3>
{/* Custom rendering logic */}
</div>
);
}
function ResultView({ content }) {
return (
<Content
content={content}
components={{
'bot_invocation': MyBotInvocationRenderer,
'my_custom_type': MyCustomRenderer,
}}
/>
);
}
The components prop is a Record<string, ComponentType> where:
- Key: The
content_type or ui_content_type string (e.g., 'bot_invocation', 'text', 'chart')
- Value: A React component that receives
{ content, className } props
When Content encounters a content type, it first checks if there’s a custom renderer in components, then falls back to built-in renderers.
Individual Renderers
import {
TextContent,
JsonContent,
MarkdownContent,
TableContent,
ChartContent,
} from '@erdoai/ui';
// Text
<TextContent content="Hello world" />
// JSON
<JsonContent content={{ key: 'value' }} />
// Markdown
<MarkdownContent text="# Heading\n\nParagraph text" />
// Table
<TableContent
columns={[{ key: 'name', column_name: 'Name' }]}
data={[{ name: 'Alice' }, { name: 'Bob' }]}
/>
Chart Components
All charts support download functionality via the download button.
BarChart
import { BarChart } from '@erdoai/ui';
<BarChart
title="Monthly Sales"
data={[
{ month: 'Jan', revenue: 1000 },
{ month: 'Feb', revenue: 1200 },
]}
displayConfig={{
month: { label: 'Month' },
revenue: { label: 'Revenue', color: '#3b82f6' },
}}
dataConfig={{
chartType: 'bar',
xAxis: { key: 'month', label: 'Month', type: 'category' },
yAxes: [{ key: 'revenue', label: 'Revenue ($)', type: 'number' }],
series: [{ key: 'revenue', name: 'Revenue', color: '#3b82f6' }],
}}
/>
LineChart
import { LineChart } from '@erdoai/ui';
<LineChart
title="Revenue Trend"
data={data}
displayConfig={config}
dataConfig={{
chartType: 'line',
xAxis: { key: 'date', label: 'Date', type: 'date', format: 'MMM yyyy' },
yAxes: [{ key: 'revenue', label: 'Revenue', type: 'number' }],
series: [{ key: 'revenue', name: 'Revenue', color: '#10b981' }],
}}
/>
PieChart
import { PieChart } from '@erdoai/ui';
<PieChart
title="Sales by Category"
data={[
{ category: 'Electronics', sales: 45 },
{ category: 'Clothing', sales: 30 },
{ category: 'Food', sales: 25 },
]}
displayConfig={config}
dataConfig={{
chartType: 'pie',
xAxis: { key: 'category', label: 'Category' },
yAxes: [{ key: 'sales', label: 'Sales %' }],
series: [{ key: 'sales', name: 'Sales', color: '#6366f1' }],
}}
/>
ScatterChart
import { ScatterChart } from '@erdoai/ui';
<ScatterChart
title="Price vs Quantity"
data={data}
displayConfig={config}
dataConfig={{
chartType: 'scatter',
xAxis: { key: 'price', label: 'Price', type: 'number' },
yAxes: [{ key: 'quantity', label: 'Quantity', type: 'number' }],
series: [{ key: 'quantity', name: 'Quantity', color: '#f59e0b' }],
}}
/>
HeatmapChart
import { HeatmapChart } from '@erdoai/ui';
<HeatmapChart
title="Activity Heatmap"
data={data}
displayConfig={config}
dataConfig={{
chartType: 'heatmap',
xAxis: { key: 'hour', label: 'Hour', type: 'number' },
yAxes: [{ key: 'day', label: 'Day', type: 'category' }],
series: [{ key: 'value', name: 'Activity', color: '#ef4444' }],
}}
/>
Chart Props
All chart components accept these common props:
| Prop | Type | Description |
|---|
title | string | Chart title |
subtitle | string | Optional subtitle |
data | any[] | Data array |
displayConfig | ChartConfig | Display configuration |
dataConfig | DataConfig | Data mapping configuration |
stacked | boolean | Stack series (bar charts) |
disableAnimation | boolean | Disable animations |
enableDownload | boolean | Show download button (default: true) |
onDownloadSuccess | (fileName) => void | Download success callback |
onDownloadError | (error) => void | Download error callback |
onZoomChange | (domain) => void | Zoom change callback |
Styling
@erdoai/ui components use Tailwind CSS classes and CSS custom properties (variables). You need to configure both for the components to render correctly.
1. Tailwind Configuration
Add @erdoai/ui to your Tailwind content paths:
// tailwind.config.js
module.exports = {
content: [
'./node_modules/@erdoai/ui/**/*.js',
// ... your content paths
],
};
2. CSS Variables
Components rely on CSS variables for theming. If you’re using shadcn/ui, these are already defined. Otherwise, add them to your global CSS:
/* globals.css or app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
Using with shadcn/ui
If you’re already using shadcn/ui, the CSS variables are configured automatically. Just ensure @erdoai/ui is in your Tailwind content paths.
Minimal Setup (No shadcn/ui)
If you’re not using shadcn/ui but want Erdo charts to work:
- Add the CSS variables above to your global styles
- Configure Tailwind content paths
- Components will use your theme colors automatically
The --chart-1 through --chart-5 variables are used for chart colors. Customize these to match your brand.
Advanced: Custom Content Renderers
The Content component handles all content types automatically. For advanced use cases, you can override specific content type renderers using the components prop.
Building Block Components
When building custom renderers that need to display nested content (like bot invocations with steps), these building block components are available:
import {
InvocationEvents, // Renders nested steps/outputs
StepInvocation, // Renders a single step
Output, // Renders output contents
} from '@erdoai/ui';
Example: Custom Bot Invocation Renderer
import { Content, InvocationEvents } from '@erdoai/ui';
function MyBotInvocationRenderer({ content, className }) {
const data = content.content;
const botInvocation = content.botInvocation;
return (
<div className={className}>
<h3>Custom Bot: {data.bot_name}</h3>
{botInvocation && (
<InvocationEvents eventsByID={botInvocation.eventsByID} />
)}
</div>
);
}
// Use in Content
<Content
content={content}
components={{
'bot_invocation': MyBotInvocationRenderer,
}}
/>
Most applications don’t need custom renderers. The default Content component handles all standard content types including nested bot invocations, charts, tables, and markdown.