Skip to content

feat(apollo-vertex): ai-chat foundation — types, hooks, utils, icons [1/6]#544

Closed
petervachon wants to merge 1 commit intomainfrom
pr/1a-aichat-foundation
Closed

feat(apollo-vertex): ai-chat foundation — types, hooks, utils, icons [1/6]#544
petervachon wants to merge 1 commit intomainfrom
pr/1a-aichat-foundation

Conversation

@petervachon
Copy link
Copy Markdown
Collaborator

@petervachon petervachon commented Apr 20, 2026

Summary

  • Adds shared TypeScript types (types.ts) and utility functions (ai-chat-utils.ts)
  • Adds hooks: use-sticky-scroll, use-typewriter
  • Adds autopilot icons (autopilot.tsx, autopilot-gradient.tsx)
  • Updates registry.json with ai-chat registry entries
  • Adds @ai-sdk/react to package.json, lib/auth.ts, and next-env.d.ts

Stack

pr/1a-aichat-foundation  →  main          ← this PR, ready for review
pr/1b-aichat-display     →  1a            draft
pr/1c-aichat-input       →  1b            draft
pr/1d-aichat-messages    →  1c            draft
pr/1e-aichat-chat        →  1d            draft
pr/2-aichat-templates    →  1e            draft

Supersedes #542 / #511 — re-split to keep each PR under 800–1000 LoC.

Test plan

  • git checkout pr/1a-aichat-foundation && pnpm dev — dev server starts without errors
  • registry/ai-chat/types.ts, utils/, hooks/, and icons/ all present
  • registry.json contains ai-chat entries

🤖 Generated with Claude Code

@petervachon petervachon requested a review from a team as a code owner April 20, 2026 15:10
@petervachon petervachon requested review from 0xr3ngar and alincadariu and removed request for a team April 20, 2026 15:10
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (PT)
apollo-design 🟢 Ready Preview, Logs Apr 29, 2026, 01:20:10 PM
apollo-docs 🟢 Ready Preview, Logs Apr 29, 2026, 01:19:22 PM
apollo-landing 🟢 Ready Preview, Logs Apr 29, 2026, 01:17:14 PM
apollo-ui-react 🟢 Ready Preview, Logs Apr 29, 2026, 01:19:01 PM
apollo-vertex 🟢 Ready Preview, Logs Apr 29, 2026, 01:18:40 PM

