Skip to content

feat(apollo-vertex): ai-chat message actions — copy, feedback, edit, regenerate [3/5]#630

Open
petervachon wants to merge 2 commits into
mainfrom
feat/ai-chat-3-message-actions
Open

feat(apollo-vertex): ai-chat message actions — copy, feedback, edit, regenerate [3/5]#630
petervachon wants to merge 2 commits into
mainfrom
feat/ai-chat-3-message-actions

Conversation

@petervachon
Copy link
Copy Markdown
Collaborator

@petervachon petervachon commented Apr 30, 2026

What this does

Adds the per-message action layer — the controls that let users interact with individual messages rather than just reading them.

  • Copy message — copies the full text of any message to clipboard with a brief "Copied!" confirmation
  • Thumbs up / thumbs down on assistant messages — surfaces a onFeedback callback so the host app can record quality signals
  • Edit user message — tap the pencil on any user bubble to revise your message; the conversation rewinds to that point and re-sends, so the thread stays coherent
  • Regenerate — reruns the last AI response without resending the user message, useful when the answer is off
  • Actions appear on hover (desktop) or are always visible on touch, so they don't clutter the UI by default
  • Shared state lives in AiChatProvider context so all message components can read it without prop-drilling

Test plan

  • Hover a message — action bar appears
  • Copy copies the message text and shows "Copied!" briefly
  • Thumbs up/down fires onFeedback with the message ID and type
  • Edit pencil opens inline edit mode; submitting rewinds history and re-sends
  • Regenerate button (on latest assistant message) fires onRegenerate

🤖 Generated with Claude Code

@petervachon petervachon requested a review from a team as a code owner April 30, 2026 13:51
@petervachon petervachon requested review from 0xr3ngar and pieman1313 and removed request for a team April 30, 2026 13:51
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

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

Project Deployment Review Updated (PT)
apollo-design 🟢 Ready Preview, Logs May 11, 2026, 07:04:17 AM
apollo-docs 🟢 Ready Preview, Logs May 11, 2026, 07:02:35 AM
apollo-landing 🟢 Ready Preview, Logs May 11, 2026, 07:01:27 AM
apollo-ui-react 🟢 Ready Preview, Logs May 11, 2026, 07:03:37 AM
apollo-vertex 🟢 Ready Preview, Logs May 11, 2026, 07:02:57 AM

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Dependency License Review

  • 2105 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 15 package(s) excluded (see details below)
License distribution
License Packages
MIT 1826
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.89.0 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/apollo-core 4.35.0, 4.35.1, 4.35.2 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, 2.31.27 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.326.0 Copyright 2022, UiPath, all rights reserved UiPath first-party package
@uipath/portal-shell-util 1.114.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 feat/ai-chat-3-message-actions branch from cc1b25a to 1cd03f4 Compare April 30, 2026 17:36
@petervachon petervachon force-pushed the feat/ai-chat-2-code-blocks branch from e6653c8 to 7817feb Compare April 30, 2026 17:36
@petervachon petervachon force-pushed the feat/ai-chat-3-message-actions branch from 1cd03f4 to c39dd48 Compare May 1, 2026 13:11
@petervachon petervachon force-pushed the feat/ai-chat-2-code-blocks branch 2 times, most recently from 7b409a5 to 2c7e90f Compare May 1, 2026 13:22
@petervachon petervachon force-pushed the feat/ai-chat-3-message-actions branch 2 times, most recently from ccb80ab to cf9efc2 Compare May 1, 2026 13:42
@petervachon petervachon force-pushed the feat/ai-chat-2-code-blocks branch 2 times, most recently from d332c03 to 6a9cb61 Compare May 1, 2026 13:57
@petervachon petervachon force-pushed the feat/ai-chat-3-message-actions branch from cf9efc2 to b3871a0 Compare May 1, 2026 13:57
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.

@petervachon since other PRs mentioned here were closed, can we close this PR as well?

@petervachon
Copy link
Copy Markdown
Collaborator Author

