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..a0dcdce4bb7b --- /dev/null +++ b/static/app/components/core/chat/assistantActions.tsx @@ -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 { + /** + * 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'; + const label = disabled + ? t('Feedback submitted') + : isPositive + ? t('I like this response') + : t("I don't like this response"); + 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/toolUse.spec.tsx b/static/app/views/seerExplorer/components/chat/toolUse.spec.tsx index cfa0e80547bd..0c9ac2656121 100644 --- a/static/app/views/seerExplorer/components/chat/toolUse.spec.tsx +++ b/static/app/views/seerExplorer/components/chat/toolUse.spec.tsx @@ -109,7 +109,7 @@ describe('ToolUseBlock', () => { it('does not render action bar', () => { render(); expect( - screen.queryByRole('button', {name: 'Feedback Thumbs Up'}) + screen.queryByRole('button', {name: 'I like this response'}) ).not.toBeInTheDocument(); }); }); diff --git a/static/app/views/seerExplorer/components/chat/user.spec.tsx b/static/app/views/seerExplorer/components/chat/user.spec.tsx index 7aede39f5cef..20a77167fea1 100644 --- a/static/app/views/seerExplorer/components/chat/user.spec.tsx +++ b/static/app/views/seerExplorer/components/chat/user.spec.tsx @@ -38,10 +38,10 @@ describe('UserBlock', () => { await userEvent.hover(container.firstChild as HTMLElement); expect( - screen.queryByRole('button', {name: 'Feedback Thumbs Up'}) + screen.queryByRole('button', {name: 'I like this response'}) ).not.toBeInTheDocument(); expect( - screen.queryByRole('button', {name: 'Copy block content'}) + screen.queryByRole('button', {name: 'Copy to clipboard'}) ).not.toBeInTheDocument(); }); }); 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 ?? ''} - + ); }