AI Chat
A composable AI chat UI component for Apollo Vertex. Built with React, TypeScript, and Tailwind CSS. Designed to work with TanStack AI
Features
- TanStack AI Integration — Works with
useChatfrom@tanstack/ai-reactandUIMessagetypes - Composable —
AiChatis the shell,AiChatMessagerenders messages, you iterate parts and render tools inline - Type-Safe Tool Rendering — Check
part.namein the parts loop and TypeScript narrowspart.outputautomatically - AgentHub Adapter — Built-in adapter for the UiPath AgentHub normalized LLM endpoint (OpenAI + Anthropic models)
- Conversational Agent Adapter — Built-in adapter for a deployed UiPath Conversational Agent, with session management
- Markdown Rendering — Renders assistant responses with GitHub Flavored Markdown
- Data Fabric Table Tool — Display entity data as filterable tables with list, search, and range filters, and multi-entity joins
- Data Fabric Distribution Tool — Render histogram charts for numeric or datetime fields with optional aggregations, filters, and joins
- Data Fabric Bar Chart Tool — Render bar charts that break one or more metrics down by a categorical (string) dimension, with optional aggregations, filters, joins, and grouped bars when multiple metrics are passed
- Data Fabric Line Chart Tool — Render time-series line charts over a datetime field with optional aggregations, filters, and joins
- Data Fabric Multi-Line Chart Tool — Compare two metrics on a shared datetime axis, each with its own Y axis, totals label, and join support
- Data Fabric KPI Tool — Render a single scalar metric (count, sum, average, min, max) with optional filters and joins
- Suggestion Buttons — Interactive choice buttons rendered from tool results
Installation
Apollo Vertex components are published to a custom shadcn registry under the @uipath namespace. Before running the add command, register the namespace once in your project’s components.json so shadcn knows where to fetch from (otherwise the CLI will prompt you for a registry URL):
{
"registries": {
"@uipath": "https://apollo-vertex.vercel.app/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @uipath/ai-chatThis
registriessetup is a one-time configuration per project — every@uipath/*component on this site uses the same alias.
Quick Start
import { useChat } from '@tanstack/ai-react';
import { AiChat } from '@/components/ui/ai-chat/components/ai-chat';
import { AiChatMessage } from '@/components/ui/ai-chat/components/ai-chat-message';
import { createAgentHubConnection } from '@/components/ui/ai-chat/adapters/agenthub/adapter';
function BasicChat() {
const connection = createAgentHubConnection({
baseUrl: 'https://cloud.uipath.com/{org}/{tenant}/agenthub_/llm/api',
model: { vendor: 'openai' as const, name: 'gpt-4o' },
accessToken: () => getAccessToken(),
systemPrompt: 'You are a helpful assistant.',
});
const { messages, sendMessage, isLoading, stop, clear, error } = useChat({
connection,
});
return (
<AiChat
messages={messages}
isLoading={isLoading}
onSendMessage={(text) => sendMessage(text)}
onStop={stop}
onClearChat={clear}
error={error}
title="AI Assistant"
>
{messages.map((message) => (
<AiChatMessage key={message.id} message={message} />
))}
</AiChat>
);
}Tool Rendering
Render tool output inline in the chat — just like TanStack AI’s own examples. Define tools with toolDefinition, pass the input through as output in your client tool, then check part.name in the parts loop. TypeScript narrows part.output automatically.
import { z } from 'zod';
import { toolDefinition } from '@tanstack/ai';
import { clientTools } from '@tanstack/ai-client';
import { stream, useChat } from '@tanstack/ai-react';
import { AiChat } from '@/components/ui/ai-chat/components/ai-chat';
import { AiChatMessage } from '@/components/ui/ai-chat/components/ai-chat-message';
// 1. Define tools — output passes input through for rendering
const showResultsInput = z.object({
entityName: z.string(),
columns: z.array(z.string()),
});
const showResultsDef = toolDefinition({
name: 'show_results',
description: 'Display a results table',
inputSchema: showResultsInput,
outputSchema: showResultsInput,
});
const showResults = showResultsDef.client((input) => input);
const toolDefs = clientTools(showResults);
// 2. Wire it up — iterate parts, render tools inline
function ChatWithTools() {
const { messages, sendMessage, isLoading, stop } = useChat({
connection,
tools: toolDefs,
});
return (
<AiChat
messages={messages}
isLoading={isLoading}
onSendMessage={(text) => sendMessage(text)}
onStop={stop}
>
{messages.map((message) => (
<AiChatMessage key={message.id} message={message}>
{message.parts.map((part) => {
// TypeScript narrows part.output when you check part.name
if (part.type === 'tool-call' && part.name === 'show_results' && part.output) {
return <ResultsTable key={part.id} entity={part.output.entityName} columns={part.output.columns} />;
}
return null;
})}
</AiChatMessage>
))}
</AiChat>
);
}AgentHub Adapter
The built-in adapter for the UiPath AgentHub normalized LLM endpoint. It converts TanStack AI UIMessage arrays to the AgentHub wire format, calls the endpoint, and parses the SSE response back into AG-UI StreamChunk events.
import { createAgentHubConnection, type AgentHubAdapterConfig } from '@/components/ui/ai-chat/adapters/agenthub/adapter';
const connection = createAgentHubConnection({
baseUrl: 'https://cloud.uipath.com/{org}/{tenant}/agenthub_/llm/api',
model: { vendor: 'openai', name: 'gpt-4o' },
accessToken: () => getAccessToken(),
systemPrompt: 'You are a helpful assistant.',
maxTokens: 2048,
temperature: 0.7,
tools: toolDefs,
});The model.vendor field controls wire-format differences:
"openai"— flat tool definitions ({ name, description, parameters })"anthropic"— Anthropic tool format ({ type: "custom", input_schema }), non-empty assistant content on tool-call messages- The
X-UiPath-LlmGateway-NormalizedApi-ModelNameheader is always sent for routing - Responses are always OpenAI-compatible SSE regardless of the underlying model
Conversational Agent Adapter
The built-in adapter for a deployed UiPath Conversational Agent. It opens a session against the agent, forwards the latest user message, and bridges the agent’s streaming response back into TanStack AI StreamChunk events.
import { useChat } from '@tanstack/ai-react';
import { UiPath } from '@uipath/uipath-typescript/core';
import {
createConversationalAgentConnection,
type ConversationalAgentAdapterConfig,
} from '@/components/ui/ai-chat/adapters/conversational-agent/adapter';
const sdk = new UiPath({ /* baseUrl, accessToken, ... */ });
const connection = createConversationalAgentConnection({
sdk,
agentId, // number — the deployed agent id
folderId, // number — the folder the agent lives in
});
const { messages, sendMessage, isLoading, stop, clear, error } = useChat({
connection,
});
// Dispose the session when the connection is no longer needed
useEffect(() => () => connection.dispose(), [connection]);Notes:
- The adapter manages a single session per connection — call
connection.dispose()when unmounting, or key the component by agent id so a new connection is created on switch. - Tools are driven by the agent itself, not by the client — the
toolsoption is not used with this adapter. - Only the latest user message is sent per turn; prior history is tracked on the agent server-side.
Data Fabric Table Tool
The data_fabric_table tool renders entity data as interactive tables powered by @uipath/apollo-dashboarding. It supports server-side filtering — list filters, text search, and numeric ranges — so users can ask for filtered views directly in the chat.
import {
createDataFabricTableTool,
dataFabricTableClient,
} from '@/components/ui/ai-chat/tools/data-fabric-table';
const tableTool = createDataFabricTableTool({
entities, // Record<string, Entity> — entity metadata with field names and types
accessToken, // Bearer token for Data Fabric API
dataFabricBaseUrl, // Base URL for Data Fabric proxy
});
// Use dataFabricTableClient in your tools array, tableTool.toolPrompt in your system prompt,
// and tableTool.renderTable(part.output, part.id) in your parts loop.Filter types
The LLM can pass filters based on the user’s request:
- List filter — match or exclude specific values:
"show invoices where Status is Pending" - Search filter — text pattern matching (contains, startsWith, endsWith):
"find customers starting with A" - Range filter (numeric) — numeric min/max:
"show orders over $200" - Range filter (datetime) — ISO 8601 min/max:
"show orders from the last 30 days". The tool prompt is given today’s date so the LLM can resolve relative phrases into absolute ISO dates before calling the tool.
Filters are passed through the table configuration to @uipath/apollo-dashboarding, which translates them to Data Fabric query filters server-side.
Multi-entity joins
The tool can combine data from related entities via the joins argument. The entityName field is the primary entity, and each join supplies the entity to attach and an on clause with EntityName.FieldName references:
{
"entityName": "Invoice",
"dimensions": ["Invoice.Number", "Invoice.Total", "Customer.Name"],
"joins": [
{
"type": "LEFT",
"entity": "Customer",
"on": { "left": "Invoice.CustomerId", "right": "Customer.Id" }
}
]
}When joins are present, dimensions and filter fields must use qualified EntityName.FieldName names (using the exact entity names from the Entity Reference — never aliases). The join condition goes in joins[].on; don’t also add it as a filter.
Data Fabric Distribution Tool
The data_fabric_distribution tool renders a histogram from a Data Fabric entity by binning a single numeric or datetime field. It shares the filter and join system with the table tool, and adds an optional aggregation metric.
import {
createDataFabricDistributionTool,
dataFabricDistributionClient,
} from '@/components/ui/ai-chat/tools/data-fabric-distribution';
const distributionTool = createDataFabricDistributionTool({
entities, // Record<string, Entity> — entity metadata with field names and types
accessToken, // Bearer token for Data Fabric API
dataFabricBaseUrl, // Base URL for Data Fabric proxy
});
// Use dataFabricDistributionClient in your tools array, distributionTool.toolPrompt in your
// system prompt, and distributionTool.renderDistribution(part.output, part.id) in your parts loop.Dimension
The dimension is the field used for binning and must be numeric or datetime:
- Datetime dimensions bin by time (e.g. orders per month).
- Numeric dimensions bin by value range.
Metric
Omit metric entirely for the default COUNT of records per bin. To plot an aggregated numeric field, pass { aggregation, field }:
COUNT— records per bin (default;fieldoptional, picks the primary key).SUM,AVG,MIN,MAX— applied to a numericfield.
{
"entityName": "Order",
"dimension": "OrderDate",
"metric": { "aggregation": "SUM", "field": "Total" }
}Filters and joins
Filters and joins use the same schemas as the table tool (including the new datetime range filter). When joins are present, the dimension and metric.field must use qualified EntityName.FieldName names.
Data Fabric Bar Chart Tool
The data_fabric_bar tool renders a bar chart from a Data Fabric entity, breaking one or more metrics down by a categorical (string) dimension. With a single metric, each category gets one bar. With multiple metrics, each category gets a grouped cluster — one bar per metric. It shares the metric, filter, and join system with the other Data Fabric chart tools.
import {
createDataFabricBarTool,
dataFabricBarClient,
} from '@/components/ui/ai-chat/tools/data-fabric-bar';
const barTool = createDataFabricBarTool({
entities, // Record<string, Entity> — entity metadata with field names and types
accessToken, // Bearer token for Data Fabric API
dataFabricBaseUrl, // Base URL for Data Fabric proxy
});
// Use dataFabricBarClient in your tools array, barTool.toolPrompt in your
// system prompt, and barTool.renderBar(part.output, part.id) in your parts loop.When to use bar vs distribution vs line
- Bar — categorical breakdown by a string field: “orders by status”, “revenue by category”, “count by region”. One bar per discrete value, or grouped bars when comparing multiple metrics across categories.
- Distribution — histogram-style requests over numeric/datetime values: “distribution of order amount”, “histogram of X”.
- Line — single-metric trends over a datetime axis: “orders over time”, “revenue by month”.
Dimension
The dimension must be a string field. Numeric or datetime fields belong on a distribution or line chart.
Metrics
Pass metrics as an array of metric specs. Omit it entirely for the default single COUNT of records per category.
COUNT— records per category (default;fieldoptional, picks the primary key).SUM,AVG,MIN,MAX— applied to a numericfield.
With multiple metrics, the chart renders grouped bars — one cluster per dimension value, one bar per metric. Schema validation rejects duplicate (aggregation, field) pairs before the chart is rendered.
{
"entityName": "Order",
"dimension": "Status",
"metrics": [
{ "aggregation": "COUNT" },
{ "aggregation": "SUM", "field": "Total" }
]
}Filters and joins
Filter and join semantics are identical to the other Data Fabric tools. When joins are present, dimension and every metric field must use qualified EntityName.FieldName names.
Data Fabric Line Chart Tool
The data_fabric_line tool renders a line chart from a Data Fabric entity, plotting a metric over a datetime dimension. It shares the metric, filter, and join system with the distribution tool — the only constraint is that the dimension must be a datetime field.
import {
createDataFabricLineTool,
dataFabricLineClient,
} from '@/components/ui/ai-chat/tools/data-fabric-line';
const lineTool = createDataFabricLineTool({
entities, // Record<string, Entity> — entity metadata with field names and types
accessToken, // Bearer token for Data Fabric API
dataFabricBaseUrl, // Base URL for Data Fabric proxy
});
// Use dataFabricLineClient in your tools array, lineTool.toolPrompt in your
// system prompt, and lineTool.renderLine(part.output, part.id) in your parts loop.When to use line vs distribution
- Line — trend / time-series questions: “orders over time”, “revenue trend by month”, “growth across quarters”. Always datetime on the X axis.
- Distribution — histogram-style requests: “distribution of order amount”, “histogram of X”, numeric value-range binning. Either numeric or datetime dimension.
For comparing two or more metrics on the same time axis, use data_fabric_multi_line instead.
Metric, filters, and joins
Metric, filter, and join semantics are identical to the distribution tool. Omit metric for COUNT, or pass { aggregation, field } for SUM / AVG / MIN / MAX of a numeric field. When joins are present, dimension and metric.field must use qualified EntityName.FieldName names.
Data Fabric Multi-Line Chart Tool
The data_fabric_multi_line tool renders two lines on a shared datetime X axis. The first metric uses the left Y axis (and gets its own color and totals label at the top); the second uses the right Y axis. Exactly two metrics — pass three and the tool call is rejected.
import {
createDataFabricMultiLineTool,
dataFabricMultiLineClient,
} from '@/components/ui/ai-chat/tools/data-fabric-multi-line';
const multiLineTool = createDataFabricMultiLineTool({
entities, // Record<string, Entity> — entity metadata with field names and types
accessToken, // Bearer token for Data Fabric API
dataFabricBaseUrl, // Base URL for Data Fabric proxy
});
// Use dataFabricMultiLineClient in your tools array, multiLineTool.toolPrompt in your
// system prompt, and multiLineTool.renderMultiLine(part.output, part.id) in your parts loop.Metrics
Pass an array of exactly two metrics with distinct (aggregation, field) pairs. Each entry uses the same shape as the line tool — { aggregation: "COUNT" } for record counts (optional field), or { aggregation, field } for SUM / AVG / MIN / MAX of a numeric field. Schema validation rejects duplicate (aggregation, field) pairs before the chart is rendered. If duplicates only emerge after field resolution (e.g., a COUNT with an explicit field that resolves to the same primary key as a default COUNT), the chart shows its no-data state instead. Order matters: the first metric gets the left axis and primary color; put the metric you want to emphasize first.
{
"entityName": "Order",
"dimension": "OrderDate",
"metrics": [
{ "aggregation": "COUNT" },
{ "aggregation": "SUM", "field": "Total" }
]
}When to use multi-line vs line
- Multi-line — comparing two metrics on the same time axis: “orders count and revenue over time”, “min vs max price by month”.
- Line — a single metric over time. Don’t use multi-line for a single metric.
- For 3+ metrics, render multiple charts (one per metric, or pair them) — the underlying chart only has two Y axes.
Filters and joins
Identical to the line tool. Filter and join schemas are shared, and qualified EntityName.FieldName names are required for the dimension and every metric field when joins are present.
Data Fabric KPI Tool
The data_fabric_kpi tool renders a single scalar value — one aggregated metric across an entity, with no dimension or breakdown. It shares the metric, filter, and join system with the other Data Fabric chart tools.
import {
createDataFabricKpiTool,
dataFabricKpiClient,
} from '@/components/ui/ai-chat/tools/data-fabric-kpi';
const kpiTool = createDataFabricKpiTool({
entities, // Record<string, Entity> — entity metadata with field names and types
accessToken, // Bearer token for Data Fabric API
dataFabricBaseUrl, // Base URL for Data Fabric proxy
});
// Use dataFabricKpiClient in your tools array, kpiTool.toolPrompt in your
// system prompt, and kpiTool.renderKpi(part.output, part.id) in your parts loop.When to use KPI vs other chart tools
- KPI — single-number questions: “how many orders are open”, “total revenue”, “average invoice amount”, “max order total”. No dimension or breakdown.
- Line / Distribution — when the user wants the value sliced across a time or value axis (“over time”, “by month”, “distribution of”).
- Table — when the user wants individual records.
Metric
Omit metric entirely for the default COUNT of records. To plot an aggregated numeric field, pass { aggregation, field }:
COUNT— total record count (default;fieldoptional, picks the primary key).SUM,AVG,MIN,MAX— applied to a numericfield.
{
"entityName": "Order",
"metric": { "aggregation": "SUM", "field": "Total" }
}Filters and joins
Filter and join schemas are shared with the other Data Fabric tools (including the datetime range filter). When joins are present, the metric.field and any filter fields must use qualified EntityName.FieldName names.
Suggestion Buttons
The presentChoices tool renders interactive suggestion buttons. Define the tool with a Zod schema, and render choices inline in the parts loop:
import {
presentChoicesClient,
renderChoices,
CHOICES_TOOL_PROMPT,
} from '@/components/ui/ai-chat/tools/choices';
// Add presentChoicesClient to your tools array and CHOICES_TOOL_PROMPT to your system prompt.
// In your parts loop:
{message.parts.map((part) => {
if (part.type === 'tool-call' && part.name === 'presentChoices' && part.output) {
return (
<div key={part.id}>
{renderChoices(part.output, { onAction: (text) => sendMessage(text) })}
</div>
);
}
return null;
})}Try it out — type “give me some choices” in the demo above to see suggestion buttons in action.
API Reference
<AiChat>
Chat shell component. Handles layout, scroll, input, loading indicator, suggestions, and errors. Render messages as children.
| Prop | Type | Default | Description |
|---|---|---|---|
messages | UIMessage[] | required | Messages from useChat |
isLoading | boolean | required | Loading state from useChat |
onSendMessage | (content: string) => void | required | Send handler |
onStop | () => void | required | Stop/abort handler |
children | ReactNode | — | Message list (typically messages.map(...)) |
onClearChat | () => void | — | Clear handler |
assistantName | string | "AI Assistant" | Assistant display name |
title | string | — | Chat title in the header |
emptyState | ReactNode | — | Custom empty state |
placeholder | string | — | Input placeholder |
showClearButton | boolean | true | Show the clear button |
error | Error | null | — | Inline error banner |
<AiChatMessage>
Renders a single message with avatar, name, markdown text, and children for custom content (tool output).
| Prop | Type | Default | Description |
|---|---|---|---|
message | UIMessage | required | The message to render |
assistantName | string | "AI Assistant" | Assistant display name |
children | ReactNode | — | Custom content rendered below the message text (tool output, etc.) |
AgentHubAdapterConfig
Configuration for the AgentHub adapter.
| Property | Type | Default | Description |
|---|---|---|---|
baseUrl | string | required | AgentHub base URL (/chat/completions is appended) |
model | { vendor: 'openai' | 'anthropic'; name: string } | required | Model config |
accessToken | string | () => string | null | required | Bearer token (refreshed per request if function) |
systemPrompt | string | () => string | — | System prompt prepended to messages (function form is called per request) |
maxTokens | number | 2048 | Max response tokens |
temperature | number | 0.7 | Sampling temperature |
tools | ReadonlyArray<AnyClientTool> | — | Client tools — wire-format definitions are derived automatically |