Hey @0xr3ngar this is part of an active stacked PR series ([1/5] #628 → [2/5] #629[3/5] #630 → [4/5] #631 → [5/5] #632). Not ready to close — happy to keep it open for review!

@0xr3ngar
Copy link
Copy Markdown
Collaborator

0xr3ngar commented May 4, 2026

Hey @0xr3ngar this is part of an active stacked PR series ([1/5] #628 → [2/5] #629[3/5] #630 → [4/5] #631 → [5/5] #632). Not ready to close — happy to keep it open for review!

Ah okay since it is still active, please put this PR in draft and fix the merge conflicts of #629

@petervachon petervachon marked this pull request as draft May 4, 2026 14:23
@petervachon petervachon force-pushed the feat/ai-chat-3-message-actions branch from b3871a0 to fffc189 Compare May 4, 2026 14:34
@KokoMilev KokoMilev enabled auto-merge (rebase) May 6, 2026 10:07
@VKravchuk VKravchuk requested a review from 0xr3ngar May 6, 2026 10:11
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.

@pieman1313 hey Razvan I feel like this is going in the wrong direction, the component and data structure seem super off. Can you take a deeper look into this and let me know if I'm going crazy or something, because the component structure feels super off, and we have to patch things with effects or contexts that I do not think should be there.

Comment on lines +61 to +84
{showCopy && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
void handleCopy();
}}
className="size-7 inline-flex items-center justify-center rounded-md hover:bg-ai-chat-muted transition-colors"
aria-label={copyLabel}
>
{copied ? (
<Check className="size-3.5 text-success" aria-hidden="true" />
) : (
<Copy
className="size-3.5 text-ai-chat-muted-foreground"
aria-hidden="true"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent>{copyLabel}</TooltipContent>
</Tooltip>
)}
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 would extract this copy state handling into a seperate component. This way the state const [copied, setCopied] = useState(false); and the handler handleCopy don't live in this component.

Extract into a generic component

import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

type CopyToClipboardButtonProps = {
    copyValue: string;
};

export const CopyToClipboardButton = ({
    copyValue,
}: CopyToClipboardButtonProps) => {
    const [hasCopied, setHasCopied] = useState(false);

    const { t } = useTranslation();

    const onClickHandler = async () => {
        await navigator.clipboard
            .writeText(copyValue)
            .then(() => setHasCopied(true))
            .catch(() => setHasCopied(false));
    };

    useEffect(() => {
        let timeout: NodeJS.Timeout;
        if (hasCopied) {
            timeout = setTimeout(() => {
                setHasCopied(false);
            }, 1500);
        }
        return () => clearTimeout(timeout);
    }, [hasCopied]);

    return (
        <Button
            testId={TestId.copyToClipboard}
            size="small"
            variant="text"
            color={hasCopied ? 'success' : 'primary'}
            onClick={async () => {
                await onClickHandler();
            }}
            startIcon={hasCopied ? <CheckIcon /> : <FileCopyIcon />}
        >
            {hasCopied ? t('copied') : t('copy')}
        </Button>
    );
};

{showCopy && (
<Tooltip>
<TooltipTrigger asChild>
<button
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 are we using and not the component from the lib?

Comment on lines +105 to +160
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onFeedback("down")}
className="size-7 inline-flex items-center justify-center rounded-md hover:bg-ai-chat-muted transition-colors"
aria-label={t("bad_response")}
>
<ThumbsDown
className="size-3.5 text-ai-chat-muted-foreground"
aria-hidden="true"
/>
</button>
</TooltipTrigger>
<TooltipContent>{t("bad_response")}</TooltipContent>
</Tooltip>
</>
)}

{messageRole === "assistant" && onRegenerate && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onRegenerate}
className="size-7 inline-flex items-center justify-center rounded-md hover:bg-ai-chat-muted transition-colors"
aria-label={t("try_again")}
>
<RefreshCw
className="size-3.5 text-ai-chat-muted-foreground"
aria-hidden="true"
/>
</button>
</TooltipTrigger>
<TooltipContent>{t("try_again")}</TooltipContent>
</Tooltip>
)}

{messageRole === "user" && onEdit && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onEdit}
className="size-7 inline-flex items-center justify-center rounded-md hover:bg-ai-chat-muted transition-colors"
aria-label={t("edit")}
>
<Pencil
className="size-3.5 text-ai-chat-muted-foreground"
aria-hidden="true"
/>
</button>
</TooltipTrigger>
<TooltipContent>{t("edit")}</TooltipContent>
</Tooltip>
)}
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.

all these Buttons are literally the same with the only difference being the handler + icon + text. Extract into a shared Component and just pass the handler + text + icon

@@ -0,0 +1,28 @@
import type React from "react";

export type MessageFeedbackType = "up" | "down";
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 would call it positive | negative

Comment on lines +14 to +19
/** Show hover action toolbar on messages */
showMessageActions: boolean;
/** Show copy button in message actions */
showCopyButton: boolean;
/** Whether the chat is currently loading */
isLoading: boolean;
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 are these in the AiChatConfig type? these booleans make no sense to me. Why would a config have the isLoading boolean? Also the showCopyButton is sus? shouldn't these always be shown, same thing goes for showMessageActions

>
{t("cancel")}
</button>
<button
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.

use component not raw

transition={ENTRANCE_TRANSITION}
>
<div className="flex flex-col items-end gap-2 w-[80%]">
<textarea
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.