@KokoMilev KokoMilev enabled auto-merge (rebase) April 20, 2026 15:10
@github-actions github-actions Bot added the size:XL 500-999 changed lines. label Apr 20, 2026
Comment on lines +8 to +11
export const AutopilotIcon = React.forwardRef<
SVGSVGElement,
AutopilotIconProps
>(({ size = 24, ...props }, ref) => (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use forwardRef? With React 19, you can finally treat ref like any other prop. No forwardRef

https://react.dev/reference/react/forwardRef

Comment on lines +16 to +19
export const AutopilotGradientIcon = React.forwardRef<
SVGSVGElement,
AutopilotGradientIconProps
>(({ size = 24, ...props }, ref) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use forwardRef? With React 19, you can finally treat ref like any other prop. No forwardRef

https://react.dev/reference/react/forwardRef

Comment on lines +6 to +15
const THINKING_LABELS = [
"Thinking…",
"Se gândește…",
"Thinking…",
"सोच रहा है…",
"Thinking…",
"Denkt nach…",
"Thinking…",
"Réflexion…",
];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be here, I think the user should provide these as props since these should be translated in the app ( via the useTranslate() hook )

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this thinking cycling ? I don't expect that the LLM will take more than 2 seconds to give a response. I feel like we are just copying claude code.

I think this is purely cosmetic (no actual information), it can feel twee or try-hard ("Réflexion…" 🙄), it adds state and timers to manage, and on slow devices the cycling can feel jank

We have to keep in mind that not all users have macbook pros and some are running on shitty thinkpads with 8 gb ram

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we replace this with a CSS pulse or shimmer on a single "Thinking…"? Same reassurance that the UI isn't frozen, zero state to manage, and it actually looks better on slow devices

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as the thinking-labels hook: do we actually need this?

The comment says ChatGPT/Claude throttle to 30-50 cps, but the reason they do it is to smooth out bursty token streams - not because throttling is inherently good. If our backend streams tokens at a reasonable speed, the browser handles pacing for free

My main concerns are

  • Fast readers end up waiting on the animation instead of the model (a real ChatGPT complaint)
  • We're re-rendering up to 60fps via rAF instead of per-token (maybe 10-30/sec). If the message contains markdown, that's re-parsing on every frame - not free on the 8GB ThinkPads
  • The "drain 2× when streaming ends" logic is a smell - it exists because the throttle falls behind reality, which means it was adding latency, not matching reading speed
  • useRef + useReducer to force re-renders is fighting React. If we keep any version of this, it should be useState for displayedLength - in short we need to refactor this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also why do we opt in for an in-house hook implementation when we can just use a package that does this? plenty of people have already solved this problem

Comment on lines +40 to +62
export function findLatestFlow(messages: UIMessage[]): ToolResultFlow | null {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (!msg || msg.role !== "assistant") continue;
const hasUserAfter = messages.slice(i + 1).some((m) => m.role === "user");
if (hasUserAfter) continue;

for (const part of msg.parts) {
if (part.type === "tool-result" && "content" in part) {
const result = tryParseFlow(part.content);
if (result) return result;
}
if (part.type === "tool-call" && "output" in part) {
const result = toolResultFlowSchema.safeParse(
(part as { output?: unknown }).output,
);
if (result.success) return result.data;
}
}
}
return null;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findLatestFlow scans backwards with hasUserAfter check

This is O(n²) because messages.slice(i + 1).some(...) re-scans for every iteration

For a long conversation this really matters.

Simpler approach could be to find the last user message index once, then only look at assistant messages after it - same structure as findActiveChoicesMessageIds already uses. These two functions are doing the same "find trailing assistant messages" work with different implementations.

Comment on lines +117 to +131
if (part.type === "tool-result" && "content" in part) {
try {
const parsed: unknown = JSON.parse(
(part as { content: string }).content,
);
if (
parsed !== null &&
typeof parsed === "object" &&
"type" in parsed &&
(parsed as { type: unknown }).type === "choices"
)
return true;
} catch {
// invalid JSON, skip
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should already be typed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The casts (part as { content: string }) and (part as { output?: unknown }) suggest the UIMessagePart type isn't discriminated properly. If type === "tool-result" doesn't narrow part to have content, that's a type definition problem upstream worth fixing

Comment on lines +134 to +145
if (part.type === "tool-call" && "output" in part) {
const output = (part as { output?: unknown }).output;
if (
output != null &&
typeof output === "object" &&
"type" in output &&
(output as { type: unknown }).type === "choices"
) {
return true;
}
}
return false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing as above, this is a type issue

Comment on lines +158 to +181
export function findActiveChoicesMessageIds(
messages: UIMessage[],
): Set<string> {
// Find the index of the most recent user message
let lastUserIdx = -1;
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg && msg.role === "user") {
lastUserIdx = i;
break;
}
}

// Trailing assistant messages — everything after the latest user message
const trailingAssistants = messages
.slice(lastUserIdx + 1)
.filter((m) => m.role === "assistant");

// Only suppress actions if at least one trailing assistant has choices
const hasActiveChoices = trailingAssistants.some((m) => messageHasChoices(m));
if (!hasActiveChoices) return new Set();

return new Set(trailingAssistants.map((m) => m.id));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this suppresses actions on all trailing assistant messages when any of them has choices. Is that what we want if an unrelated assistant message (status update, other tool result) happens to land after a choices message? It'd lose its copy/regenerate too.

If our turn structure guarantees that can't happen, worth a comment stating the invariant. Otherwise we probably want to scope the suppression more tightly

Also: this can be one backwards pass instead of three (scan -> slice/filter -> some -> map), and the name implies it finds the choices messages when it actually finds their whole turn - getActiveChoiceTurnMessageIds would be clearer.

Copy link
Copy Markdown
Collaborator

@0xr3ngar 0xr3ngar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also another thing, ci checks are all failing

@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from 6e13d73 to 47b05ed Compare April 21, 2026 13:44
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

Dependency License Review

  • 2104 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 15 package(s) excluded (see details below)
License distribution
License Packages
MIT 1825
ISC 104
Apache-2.0 69
BSD-3-Clause 30
BSD-2-Clause 24
Copyright 2022, UiPath, all rights reserved 9
BlueOak-1.0.0 8
MPL-2.0 5
MIT OR Apache-2.0 3
MIT-0 3
Unknown 3
Unlicense 3
CC0-1.0 3
LGPL-3.0-or-later 2
(MIT OR Apache-2.0) 2
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
BSD 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later LGPL pre-built binary, not linked
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later LGPL pre-built binary, not linked
@uipath/apollo-angular-elements 5.86.3 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/apollo-core 4.35.0, 4.35.1 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/apollo-fonts 1.25.8 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/apollo-icons 1.33.7 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/apollo-mui5 2.31.26 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/portal-shell 3.351.4 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/portal-shell-react 3.149.36 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/portal-shell-types 3.325.2 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/portal-shell-util 1.112.0 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/apollo-lab 25.12.0 Unknown UiPath first-party package
@uipath/telemetry-client-web 5.1.0 Unknown UiPath first-party package
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json
hyperx 2.5.4 BSD BSD-2-Clause per LICENSE file, non-SPDX "BSD" in package.json

@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from 47b05ed to a5af7e0 Compare April 21, 2026 14:11
@github-actions github-actions Bot added size:XXL 1,000+ changed lines. and removed size:XL 500-999 changed lines. labels Apr 21, 2026
@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from a5af7e0 to d578831 Compare April 21, 2026 14:19
@github-actions github-actions Bot added size:XL 500-999 changed lines. and removed size:XXL 1,000+ changed lines. labels Apr 21, 2026
@petervachon petervachon mentioned this pull request Apr 21, 2026
19 tasks
@alincadariu alincadariu requested a review from pieman1313 April 24, 2026 09:01

// ─── Flow (client-side multi-step) ───────────────────────────────────────────

const flowOptionSchema = z.object({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this entire file is gone on my pr where we refactor the choices tool to be more generic #584

I suggest we wait until mine is merged, and restart this approach because having just types being checked in with implementation on subsequent prs is hard to follow

we should strive to have end to end concerns / implementations in one pr

@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from d578831 to 7bc966d Compare April 28, 2026 17:21
@github-actions github-actions Bot added size:XXL 1,000+ changed lines. and removed size:XL 500-999 changed lines. labels Apr 28, 2026
@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from 7bc966d to c5ba891 Compare April 28, 2026 17:30
@github-actions github-actions Bot added size:XL 500-999 changed lines. and removed size:XXL 1,000+ changed lines. labels Apr 28, 2026
@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch 2 times, most recently from 10398bf to 4e7bebd Compare April 28, 2026 17:45
@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from 4e7bebd to 893e96c Compare April 28, 2026 18:08
@petervachon
Copy link
Copy Markdown
Collaborator Author

Hi @0xr3ngar and @pieman1313 — thanks for the detailed feedback. Here's a summary of what's been addressed since your reviews:

@0xr3ngar

  • use-typewriter.ts — removed entirely. Stream pacing from the backend handles the reveal naturally; no rAF loop, no re-parsing markdown on every frame.
  • use-thinking-label.ts — removed. AiChatLoading now uses a static "Thinking…" with a CSS shimmer (no state, no timers). We've left a TODO comment in the component outlining a latency-aware progression (500ms→2s→4s+) for when we want to build that out properly.
  • forwardRef on icons — the icons use the React 19 ref-as-prop pattern already; no forwardRef wrapper.
  • findLatestFlow O(n²) — fixed. One backwards pass to find lastUserIdx, then a single scan from the tail down to that index.
  • Type casts — removed. tool-result content is accessed directly; tool-call output is routed through unknown before being passed to safeParse.
  • getActiveChoiceTurnMessageIds naming — already using the clearer name you suggested.

@pieman1313

PR #584 has merged, so the conflict around ai-chat-utils.ts is no longer an issue. Our branches were rebased onto main after the merge.

Would appreciate a re-review when you get a chance!

Copy link
Copy Markdown
Contributor

@pieman1313 pieman1313 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at the initial pr, and considered it as the end state / north start towards which your current prs are headed.
I do not agree with the splitting of prs, and would prefer we try to have small, end to end features building one upon the other, instead of what we have now, where we took the end state pr and just broke it down to smaller prs that on their own don't give the full picture.

I poked Claude on the end state pr to break it down into complete end to end example prs, although I do doubt some of them due to the features themselves.

Here is an example breakdown of the big pr, although, I do not agree with some features, and would like to postpone them a bit until we get the bulk of the work in, and then we can focus on doing those features better.

  • We should not add any mock responses or the like. we have hooked up actual llms, and should showcase everything using them
  • we should postpone the flow step + choices feature until after we get the bulk of the features in

what do you think?

PR 1 — feat(apollo-vertex): ai-chat welcome screen + Autopilot rebrand

What the user gets: Open /patterns/ai-chat, see a centered "What are we
tackling today?" empty state with Autopilot branding and 3 starter
suggestion chips. Click a chip, it sends as a message.

Includes:

  • New ai-chat-empty-state component
  • emptyState and suggestions props on AiChat
  • --ai-chat-muted-foreground token (only what the empty state paints) added
    to registry.json
  • AgentHub wiring: title set to "Autopilot", emptyState passed in,
    suggestions array passed in
  • Preview page: scaffolds the ai-chat preview page with an "Empty state"
    section and a nav entry

PR 2 — feat(apollo-vertex): syntax-highlighted code blocks with copy

What the user gets: Ask Autopilot for a code snippet — the response renders
in a styled block with language label, syntax highlighting (light and dark
via github-dark-dimmed), and a copy button.

Includes:

  • New ai-chat-code-block component, with ref-based innerHTML safety folded
    in from the CI-fix commit
  • ai-chat-markdown rewires pre/code blocks to render AiChatCodeBlock
  • Preview page: adds a "Code blocks" section showing three languages and the
    dark variant

PR 3 — feat(apollo-vertex): multi-step interactive flows

What the user gets: Ask Autopilot something open-ended ("help me draft a
brief") and it presents a 2–8 step decision flow inline. User clicks
through, can go back or skip, then a single message with all answers is
sent. Suggestion chips also gain the same step counter / back / skip /
dismiss UI for the existing presentChoices tool.

Includes:

  • New ai-chat-flow component and flow-tool example
  • ai-chat-suggestions extended with step, totalSteps, canGoBack, canSkip and
    framer-motion entrance animations
  • choices-tool prompt updated to describe multi-step usage
  • findLatestFlow, tryParseFlow, and findActiveChoicesMessageIds added to
    ai-chat-utils, plus the nesting-depth refactor from the CI-fix commit
  • AgentHub wiring: register flowTool, append FLOW_TOOL_PROMPT to the system
    prompt
  • Preview page: adds an interactive "Flow" demo using the existing MOCK_FLOW

PR 4 — feat(apollo-vertex): animated thinking + assistant typewriter reveal

What the user gets: Send a message — see an animated sparkle morph into a
pulsing circle, with the "Thinking…" label cycling through translations.
When the response streams, text is throttled to ~75 characters/second for
readable reveal. Suggestion chips wait until the typewriter finishes before
appearing.

Includes:

  • New ai-chat-thinking component, use-thinking-label hook, use-typewriter
    hook
  • ai-chat-loading rewritten (shimmer plus label cycler)
  • Introduces ai-chat-provider and the AiChatConfig context, with only the
    fields this PR needs: typewriterCps, latestAssistantMessageId,
    isLatestResponseAnimating, and its setter. Future PRs extend it
  • AgentHub wiring: wrap the chat with AiChatProvider configured at 75 cps
  • Preview page: adds the ThinkingDemo as an isolated playground

PR 5 — feat(apollo-vertex): message actions + inline user edit

What the user gets: Hover any assistant message — get copy, thumbs up,
thumbs down, and regenerate buttons. Hover a user message — get an edit
pencil. Click edit, inline textarea with save and cancel; on save the
conversation rewinds and re-sends. Actions are suppressed during an active
choice turn, using the helper introduced in PR 3.

Includes:

  • New ai-chat-message-actions component
  • Edit mode added to ai-chat-message
  • AiChatConfig extended with showMessageActions, showCopyButton,
    activeChoicesMessageIds, onFeedback, onRegenerate, onEditMessage, plus the
    MessageFeedbackType type
  • AgentHub wiring: onRegenerate calls reload, onEditMessage calls sendMessage
  • Preview page: adds "Message actions" and "Edit mode" sections

PR 6 — feat(apollo-vertex): file attachments, paste image, source citations

What the user gets: Paste an image into the input — thumbnail appears as a
chip, click to expand in a lightbox. Click the paperclip — file picker for
any file type. Send — message bubble shows the attachment chips. Assistant
responses can render source citation chips below.

Includes:

  • ai-chat-input rewrite: PendingFile model, file picker, paste handler,
    thumbnail and dialog-based lightbox. Folds in the uid, instanceof
    narrowing, and void onClick fixes from the CI-fix commit
  • MessageSource and MessageAttachment types, with rendering in
    ai-chat-message
  • AgentHub wiring: onSubmit accepts (text, files?) and forwards files
  • Preview page: adds "Attachments" and "Sources" sections using
    MOCK_ATTACHMENTS_BASIC and MOCK_SOURCES_BASIC

PR 7 — feat(apollo-vertex): "Ask Autopilot" text selection menu

What the user gets: Highlight any text in an assistant message — a small
"Ask Autopilot" pill appears above the selection. Click it — the input gets
a quote chip with the highlighted text. Type a follow-up and send. Quote
chip is removable.

Includes:

  • New ai-chat-selection-menu component, autopilot icon, and autopilot
    gradient icon
  • Quote chip slot added to ai-chat-input
  • Selection capture wired into ai-chat-message
  • AiChatConfig extended with onQuoteSelect, plus a new enableTextSelection
    prop on AiChat
  • AgentHub wiring: enableTextSelection turned on, onQuoteSelect populates
    the input quote chip
  • Preview page: adds a "Selection menu" section with mock assistant text the
    user can highlight

@@ -0,0 +1,185 @@
import type { UIMessage } from "@tanstack/ai-client";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we moved away from this approach, I am doubting if its a good ideea to re-build on top of it. I would prefer we have a complete end to end working example to judge, rather than just some types.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I originally had that in there to use with the recommended prompts (as part of assistant response) as well as a multi-step interaction which presents in more of a card carousel of questions and answers.

I've removed those to clean things up, we can revisit later on.

…[1/5]

Adds shared types, utility functions, hooks (use-sticky-scroll,
use-thinking-label, use-typewriter), autopilot icons, registry.json
entries, and supporting config (package.json, lib/auth.ts, next-env.d.ts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@petervachon petervachon force-pushed the pr/1a-aichat-foundation branch from 893e96c to 3b8ce4f Compare April 29, 2026 20:15
@github-actions github-actions Bot added size:L 100-499 changed lines. and removed size:XL 500-999 changed lines. labels Apr 29, 2026
@petervachon
Copy link
Copy Markdown
Collaborator Author

This PR is part of the original ai-chat stack which has been superseded by a restructured version. Closing in favour of the new stack:

The new stack is reorganised by user-facing feature rather than component type, includes all the same functionality plus attachments and text selection, and has a clean single-commit-per-PR history.

auto-merge was automatically disabled April 30, 2026 13:52

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:apollo-vertex size:L 100-499 changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants