Skip to Content
PatternsAI Chat

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 useChat from @tanstack/ai-react and UIMessage types
  • ComposableAiChat is the shell, AiChatMessage renders messages, you iterate parts and render tools inline
  • Type-Safe Tool Rendering — Check part.name in the parts loop and TypeScript narrows part.output automatically
  • 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-chat

This registries setup 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-ModelName header 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 tools option 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; field optional, picks the primary key).
  • SUM, AVG, MIN, MAX — applied to a numeric field.
{ "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; field optional, picks the primary key).
  • SUM, AVG, MIN, MAX — applied to a numeric field.

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; field optional, picks the primary key).
  • SUM, AVG, MIN, MAX — applied to a numeric field.
{ "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.

PropTypeDefaultDescription
messagesUIMessage[]requiredMessages from useChat
isLoadingbooleanrequiredLoading state from useChat
onSendMessage(content: string) => voidrequiredSend handler
onStop() => voidrequiredStop/abort handler
childrenReactNodeMessage list (typically messages.map(...))
onClearChat() => voidClear handler
assistantNamestring"AI Assistant"Assistant display name
titlestringChat title in the header
emptyStateReactNodeCustom empty state
placeholderstringInput placeholder
showClearButtonbooleantrueShow the clear button
errorError | nullInline error banner

<AiChatMessage>

Renders a single message with avatar, name, markdown text, and children for custom content (tool output).

PropTypeDefaultDescription
messageUIMessagerequiredThe message to render
assistantNamestring"AI Assistant"Assistant display name
childrenReactNodeCustom content rendered below the message text (tool output, etc.)

AgentHubAdapterConfig

Configuration for the AgentHub adapter.

PropertyTypeDefaultDescription
baseUrlstringrequiredAgentHub base URL (/chat/completions is appended)
model{ vendor: 'openai' | 'anthropic'; name: string }requiredModel config
accessTokenstring | () => string | nullrequiredBearer token (refreshed per request if function)
systemPromptstring | () => stringSystem prompt prepended to messages (function form is called per request)
maxTokensnumber2048Max response tokens
temperaturenumber0.7Sampling temperature
toolsReadonlyArray<AnyClientTool>Client tools — wire-format definitions are derived automatically