dont we also jhave a textarea component in the registry?

Comment on lines +79 to +85
// Streaming state — explicit prop wins, otherwise derive from chat-level isLoading
// and whether this is the latest assistant message currently being generated.
const isStreaming =
isStreamingProp ??
(config.isLoading &&
!isUser &&
config.latestAssistantMessageId === message.id);
Copy link
Copy Markdown
Collaborator

@0xr3ngar 0xr3ngar May 6, 2026

Choose a reason for hiding this comment

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

this is super sus, this feels like a code smell. Why is the isStreaming based on the loading and if there is no user and if the last msg id is the current message id

Comment on lines +59 to +70
// Auto-focus, select all, and scroll into view when entering edit mode.
// rAF defers the scroll until after React has committed the new layout.
useEffect(() => {
if (isEditing && editTextareaRef.current) {
editTextareaRef.current.focus();
editTextareaRef.current.select();
const el = editTextareaRef.current;
requestAnimationFrame(() => {
el.scrollIntoView({ behavior: "smooth", block: "center" });
});
}
}, [isEditing]);
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.

what is this? cant we create an authofocus ref and attach it to the button or textarea?

const autoFocus = (container: HTMLDivElement | null) => {
if (!container) return;
const focusable = container.querySelector(FOCUSABLE_SELECTOR);
focusable?.focus();
};

export const useAutoFocusFirst = () => {
return autoFocus;
};

const ref = useAutofucusFirst();

...

Comment on lines +54 to +57
// Keep editValue in sync if message content changes externally (e.g. regenerate)
useEffect(() => {
if (!isEditing) setEditValue(displayContent);
}, [displayContent, isEditing]);
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 is also super weird side effect that I think should never happen

@pieman1313
Copy link
Copy Markdown
Contributor

@0xr3ngar valid callouts, @VKravchuk can you have a look? not sure about the provider altogether tbh

Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat.tsx Outdated
Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat.tsx Outdated
Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat.tsx Outdated
Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx Outdated
Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx Outdated
@VKravchuk VKravchuk requested review from 0xr3ngar and pieman1313 May 7, 2026 13:23
@pieman1313 pieman1313 force-pushed the feat/ai-chat-3-message-actions branch 8 times, most recently from ac1db1f to 8d64feb Compare May 11, 2026 07:12
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.

OK I started reviewing the code again but I see smells everywhere :panic:

rn the data flow feels pretty shady. We pass messages into <AiChat />, but then the consumer also maps over messages and renders <AiChatMessage /> manually. On top of that, getAiChatMessageProps re-derives streaming/latest/action state, AiChatMessage re-derives display text/content visibility, and the template owns tool rendering as children

So the same source of truth is being interpreted in a bunch of different places

  1. AiChat handles empty state, loading, header, copy conversation, scroll, etc..
  2. getAiChatMessageProps handles latest message/streaming/action visibility
  3. AiChatMessage handles display text and whether the message is renderable
  4. the template handles message mapping and tool rendering

that feels like a pretty big code smell, we’ve kind of split ownership in the worst middle-ground way- AiChat both owns and doesn’t own messages at the same time :/

It also creates a bunch of perf smells. During streaming, we’re constantly re-rendering/recomputing across multiple layers: the template maps every message again, tool parts are checked/rendered again, message props are re-derived, display text is rebuilt, and AiChat also walks messages for shell-level state like copy conversation/loading

I don’t think the current pattern/architecture is healthy at all. I think we need to go back to the drawing board and create a better architecture where message state is derived in one place instead of being re-interpreted and re-computed constantly across multiple components

@pieman1313

Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx Outdated
Comment thread apps/apollo-vertex/registry/ai-chat/components/ai-chat-message.tsx Outdated
@pieman1313 pieman1313 force-pushed the feat/ai-chat-3-message-actions branch 2 times, most recently from 882f789 to 67f3652 Compare May 11, 2026 10:16
@github-actions github-actions Bot added size:XXL 1,000+ changed lines. and removed size:XL 500-999 changed lines. labels May 11, 2026
@pieman1313 pieman1313 force-pushed the feat/ai-chat-3-message-actions branch 2 times, most recently from 60ffe60 to 8ddf50c Compare May 11, 2026 11:04
@pieman1313 pieman1313 requested a review from 0xr3ngar May 11, 2026 11:08
@pieman1313 pieman1313 force-pushed the feat/ai-chat-3-message-actions branch from 8ddf50c to 9f766ba Compare May 11, 2026 13:32
@pieman1313 pieman1313 force-pushed the feat/ai-chat-3-message-actions branch from 9f766ba to 87429af Compare May 11, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:apollo-vertex size:XXL 1,000+ changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants