From 04cf33eecaaa6d4a2f8bb257937321f57a3e0fad Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 20 Apr 2026 11:09:52 -0400 Subject: [PATCH 1/4] feat(apollo-vertex): ai-chat templates and documentation pages Adds AiChatTemplate, AiChatAgentHubMode, AiChatConversationalAgentMode, AiChatLoginGate, interactive preview page with mock data and thinking demo, patterns/ai-chat doc page, and vertex-components nav entry. Co-Authored-By: Claude Sonnet 4.6 --- apps/apollo-vertex/app/_meta.ts | 1 + .../app/patterns/ai-chat/page.mdx | 238 +--------- .../ai-chat/preview/mock-data.ts | 218 +++++++++ .../ai-chat/preview/page.tsx | 440 ++++++++++++++++++ .../ai-chat/preview/thinking-demo.tsx | 63 +++ apps/apollo-vertex/registry/ai-chat/types.ts | 7 + .../templates/AiChatTemplate.tsx | 10 +- .../templates/ai-chat/AiChatAgentHubMode.tsx | 170 +------ .../ai-chat/AiChatConversationalAgentMode.tsx | 19 +- .../templates/ai-chat/AiChatLoginGate.tsx | 16 +- 10 files changed, 798 insertions(+), 384 deletions(-) create mode 100644 apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts create mode 100644 apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx create mode 100644 apps/apollo-vertex/app/vertex-components/ai-chat/preview/thinking-demo.tsx diff --git a/apps/apollo-vertex/app/_meta.ts b/apps/apollo-vertex/app/_meta.ts index 152665709..133fd3f89 100644 --- a/apps/apollo-vertex/app/_meta.ts +++ b/apps/apollo-vertex/app/_meta.ts @@ -6,6 +6,7 @@ export default { templates: "Templates", guidelines: "Guidelines", experiment: "Experiment", + "vertex-components": "Vertex Components", "data-querying": "Data Querying", localization: "Localization", mcp: "MCP Server", diff --git a/apps/apollo-vertex/app/patterns/ai-chat/page.mdx b/apps/apollo-vertex/app/patterns/ai-chat/page.mdx index c4383df18..31afed000 100644 --- a/apps/apollo-vertex/app/patterns/ai-chat/page.mdx +++ b/apps/apollo-vertex/app/patterns/ai-chat/page.mdx @@ -1,12 +1,15 @@ -import { AiChatTemplateLazy } from '@/templates/AiChatTemplateLazy'; +import { AiChatTemplate } from '@/templates/AiChatTemplate'; +import { PreviewFullScreen } from '@/app/_components/preview-full-screen'; # AI Chat -A composable AI chat UI component for Apollo Vertex. Built with React, TypeScript, and Tailwind CSS. Designed to work with [TanStack AI](https://tanstack.com/ai) +A composable AI chat UI component for Apollo Vertex. Built with React, TypeScript, and Tailwind CSS. Designed to work with [TanStack AI](https://tanstack.com/ai) — you bring `useChat` and a connection adapter, the component handles the chrome (scroll, input, loading, suggestions, errors) while you control how messages and tool calls render. -
- -
+ + + + +> **All visual states and sub-components →** [Component Preview](/vertex-components/ai-chat/preview) ## Features @@ -14,33 +17,20 @@ A composable AI chat UI component for Apollo Vertex. Built with React, TypeScrip - **Composable** — `AiChat` 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 Line Chart Tool** — Render time-series line charts over a datetime field with optional aggregations, filters, and joins -- **Suggestion Buttons** — Interactive choice buttons rendered from tool results +- **Error Display** — Inline error banner for API and network errors +- **i18n Support** — Built-in internationalization via react-i18next +- **Accessible** — WCAG 2.1 compliant with keyboard navigation and ARIA live regions ## 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): - -```json -{ - "registries": { - "@uipath": "https://apollo-vertex.vercel.app/r/{name}.json" - } -} -``` - -Then install the component: - ```bash 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 ```tsx @@ -161,204 +151,24 @@ The `model.vendor` field controls wire-format differences: - 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. - -```tsx -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. - -```tsx -import { - createDataFabricTableTool, - dataFabricTableClient, -} from '@/components/ui/ai-chat/tools/data-fabric-table'; - -const tableTool = createDataFabricTableTool({ - entities, // Record — 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: - -```json -{ - "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. +## Error Display ---- - -## 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. - -```tsx -import { - createDataFabricDistributionTool, - dataFabricDistributionClient, -} from '@/components/ui/ai-chat/tools/data-fabric-distribution'; - -const distributionTool = createDataFabricDistributionTool({ - entities, // Record — 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`. - -```json -{ - "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 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. +Pass an `Error` object to show an inline error banner: ```tsx -import { - createDataFabricLineTool, - dataFabricLineClient, -} from '@/components/ui/ai-chat/tools/data-fabric-line'; - -const lineTool = createDataFabricLineTool({ - entities, // Record — 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. + sendMessage(text)} + onStop={stop} + error={error} +> + {messages.map((message) => ( + + ))} + ``` -### 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. - -### 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. - ---- - -## Suggestion Buttons - -The `presentChoices` tool renders interactive suggestion buttons. Define the tool with a Zod schema, and render choices inline in the parts loop: - -```tsx -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 ( -
- {renderChoices(part.output, { onAction: (text) => sendMessage(text) })} -
- ); - } - return null; -})} -``` - -> **Try it out** — type *"give me some choices"* in the demo above to see suggestion buttons in action. - - --- ## API Reference diff --git a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts new file mode 100644 index 000000000..22630face --- /dev/null +++ b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts @@ -0,0 +1,218 @@ +import type { UIMessage } from "@tanstack/ai-client"; + +export const MOCK_MESSAGES_BASIC: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [{ type: "text", content: "What is Apollo Design System?" }], + createdAt: new Date(Date.now() - 120000), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: + "Apollo is UiPath's open-source design system. It provides a unified component library with **design tokens**, **React components**, and **Web Components** for building consistent user interfaces.", + }, + ], + createdAt: new Date(Date.now() - 60000), + }, +]; + +export const MOCK_MESSAGES_MARKDOWN: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [ + { + type: "text", + content: "Show me all the markdown formatting you support.", + }, + ], + createdAt: new Date(), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: `Here's a comprehensive demo of supported markdown: + +## Headings & Text + +This is a paragraph with **bold**, *italic*, and \`inline code\`. + +> This is a blockquote with some important information. + +--- + +## Lists + +**Unordered list:** +- First item +- Second item with **bold** +- Third item + +**Ordered list:** +1. Step one +2. Step two +3. Step three + +## Code Block + +\`\`\`typescript +interface AiChatProps { + messages: UIMessage[]; + isLoading: boolean; + onSendMessage: (content: string) => void; +} +\`\`\` + +## Table + +| Component | Status | Description | +|-----------|--------|-------------| +| AiChat | Ready | Main chat shell | +| AiChatMessage | Ready | Message renderer | +| AiChatInput | Ready | Text input | +| AiChatMarkdown | Ready | Markdown renderer | + +## Links + +Check [Apollo UI](https://github.com/UiPath/apollo-ui) for more info.`, + }, + ], + createdAt: new Date(), + }, +]; + +export const MOCK_MESSAGES_CONVERSATION: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [{ type: "text", content: "Can you help me set up a new project?" }], + createdAt: new Date(Date.now() - 300000), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: + "Of course! I'd be happy to help. What kind of project are you looking to create?", + }, + ], + createdAt: new Date(Date.now() - 240000), + }, + { + id: "3", + role: "user", + parts: [ + { + type: "text", + content: "A React app with Tailwind CSS and TypeScript.", + }, + ], + createdAt: new Date(Date.now() - 180000), + }, + { + id: "4", + role: "assistant", + parts: [ + { + type: "text", + content: + "Great choice! Here's how to get started:\n\n1. Run `npx create-next-app@latest`\n2. Select **TypeScript** and **Tailwind CSS** during setup\n3. Install Apollo components:\n\n```bash\nnpx shadcn@latest add @uipath/button\n```\n\nWould you like me to walk through any of these steps in detail?", + }, + ], + createdAt: new Date(Date.now() - 120000), + }, + { + id: "5", + role: "user", + parts: [{ type: "text", content: "Yes, explain step 3 please." }], + createdAt: new Date(Date.now() - 60000), + }, + { + id: "6", + role: "assistant", + parts: [ + { + type: "text", + content: + "The `shadcn` CLI copies component source code directly into your project. When you run the command, it will:\n\n- Create `components/ui/button.tsx` in your project\n- Add any required dependencies to `package.json`\n- Set up the `cn()` utility if not present\n\nYou own the code — modify it however you need.", + }, + ], + createdAt: new Date(), + }, +]; + +export const MOCK_MESSAGES_WITH_CHOICES: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [{ type: "text", content: "What presentation style should I use?" }], + createdAt: new Date(), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: "Here are some options based on your audience:", + }, + { + type: "tool-call", + id: "call-1", + name: "presentChoices", + arguments: "{}", + state: "input-complete" as const, + }, + { + type: "tool-result", + toolCallId: "call-1", + state: "complete" as const, + content: JSON.stringify({ + type: "choices", + prompt: "Pick a presentation style:", + options: [ + { id: "1", label: "Executive Summary", recommended: true }, + { id: "2", label: "Technical Deep Dive" }, + { id: "3", label: "Workshop Format" }, + ], + }), + }, + ], + createdAt: new Date(), + }, +]; + +export const MOCK_SOURCES_BASIC: Record< + string, + { label: string; url: string }[] +> = { + "2": [ + { label: "Apollo UI GitHub", url: "https://github.com/UiPath/apollo-ui" }, + { label: "Tailwind CSS", url: "https://tailwindcss.com" }, + { label: "shadcn/ui", url: "https://ui.shadcn.com" }, + ], +}; + +export const MOCK_ATTACHMENTS_BASIC: Record< + string, + { name: string; type: string; size: number }[] +> = { + "1": [{ name: "Q4_Report.pdf", type: "application/pdf", size: 245000 }], +}; + +export const MOCK_CHOICE_OPTIONS = [ + { id: "1", label: "Option A — Recommended", recommended: true }, + { id: "2", label: "Option B — Alternative" }, + { id: "3", label: "Option C — Experimental" }, + { id: "4", label: "Option D — Conservative" }, +]; diff --git a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx new file mode 100644 index 000000000..e1dad22f8 --- /dev/null +++ b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx @@ -0,0 +1,440 @@ +// oxlint-disable eslint/max-lines -- preview page documents many visual states +"use client"; + +import { Maximize2, Minimize2 } from "lucide-react"; +import { type ReactNode, useState } from "react"; +import { AiChat } from "@/registry/ai-chat/components/ai-chat"; +import { AiChatCodeBlock } from "@/registry/ai-chat/components/ai-chat-code-block"; +import { AiChatEmptyState } from "@/registry/ai-chat/components/ai-chat-empty-state"; +import { AiChatInput } from "@/registry/ai-chat/components/ai-chat-input"; +import { AiChatMarkdown } from "@/registry/ai-chat/components/ai-chat-markdown"; +import { AiChatMessage } from "@/registry/ai-chat/components/ai-chat-message"; +import { AiChatMessageActions } from "@/registry/ai-chat/components/ai-chat-message-actions"; +import { AiChatSuggestions } from "@/registry/ai-chat/components/ai-chat-suggestions"; +import { Button } from "@/registry/button/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/registry/dialog/dialog"; +import { + MOCK_ATTACHMENTS_BASIC, + MOCK_CHOICE_OPTIONS, + MOCK_MESSAGES_BASIC, + MOCK_MESSAGES_CONVERSATION, + MOCK_MESSAGES_MARKDOWN, + MOCK_MESSAGES_WITH_CHOICES, + MOCK_SOURCES_BASIC, +} from "./mock-data"; +import { ThinkingDemo } from "./thinking-demo"; + +function noop(..._args: unknown[]) { + // no-op for preview +} + +function SectionHeader({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
+

