From 63d50feafe11d452dfbf4b9278d4631419641150 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 21 Jul 2026 08:02:25 +0200 Subject: [PATCH 1/4] ref(scraps): Add chat primitives and adopt in Seer Explorer Add presentation-only chat primitives to @sentry/scraps/chat (MessageRow, UserBubble, AssistantMessage, AssistantActions, ToolCallIndicator, Spinner) and adopt them in Seer Explorer. MessageRow positions a turn by role via a from prop; AssistantActions renders thumbs up/down and a copy button driven by flat copyText/onCopy. App-specific logic (markdown, analytics, feedback persistence) stays in the view. Each primitive ships with a story. --- .../components/core/chat/assistantActions.mdx | 69 ++++++++++++ .../components/core/chat/assistantActions.tsx | 105 ++++++++++++++++++ .../components/core/chat/assistantMessage.mdx | 51 +++++++++ .../components/core/chat/assistantMessage.tsx | 22 ++++ static/app/components/core/chat/index.tsx | 3 + .../app/components/core/chat/messageRow.mdx | 43 +++++++ .../app/components/core/chat/messageRow.tsx | 31 ++++++ .../components/chat/assistant.tsx | 82 +++----------- .../seerExplorer/components/chat/shared.tsx | 28 ++--- .../seerExplorer/components/chat/user.tsx | 7 +- 10 files changed, 355 insertions(+), 86 deletions(-) create mode 100644 static/app/components/core/chat/assistantActions.mdx create mode 100644 static/app/components/core/chat/assistantActions.tsx create mode 100644 static/app/components/core/chat/assistantMessage.mdx create mode 100644 static/app/components/core/chat/assistantMessage.tsx create mode 100644 static/app/components/core/chat/messageRow.mdx create mode 100644 static/app/components/core/chat/messageRow.tsx diff --git a/static/app/components/core/chat/assistantActions.mdx b/static/app/components/core/chat/assistantActions.mdx new file mode 100644 index 000000000000..ff37c001aa96 --- /dev/null +++ b/static/app/components/core/chat/assistantActions.mdx @@ -0,0 +1,69 @@ +--- +title: AssistantActions +description: Thumbs up/down feedback and a copy button for an agent's response. +category: chat +source: '@sentry/scraps/chat' +resources: + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/assistantActions.tsx +--- + +import {AssistantActions} from '@sentry/scraps/chat'; + +import * as Storybook from 'sentry/stories'; + +export const documentation = import('!!type-loader!@sentry/scraps/chat/assistantActions'); + +The `AssistantActions` renders the action bar for an agent's response: thumbs +up/down feedback and a copy button. + +It is presentation only. It reports votes through `onFeedback` and copies +through `onCopy`, and holds no submitted state — the caller decides what a vote +means, what to copy, and owns any analytics or persistence. Placement and +hover-reveal are the caller's responsibility via the forwarded `ButtonBar` +props. + +## With copy button + +Pass `copyText` to show the copy button alongside feedback. + + + {}} copyText="The agent response." /> + + +```jsx + recordFeedback(feedback)} + copyText={response} + onCopy={() => trackCopy()} +/> +``` + +## Without copy button + +Omit `copyText` (or leave it empty) to render feedback only. + + + {}} /> + + +```jsx + +``` + +## Disabled + +Set `feedbackDisabled` once a vote has been recorded to lock both feedback +buttons and switch their tooltip to a submitted state. Hover a thumb to see the +"Feedback submitted" tooltip. + + + {}} + feedbackDisabled + copyText="The agent response." + /> + + +```jsx + +``` diff --git a/static/app/components/core/chat/assistantActions.tsx b/static/app/components/core/chat/assistantActions.tsx new file mode 100644 index 000000000000..c4f1dbe502f0 --- /dev/null +++ b/static/app/components/core/chat/assistantActions.tsx @@ -0,0 +1,105 @@ +import {Button, ButtonBar, type ButtonBarProps} from '@sentry/scraps/button'; + +import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton'; +import {IconThumb} from 'sentry/icons'; +import {t} from 'sentry/locale'; + +type AssistantFeedback = 'positive' | 'negative'; + +interface AssistantActionsProps extends Omit { + /** + * Text copied by the copy button. When omitted or empty, the copy button is + * hidden. + */ + copyText?: string; + /** + * Locks the feedback buttons — e.g. once a vote has been recorded — and + * switches their tooltip to a submitted state. + */ + feedbackDisabled?: boolean; + /** + * Fired after the response is copied. Analytics are the caller's + * responsibility. + */ + onCopy?: (copiedText: string) => void; + /** + * Fired when the user votes on the response. The meaning of a vote and any + * analytics/persistence are the caller's responsibility. + */ + onFeedback?: (feedback: AssistantFeedback) => void; +} + +/** + * The action bar for an assistant's response: thumbs up/down feedback and a + * copy button. + * + * Presentation only — it renders the controls and reports votes through + * `onFeedback` and copies through `onCopy`. It holds no submitted state and + * fires no analytics; the caller owns what a vote means and what to copy. + * Placement (and any hover-reveal) is the caller's responsibility via the + * forwarded `ButtonBar` props. + */ +export function AssistantActions({ + onFeedback, + feedbackDisabled, + copyText, + onCopy, + size = 'xs', + ...props +}: AssistantActionsProps) { + return ( + + + + {copyText ? ( + { + e.stopPropagation(); + }} + /> + ) : null} + + ); +} + +function FeedbackButton({ + feedback, + disabled, + onClick, +}: { + feedback: AssistantFeedback; + disabled?: boolean; + onClick?: (feedback: AssistantFeedback) => void; +}) { + const isPositive = feedback === 'positive'; + return ( + + /> ); } diff --git a/static/app/views/seerExplorer/components/chat/shared.tsx b/static/app/views/seerExplorer/components/chat/shared.tsx index a3efe3dcd423..19e3787719d9 100644 --- a/static/app/views/seerExplorer/components/chat/shared.tsx +++ b/static/app/views/seerExplorer/components/chat/shared.tsx @@ -1,4 +1,4 @@ -import {Spinner, type ToolCallStatus} from '@sentry/scraps/chat'; +import {MessageRow, Spinner, type ToolCallStatus} from '@sentry/scraps/chat'; import {Flex} from '@sentry/scraps/layout'; import {SeerMarkdown} from 'sentry/components/seer/markdown'; @@ -66,19 +66,21 @@ export function hasValidContent(content: string | null | undefined): content is export function MessagePlaceholder({content}: {content?: string}) { return ( - - - + + + + + + {hasValidContent(content) && } - {hasValidContent(content) && } - + ); } diff --git a/static/app/views/seerExplorer/components/chat/user.tsx b/static/app/views/seerExplorer/components/chat/user.tsx index 2940d8278929..7c80cf462094 100644 --- a/static/app/views/seerExplorer/components/chat/user.tsx +++ b/static/app/views/seerExplorer/components/chat/user.tsx @@ -1,12 +1,11 @@ -import {UserBubble} from '@sentry/scraps/chat'; -import {Flex} from '@sentry/scraps/layout'; +import {MessageRow, UserBubble} from '@sentry/scraps/chat'; import type {UserBlockProps} from './shared'; export function UserBlock({block}: UserBlockProps) { return ( - + {block.message.content ?? ''} - + ); } From 39eebbf46ec499340d540bb1d1af873e2101ac06 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 21 Jul 2026 09:16:01 +0200 Subject: [PATCH 2/4] ref(scraps): Let MessageRow own the turn gutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AssistantMessage also applied padding=xl, so wrapping it in MessageRow double-padded the turn — which is why Seer Explorer omitted the row around assistant responses. Drop padding from AssistantMessage (like UserBubble, it now styles only its content) and wrap it in MessageRow in the view, so user and assistant turns share one consistent gutter. --- .../components/core/chat/assistantMessage.tsx | 7 +++---- .../seerExplorer/components/chat/assistant.tsx | 18 +++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/static/app/components/core/chat/assistantMessage.tsx b/static/app/components/core/chat/assistantMessage.tsx index 547f5c2eb8f2..8a6c62842374 100644 --- a/static/app/components/core/chat/assistantMessage.tsx +++ b/static/app/components/core/chat/assistantMessage.tsx @@ -9,13 +9,12 @@ interface AssistantMessageProps extends React.HTMLAttributes { * * Unlike the sender's messages (see `UserBubble`), an agent response is not * bubbled — it renders as full-width left-aligned content so rich output - * (markdown, tables, code) reads naturally. This is presentation only: it owns - * the turn's gutter and keeps wide content from forcing the row wider; the - * rendered content is the caller's responsibility. + * (markdown, tables, code) reads naturally. It keeps wide content from forcing + * the row wider; the turn's gutter is owned by the wrapping `MessageRow`. */ export function AssistantMessage({children, ...props}: AssistantMessageProps) { return ( - + {children} ); diff --git a/static/app/views/seerExplorer/components/chat/assistant.tsx b/static/app/views/seerExplorer/components/chat/assistant.tsx index c1c668e6588a..9f3a0dde3a83 100644 --- a/static/app/views/seerExplorer/components/chat/assistant.tsx +++ b/static/app/views/seerExplorer/components/chat/assistant.tsx @@ -1,7 +1,7 @@ import {Fragment} from 'react'; import {css} from '@emotion/react'; -import {AssistantActions, AssistantMessage} from '@sentry/scraps/chat'; +import {AssistantActions, AssistantMessage, MessageRow} from '@sentry/scraps/chat'; import {SeerMarkdown} from 'sentry/components/seer/markdown'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -28,9 +28,11 @@ export function AssistantBlock({ if (block.loading) { if (isStreamingEnabled && hasValidContent(content)) { return ( - - - + + + + + ); } return ; @@ -39,9 +41,11 @@ export function AssistantBlock({ return ( {hasValidContent(content) && ( - - - + + + + + )} Date: Tue, 21 Jul 2026 09:22:33 +0200 Subject: [PATCH 3/4] ref(scraps): Make AssistantMessage fill the turn width As a flex item inside MessageRow, AssistantMessage shrink-wrapped to its content instead of filling the row, so min-width:100% content (markdown tables, code blocks) measured against the shrunk width. Add width=100% so it fills the turn as documented. --- static/app/components/core/chat/assistantMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/core/chat/assistantMessage.tsx b/static/app/components/core/chat/assistantMessage.tsx index 8a6c62842374..c8cba5b26a8c 100644 --- a/static/app/components/core/chat/assistantMessage.tsx +++ b/static/app/components/core/chat/assistantMessage.tsx @@ -14,7 +14,7 @@ interface AssistantMessageProps extends React.HTMLAttributes { */ export function AssistantMessage({children, ...props}: AssistantMessageProps) { return ( - + {children} ); From 8e9555cc31a74951801fabe8ec18e0ac3d2a9d45 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 22 Jul 2026 08:45:32 +0200 Subject: [PATCH 4/4] fix(chat): align AssistantActions aria-labels with tooltip text The feedback and copy buttons in the shared AssistantActions primitive had aria-labels that disagreed with their visible tooltips, and the copy button carried Seer Explorer-specific wording ("Copy block content") that is misleading on other chat surfaces. Derive each feedback button's aria-label from the same string as its tooltip title, and match the copy button's aria-label to its "Copy to clipboard" tooltip. Update the seerExplorer chat specs that queried by the old accessible names. --- .../components/core/chat/assistantActions.tsx | 17 ++++++------ .../components/chat/assistant.spec.tsx | 26 +++++++++---------- .../components/chat/toolUse.spec.tsx | 2 +- .../components/chat/user.spec.tsx | 4 +-- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/static/app/components/core/chat/assistantActions.tsx b/static/app/components/core/chat/assistantActions.tsx index c4f1dbe502f0..a0dcdce4bb7b 100644 --- a/static/app/components/core/chat/assistantActions.tsx +++ b/static/app/components/core/chat/assistantActions.tsx @@ -61,7 +61,7 @@ export function AssistantActions({ /> {copyText ? ( void; }) { const isPositive = feedback === 'positive'; + const label = disabled + ? t('Feedback submitted') + : isPositive + ? t('I like this response') + : t("I don't like this response"); return (