-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
ref(scraps): Add chat primitives (MessageRow, AssistantMessage, MessageActions) #120052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
63d50fe
39eebbf
1de4c35
e414d0e
8e9555c
9f645ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
||
| <Storybook.Demo> | ||
| <AssistantActions onFeedback={() => {}} copyText="The agent response." /> | ||
| </Storybook.Demo> | ||
|
|
||
| ```jsx | ||
| <AssistantActions | ||
| onFeedback={feedback => recordFeedback(feedback)} | ||
| copyText={response} | ||
| onCopy={() => trackCopy()} | ||
| /> | ||
| ``` | ||
|
|
||
| ## Without copy button | ||
|
|
||
| Omit `copyText` (or leave it empty) to render feedback only. | ||
|
|
||
| <Storybook.Demo> | ||
| <AssistantActions onFeedback={() => {}} /> | ||
| </Storybook.Demo> | ||
|
|
||
| ```jsx | ||
| <AssistantActions onFeedback={recordFeedback} /> | ||
| ``` | ||
|
|
||
| ## 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. | ||
|
|
||
| <Storybook.Demo> | ||
| <AssistantActions | ||
| onFeedback={() => {}} | ||
| feedbackDisabled | ||
| copyText="The agent response." | ||
| /> | ||
| </Storybook.Demo> | ||
|
|
||
| ```jsx | ||
| <AssistantActions onFeedback={recordFeedback} feedbackDisabled copyText={response} /> | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| 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<ButtonBarProps, 'children' | 'onCopy'> { | ||
| /** | ||
| * 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 ( | ||
| <ButtonBar size={size} {...props}> | ||
| <FeedbackButton | ||
| feedback="positive" | ||
| disabled={feedbackDisabled} | ||
| onClick={onFeedback} | ||
| /> | ||
| <FeedbackButton | ||
| feedback="negative" | ||
| disabled={feedbackDisabled} | ||
| onClick={onFeedback} | ||
| /> | ||
| {copyText ? ( | ||
| <CopyToClipboardButton | ||
| aria-label={t('Copy to clipboard')} | ||
| text={copyText} | ||
| tooltipProps={{title: t('Copy to clipboard')}} | ||
| onCopy={onCopy} | ||
| onClick={e => { | ||
| e.stopPropagation(); | ||
| }} | ||
| /> | ||
| ) : null} | ||
| </ButtonBar> | ||
| ); | ||
| } | ||
|
|
||
| function FeedbackButton({ | ||
| feedback, | ||
| disabled, | ||
| onClick, | ||
| }: { | ||
| feedback: AssistantFeedback; | ||
| disabled?: boolean; | ||
| onClick?: (feedback: AssistantFeedback) => 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 ( | ||
| <Button | ||
| aria-label={label} | ||
| icon={<IconThumb direction={isPositive ? 'up' : 'down'} />} | ||
| disabled={disabled} | ||
| tooltipProps={{title: label}} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate disabled feedback labelsLow Severity When Reviewed by Cursor Bugbot for commit 9f645ad. Configure here. |
||
| onClick={e => { | ||
| e.stopPropagation(); | ||
| onClick?.(feedback); | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| --- | ||
| title: AssistantMessage | ||
| description: The content region for an agent's response in a conversation. | ||
| category: chat | ||
| source: '@sentry/scraps/chat' | ||
| resources: | ||
| js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/assistantMessage.tsx | ||
| --- | ||
|
|
||
| import {AssistantMessage, MessageRow, UserBubble} from '@sentry/scraps/chat'; | ||
| import {Stack} from '@sentry/scraps/layout'; | ||
|
|
||
| import * as Storybook from 'sentry/stories'; | ||
|
|
||
| export const documentation = import('!!type-loader!@sentry/scraps/chat/assistantMessage'); | ||
|
|
||
| The `AssistantMessage` renders the content region for an agent's response. | ||
| Unlike the sender's own messages — which are bubbled with `UserBubble` — an | ||
| agent response is not bubbled: it renders as full-width, left-aligned content so | ||
| rich output like markdown, tables, and code reads naturally. | ||
|
|
||
| ## Usage | ||
|
|
||
| Shown here in a conversation for context; on its own it is just the agent's | ||
| content region. | ||
|
|
||
| <Storybook.Demo align="stretch"> | ||
| <Stack width="100%"> | ||
| <MessageRow from="user"> | ||
| <UserBubble>How many errors happened in the last 24 hours?</UserBubble> | ||
| </MessageRow> | ||
| <MessageRow from="assistant"> | ||
| <AssistantMessage> | ||
| There were 1,204 errors in the last 24 hours — a 32% jump from the previous day, | ||
| mostly TypeErrors from the checkout flow. The spike began around 2pm UTC and has | ||
| been trending down since the latest deploy. | ||
| </AssistantMessage> | ||
| </MessageRow> | ||
| </Stack> | ||
| </Storybook.Demo> | ||
|
|
||
| ```jsx | ||
| <AssistantMessage> | ||
| There were 1,204 errors in the last 24 hours — a 32% jump from the previous day, mostly | ||
| TypeErrors from the checkout flow. The spike began around 2pm UTC and has been trending | ||
| down since the latest deploy. | ||
| </AssistantMessage> | ||
| ``` | ||
|
|
||
| Wide block content (tables, code) is kept from forcing the row wider, so it | ||
| scrolls within the region instead of overflowing the conversation. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import {Container} from '@sentry/scraps/layout'; | ||
|
|
||
| interface AssistantMessageProps extends React.HTMLAttributes<HTMLDivElement> { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| /** | ||
| * The content region for an agent's response in a conversation. | ||
| * | ||
| * 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. 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 ( | ||
| <Container width="100%" minWidth={0} overflow="hidden" {...props}> | ||
| {children} | ||
| </Container> | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| ); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| export {AssistantMessage} from './assistantMessage'; | ||
| export {AssistantActions} from './assistantActions'; | ||
| export {UserBubble} from './userBubble'; | ||
| export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; | ||
| export {Spinner} from './spinner'; | ||
| export {MessageRow} from './messageRow'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| --- | ||
| title: MessageRow | ||
| description: The full-width row that positions a single message turn in a conversation. | ||
| category: chat | ||
| source: '@sentry/scraps/chat' | ||
| resources: | ||
| js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/messageRow.tsx | ||
| --- | ||
|
|
||
| import {MessageRow, UserBubble} from '@sentry/scraps/chat'; | ||
| import {Container} from '@sentry/scraps/layout'; | ||
|
|
||
| import * as Storybook from 'sentry/stories'; | ||
|
|
||
| export const documentation = import('!!type-loader!@sentry/scraps/chat/messageRow'); | ||
|
|
||
| The `MessageRow` lays out a single message turn within a conversation. It is | ||
| presentation only — it owns alignment and the consistent gutter around a turn, | ||
| while the bubble or content is the caller's responsibility. | ||
|
|
||
| ## Alignment | ||
|
|
||
| Set `from="user"` for the sender's own messages and `from="assistant"` for the | ||
| agent's responses, so the two sides sit opposite each other. The row owns the | ||
| role-to-side mapping — callers express whose turn it is, not the layout. | ||
|
|
||
| <Storybook.Demo align="stretch"> | ||
| <Container width="100%" background="secondary" radius="md"> | ||
| <MessageRow from="user"> | ||
| <UserBubble>Which of my issues are getting worse?</UserBubble> | ||
| </MessageRow> | ||
| <MessageRow from="assistant"> | ||
| Here are the three escalating issues I found. | ||
| </MessageRow> | ||
| </Container> | ||
| </Storybook.Demo> | ||
|
|
||
| ```jsx | ||
| <MessageRow from="user"> | ||
| <UserBubble>Which of my issues are getting worse?</UserBubble> | ||
| </MessageRow> | ||
| <MessageRow from="assistant">Here are the three escalating issues I found.</MessageRow> | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import {Flex} from '@sentry/scraps/layout'; | ||
|
|
||
| interface MessageRowProps extends React.HTMLAttributes<HTMLDivElement> { | ||
| children: React.ReactNode; | ||
| /** | ||
| * Whose turn this is: `user` sits on the right, `assistant` on the left. The | ||
| * row owns the role-to-side mapping so callers express intent, not layout. | ||
| */ | ||
| from: 'user' | 'assistant'; | ||
| } | ||
|
|
||
| /** | ||
| * The full-width row that positions a single message turn within a conversation. | ||
| * | ||
| * Presentation only — it owns the row's alignment and the consistent gutter | ||
| * around a turn. The bubble or content is the caller's responsibility; lay out | ||
| * multi-part content in your own wrapper. | ||
| */ | ||
| export function MessageRow({children, from, ...props}: MessageRowProps) { | ||
| return ( | ||
| <Flex | ||
| align="start" | ||
| justify={from === 'user' ? 'end' : 'start'} | ||
| width="100%" | ||
| padding="xl" | ||
| {...props} | ||
| > | ||
| {children} | ||
| </Flex> | ||
| ); | ||
| } |


Uh oh!
There was an error while loading. Please reload this page.