{title}

+

{description}

+
+ ); +} + +function PreviewCard({ + children, + className, + title, +}: { + children: ReactNode; + className?: string; + title?: string; +}) { + const [isOpen, setIsOpen] = useState(false); + const baseClass = `border rounded-lg bg-background overflow-hidden ${className ?? ""}`; + + if (!title) { + return
{children}
; + } + + return ( + +
+ {!isOpen && children} + + + +
+ + {title} + + + +
{children}
+
+
+ ); +} + +function renderBasicMsg(msg: (typeof MOCK_MESSAGES_BASIC)[number]) { + return ( + + ); +} + +export default function AiChatPreviewPage() { + return ( +
+
+

+ {"AI Chat — Component Preview"} +

+

+ {"All visual states and sub-components rendered with mock data."} +

+
+ + {/* ── 1. Empty State (default) ────────────────────────── */} +
+ + + + +
+ + {/* ── 2. Empty State (with suggestions) ──────────────── */} +
+ + + + } + suggestions={["Create a React app", "Debug my code", "Write tests"]} + /> + +
+ + {/* ── 3. Basic Conversation with timestamps ──────────── */} +
+ + + + {MOCK_MESSAGES_BASIC.map((msg) => renderBasicMsg(msg))} + + +
+ + {/* ── 4. Multi-turn Conversation ─────────────────────── */} +
+ + + + {MOCK_MESSAGES_CONVERSATION.map((msg) => ( + + ))} + + +
+ + {/* ── 5. Markdown + Code Block ───────────────────────── */} +
+ + + + {MOCK_MESSAGES_MARKDOWN.map((msg) => ( + + ))} + + +
+ + {/* ── 6. Loading State ───────────────────────────────── */} +
+ +
+ + + {renderBasicMsg(MOCK_MESSAGES_BASIC[0])} + + + +

+ {"Thinking indicator (interactive)"} +

+ +
+
+
+ + {/* ── 7. Error + Retry ───────────────────────────────── */} +
+ + + + {MOCK_MESSAGES_BASIC.map((msg) => renderBasicMsg(msg))} + + +
+ + {/* ── 8. Suggestion Buttons ──────────────────────────── */} +
+ + + + {MOCK_MESSAGES_WITH_CHOICES.map((msg) => ( + + ))} + + +
+ + {/* ── 9. Message Actions ─────────────────────────────── */} +
+ +
+ +

+ {"Assistant actions"} +

+ +
+ +

+ {"User actions"} +

+ +
+
+
+ + {/* ── 10. Edit Mode ──────────────────────────────────── */} +
+ + + + {MOCK_MESSAGES_BASIC.map((msg) => renderBasicMsg(msg))} + + +
+ + {/* ── 11. Isolated Sub-Components ────────────────────── */} +
+ +
+ +

+ {"AiChatSuggestions (with recommended)"} +

+ +
+ + +

+ {"AiChatInput (with text + attachment)"} +

+ +
+ + +

+ {"AiChatCodeBlock"} +

+ + {`import { AiChat } from "@/components/ui/ai-chat"; + +function App() { + return ; +}`} + +
+ + +

+ {"AiChatMarkdown (standalone)"} +

+ + {`Here's a quick example with **bold**, *italic*, \`code\`, and a [link](https://example.com).\n\n| Feature | Supported |\n|---------|----------|\n| Tables | Yes |\n| Code | Yes |`} + +
+
+
+
+ ); +} diff --git a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/thinking-demo.tsx b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/thinking-demo.tsx new file mode 100644 index 000000000..e19705208 --- /dev/null +++ b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/thinking-demo.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { motion } from "framer-motion"; +import { useState } from "react"; +import { AiChatThinking } from "@/registry/ai-chat/components/ai-chat-thinking"; +import { Button } from "@/registry/button/button"; + +const ENTRANCE_EASE = [0.22, 1, 0.36, 1] as const; + +const shimmerStyle = { + display: "inline-block", + whiteSpace: "nowrap", + lineHeight: 1.3, + fontSize: "14px", + fontWeight: 500, + backgroundImage: + "linear-gradient(90deg, var(--muted-foreground) 0%, var(--muted-foreground) 30%, #6C5AEF 42%, var(--foreground) 50%, #69C7DD 58%, var(--muted-foreground) 70%, var(--muted-foreground) 100%)", + backgroundSize: "200% 100%", + backgroundClip: "text", + WebkitBackgroundClip: "text", + color: "transparent", + animation: "ap-chat-shimmer-thinking 2.4s linear infinite", +} as const; + +export function ThinkingDemo() { + const [isThinking, setIsThinking] = useState(false); + + return ( +
+ +
+ + + {"Thinking\u2026"} + +
+ +
+ ); +} diff --git a/apps/apollo-vertex/registry/ai-chat/types.ts b/apps/apollo-vertex/registry/ai-chat/types.ts index 6eb99f528..f66f97d08 100644 --- a/apps/apollo-vertex/registry/ai-chat/types.ts +++ b/apps/apollo-vertex/registry/ai-chat/types.ts @@ -4,6 +4,13 @@ export type { } from "./components/ai-chat-message"; export type MessageFeedbackType = "up" | "down"; +export interface ChoiceOption { + id: string; + label: string; + value?: string; + recommended?: boolean; +} + export interface MessageAction { /** Unique key for the action */ key: string; diff --git a/apps/apollo-vertex/templates/AiChatTemplate.tsx b/apps/apollo-vertex/templates/AiChatTemplate.tsx index ef8fcc385..fca0a6109 100644 --- a/apps/apollo-vertex/templates/AiChatTemplate.tsx +++ b/apps/apollo-vertex/templates/AiChatTemplate.tsx @@ -34,8 +34,8 @@ function AiChatWithConnection({ }); return ( -
-
+
+
{ @@ -64,7 +64,7 @@ function AiChatWithConnection({
-
+
{mode === "agenthub" ? ( ) : ( @@ -78,9 +78,9 @@ function AiChatWithConnection({ ); } -export function AiChatTemplate() { +export function AiChatTemplate({ className }: { className?: string }) { return ( -
+
; } -function AgentHubChatInner({ - accessToken, - orgTenant, - entities, -}: AgentHubChatInnerProps) { +export function AgentHubChat({ accessToken, orgTenant }: AgentHubChatProps) { const { t } = useTranslation(); - const dataFabricBaseUrl = `/api/datafabric/${orgTenant.orgName}/${orgTenant.tenantName}/datafabric_/api`; - - const tableTool = createDataFabricTableTool({ - entities, - accessToken, - dataFabricBaseUrl, - }); - - const distributionTool = createDataFabricDistributionTool({ - entities, - accessToken, - dataFabricBaseUrl, - }); - - const lineTool = createDataFabricLineTool({ - entities, - accessToken, - dataFabricBaseUrl, - }); - - const tools = clientTools( - presentChoicesClient, - dataFabricTableClient, - dataFabricDistributionClient, - dataFabricLineClient, - ); - - const chartToolSteering = [ - "When the user asks about Data Fabric data, pick the chart tool that best fits the request:", - '- "data_fabric_table" — list/show records, view fields side by side.', - '- "data_fabric_line" — trend / time-series questions ("orders over time", "revenue by month", "growth across quarters").', - '- "data_fabric_distribution" — histogram-style requests ("distribution of X", "histogram of X", numeric value-range binning).', - "If two tools could both answer, prefer the more specific one (line beats distribution for explicit time-series phrasing).", - ].join("\n"); - - const systemPrompt = [ - "You are a helpful assistant. Always respond using markdown format.", - CHOICES_TOOL_PROMPT, - chartToolSteering, - tableTool.toolPrompt, - distributionTool.toolPrompt, - lineTool.toolPrompt, - ].join("\n\n"); - const connection = createAgentHubConnection({ baseUrl: `/api/agenthub/${orgTenant.orgName}/${orgTenant.tenantName}/agenthub_/llm/api`, model: { vendor: "openai", name: "gpt-4.1-mini-2025-04-14" }, accessToken: () => accessToken, systemPrompt, - tools, }); - const { messages, sendMessage, isLoading, stop, clear, error } = useChat({ - connection, - tools, - }); + const { messages, sendMessage, isLoading, stop, clear, reload, error } = + useChat({ connection }); return ( { + void reload(); + }} + onEditMessage={(_id, content) => { + void sendMessage(content); + }} + title="Autopilot" assistantName={t("assistant")} + enableTextSelection error={error ?? null} + emptyState={} + suggestions={[ + "Summarize a PDF", + "Create an executive brief", + "Draft a follow-up for my last meeting", + ]} > {messages.map((message) => ( - - {message.parts.map((part) => { - if (part.type !== "tool-call" || !part.output) return null; - - if (part.name === "data_fabric_table") { - return ( - - {tableTool.renderTable(part.output, part.id)} - - ); - } - - if (part.name === "data_fabric_distribution") { - return ( - - {distributionTool.renderDistribution(part.output, part.id)} - - ); - } - - if (part.name === "data_fabric_line") { - return ( - - {lineTool.renderLine(part.output, part.id)} - - ); - } - - if (part.name === "presentChoices") { - return ( -
- {renderChoices(part.output, { - onAction: (text) => { - void sendMessage(text); - }, - })} -
- ); - } - - return null; - })} -
+ ))}
); } - -export function AgentHubChat({ accessToken, orgTenant }: AgentHubChatProps) { - const sdk = createUiPathSdk(accessToken, orgTenant); - const entitiesService = new Entities(sdk); - - return ( - - {({ entities }) => ( - - )} - - ); -} diff --git a/apps/apollo-vertex/templates/ai-chat/AiChatConversationalAgentMode.tsx b/apps/apollo-vertex/templates/ai-chat/AiChatConversationalAgentMode.tsx index 5ed2d6d06..1cfb33183 100644 --- a/apps/apollo-vertex/templates/ai-chat/AiChatConversationalAgentMode.tsx +++ b/apps/apollo-vertex/templates/ai-chat/AiChatConversationalAgentMode.tsx @@ -39,9 +39,10 @@ function ConversationalAgentChatInner({ }; }, [connection]); - const { messages, sendMessage, isLoading, stop, clear, error } = useChat({ - connection, - }); + const { messages, sendMessage, isLoading, stop, clear, reload, error } = + useChat({ + connection, + }); return ( { + void reload(); + }} title={title} assistantName={assistantName} + enableTextSelection error={error ?? null} > {messages.map((message) => ( - + ))} ); @@ -151,7 +152,7 @@ export function ConversationalAgentChat({ sdk={sdk} agentId={selectedAgentConfig.agentId} folderId={selectedAgentConfig.folderId} - title={t("ai_assistant")} + title="Autopilot" assistantName={t("assistant")} />
diff --git a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx index c14afab29..d3b73a33d 100644 --- a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx +++ b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx @@ -1,9 +1,9 @@ "use client"; +import { useLocalStorage } from "@mantine/hooks"; import { useQuery } from "@tanstack/react-query"; import { jwtDecode } from "jwt-decode"; import { ChevronRight, LogIn, LogOut } from "lucide-react"; -import { useLocalStorage } from "@mantine/hooks"; import type { ReactNode } from "react"; import { z } from "zod"; import { Button } from "@/components/ui/button"; @@ -194,7 +194,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { if (isLoading) { return ( -
+
Signing in...
); @@ -202,7 +202,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { if (!isAuthenticated) { return ( -
+

Sign in to UiPath to use the AI Chat demo

@@ -221,7 +221,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { if (isOrgError) { return ( -
+

We couldn't load your organization info. Please try signing out and signing back in. @@ -233,7 +233,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { if (isOrgLoading || !accessToken) { return ( -

+
Loading organization info...
); @@ -241,7 +241,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { if (!orgTenant || !orgInfo) { return ( -
+

Unable to resolve your organization. Please try signing out and signing back in. @@ -252,8 +252,8 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { } return ( -

-
+
+
{user && ( <> From 86178a5e8b85e4528a3168d90a51f63006c8894c Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 20 Apr 2026 12:00:41 -0400 Subject: [PATCH 2/4] chore(apollo-vertex): new skill --- .claude/commands/github-manual-stacked-prs.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .claude/commands/github-manual-stacked-prs.md diff --git a/.claude/commands/github-manual-stacked-prs.md b/.claude/commands/github-manual-stacked-prs.md new file mode 100644 index 000000000..91fa11c8d --- /dev/null +++ b/.claude/commands/github-manual-stacked-prs.md @@ -0,0 +1,172 @@ +--- +name: github-manual-stacked-prs +description: Manage manual stacked pull requests on GitHub without Graphite or other stack tools. Use when the user wants one GitHub PR in review at a time, keeps later branches as draft or local only, and rebases each next branch onto main after the previous PR merges. +--- + +# GitHub Manual Stacked PRs + +Use this skill when the user wants a GitHub-only stacked PR workflow without stack automation. The rule is simple: build branches in order, keep only one PR ready for review, and rebase the next branch onto `main` after the previous PR merges. + +## Defaults + +- GitHub only. +- No Graphite, `gh stack`, `git-town`, or custom stacking tools unless the user asks. +- Only one PR should be non-draft and actively in review at a time. +- Later changes can stay as local branches or draft PRs, but they are not review targets yet. +- Before opening or marking the next PR ready, rebase that branch onto the latest `main`. +- **Keep each PR under 800–1000 lines of diff (insertions + deletions).** If a proposed slice exceeds this, suggest splitting further before creating branches. + +## Start Here + +Before proposing commands, confirm: + +- trunk branch, usually `main` +- intended merge order from first PR to last PR +- whether later branches already have draft PRs or only local branches +- whether force-push is allowed + +If the split is weak, suggest collapsing it before writing commands. If any slice exceeds 1000 LoC, suggest splitting it further before proceeding. + +## Branch Model + +For work `A -> B -> C`: + +- create branch `A` from `main` +- create branch `B` from `A` +- create branch `C` from `B` +- open PR `A` against `main` +- keep `B` and `C` as draft PRs or local branches until their turn + +If draft PRs are used early, base them on their parent branch so the diff stays clean: + +- PR `A`: base `main` +- draft PR `B`: base `A` +- draft PR `C`: base `B` + +When a parent merges, rebase the child branch onto `main`, change the draft PR base to `main`, verify the diff, then mark it ready. + +## Fresh Workflow + +1. Sync `main`. + +```bash +git checkout main +git pull --ff-only +``` + +2. Create the first branch and commit only the first slice. + +```bash +git checkout -b feat/a +# edit +git commit -m "feat(scope): change A" +``` + +3. Create each later branch from its direct parent. + +```bash +git checkout -b feat/b +# edit +git commit -m "feat(scope): change B" + +git checkout -b feat/c +# edit +git commit -m "feat(scope): change C" +``` + +4. Push the branches. + +```bash +git push -u origin feat/a +git push -u origin feat/b +git push -u origin feat/c +``` + +5. Open PR `A` against `main`. +6. Optionally open `B` and `C` as draft PRs against their parent branches. If you do, state clearly that they are placeholders and not ready for review yet. + +Use a note like this in every draft PR body: + +```md +Manual stack: +- depends on: #123 +- status: draft, do not review yet +- next step: rebase onto `main` after #123 merges +``` + +## Promotion Rule + +Only promote one PR at a time. + +When PR `A` merges: + +1. Update local `main`. +2. Rebase branch `B` onto `main`. +3. Force-push `B`. +4. If PR `B` already exists as draft, change its base to `main`. +5. Verify the diff contains only `B`. +6. Mark PR `B` ready for review. + +Example: + +```bash +git checkout main +git pull --ff-only + +git checkout feat/b +git rebase main +git push --force-with-lease +``` + +When PR `B` merges, do the same for `C`: + +```bash +git checkout main +git pull --ff-only + +git checkout feat/c +git rebase main +git push --force-with-lease +``` + +## Review Rules + +- Only one PR is reviewable at a time. +- Later PRs stay draft or unopened until their parent PR merges. +- Do not ask reviewers to reason about stacked diffs across multiple open review branches. +- Before marking a draft PR ready, always verify its base is `main` and its diff is clean. + +## Recovery Patterns + +- Draft PR shows parent commits after a merge: rebase the branch onto `main`, force-push, then set the base to `main`. +- Diff is still noisy after retargeting: close and reopen the draft PR instead of fighting bad history. +- Extra commit landed in the wrong branch: move it with `git cherry-pick` or clean it up with `git rebase -i`, then force-push. +- Conflict storm: rebase from the next branch to be promoted, not from the top of the old stack. + +## Response Shape + +When helping the user, produce: + +- the branch order +- the current active PR +- exact git commands in order +- any force-push step +- the GitHub action needed next, such as create draft, change base, or mark ready + +Default to this format: + +```md +Stack +- feat/a -> main, ready for review +- feat/b -> draft until feat/a merges +- feat/c -> draft until feat/b merges + +Commands +```bash +# commands here +``` + +GitHub +- Open `feat/a` against `main`. +- Keep `feat/b` as draft. +``` From 6e8ab1b8e980e9d9bb77ec40614414c0daa5e4a7 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Tue, 28 Apr 2026 12:06:52 -0400 Subject: [PATCH 3/4] feat(apollo-vertex): rename Autopilot to AI assistant in ui labels --- .../ai-chat/preview/page.tsx | 18 ++++---- .../ai-chat/components/ai-chat-message.tsx | 45 +++++++++---------- .../components/ai-chat-selection-menu.tsx | 4 +- .../templates/ai-chat/AiChatAgentHubMode.tsx | 2 +- .../ai-chat/AiChatConversationalAgentMode.tsx | 2 +- 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx index e1dad22f8..d99fc8760 100644 --- a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx +++ b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx @@ -135,7 +135,7 @@ export default function AiChatPreviewPage() { isLoading={false} onSendMessage={noop} onStop={noop} - title="Autopilot" + title="AI assistant" /> @@ -155,7 +155,7 @@ export default function AiChatPreviewPage() { isLoading={false} onSendMessage={noop} onStop={noop} - title="Autopilot" + title="AI assistant" emptyState={ @@ -227,7 +227,7 @@ export default function AiChatPreviewPage() { isLoading={false} onSendMessage={noop} onStop={noop} - title="Autopilot" + title="AI assistant" onEditMessage={noop} > {MOCK_MESSAGES_MARKDOWN.map((msg) => ( @@ -250,7 +250,7 @@ export default function AiChatPreviewPage() { isLoading onSendMessage={noop} onStop={noop} - title="Autopilot" + title="AI assistant" onEditMessage={noop} > {renderBasicMsg(MOCK_MESSAGES_BASIC[0])} @@ -279,7 +279,7 @@ export default function AiChatPreviewPage() { onStop={noop} onRetry={noop} onEditMessage={noop} - title="Autopilot" + title="AI assistant" error={ new Error( "Failed to connect to the AI service. Please check your network connection.", @@ -304,7 +304,7 @@ export default function AiChatPreviewPage() { onSendMessage={noop} onStop={noop} onEditMessage={noop} - title="Autopilot" + title="AI assistant" > {MOCK_MESSAGES_WITH_CHOICES.map((msg) => ( @@ -360,7 +360,7 @@ export default function AiChatPreviewPage() { isLoading={false} onSendMessage={noop} onStop={noop} - title="Autopilot" + title="AI assistant" showMessageActions onEditMessage={noop} onRegenerate={noop} diff --git a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx index 555d7a558..ca392374e 100644 --- a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx +++ b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx @@ -313,29 +313,28 @@ export function AiChatMessage({ )} - {config.showMessageActions && - isResponseFullyRevealed && ( - - config.onFeedback?.(message.id, type)), - } - : {})} - onRegenerate={onRegenerate ?? config.onRegenerate} - /> - - )} + {config.showMessageActions && isResponseFullyRevealed && ( + + config.onFeedback?.(message.id, type)), + } + : {})} + onRegenerate={onRegenerate ?? config.onRegenerate} + /> + + )}
diff --git a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-selection-menu.tsx b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-selection-menu.tsx index ae581625c..62083ca09 100644 --- a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-selection-menu.tsx +++ b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-selection-menu.tsx @@ -48,10 +48,10 @@ export function AiChatSelectionMenu({ top: y, transform: "translate(-50%, -100%) translateY(-8px)", }} - aria-label="Ask Autopilot about selected text" + aria-label="Ask AI assistant about selected text" >
From bb9eab6b465a629d07e961a19c41488d9c8b4646 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Tue, 28 Apr 2026 13:21:24 -0400 Subject: [PATCH 4/4] fix(apollo-vertex): fix multi-step choices UI and message context - AiChatSuggestions multi-step mode now matches AiChatFlow visually: numbered rows with dividers, bold question, pb-[76px] for input overlap - Multi-step choices rendered at bottom anchor (same as AiChatFlow), not inside a centered absolute overlay - Gate multi-step choices and blur on !isLoading && !isLatestResponseAnimating (consistent with single-step behaviour) - onSelect sends "Question: Answer" so the AI has full context per step Co-Authored-By: Claude Sonnet 4.6 --- .../components/ai-chat-suggestions.tsx | 117 +++++++++--------- 1 file changed, 56 insertions(+), 61 deletions(-) diff --git a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-suggestions.tsx b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-suggestions.tsx index 213f5e0b5..479237110 100644 --- a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-suggestions.tsx +++ b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-suggestions.tsx @@ -1,7 +1,7 @@ "use client"; -import { ChevronLeft, ChevronRight, Loader2, X } from "lucide-react"; import { motion } from "framer-motion"; +import { ChevronLeft, ChevronRight, X } from "lucide-react"; import type { ChoiceOption } from "../types"; const ENTRANCE_EASE = [0.22, 1, 0.36, 1] as const; @@ -61,51 +61,45 @@ export function AiChatSuggestions({ if (isMultiStep) { return ( - {/* Header */} -
-
- {isLoading ? ( -
-
- {canSkip && onSkip && ( + {/* Header: prompt + controls */} +
+

+ {prompt} +

+
+
+ + {totalSteps && ( + + {step} + {"/"} + {totalSteps} + + )} - )} +
{onDismiss && (
- {/* Prompt */} - {prompt && ( -

- {prompt} -

- )} - - {/* Options */} -
- {options.map((option) => ( - onSelect(option)} - > - {option.label} - + {/* Options with numbered circles and dividers */} +
+ {options.map((option, i) => ( +
+ {i > 0 && ( + ))}