Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ function ConversationView({
value={activeTab}
onChange={key => handleTabChange(key as ConversationTab)}
>
<Container padding="xs lg">
<Container paddingTop="lg" borderBottom="primary">
<TabList>
<TabList.Item key="messages">{t('Messages')}</TabList.Item>
<TabList.Item key="trace">{t('AI Spans')}</TabList.Item>
Expand All @@ -271,7 +271,7 @@ function ConversationView({
/>
</TabPanels.Item>
<TabPanels.Item key="trace">
<Container padding="md lg">
<Container padding="md lg md lg">
<AISpanList
nodes={nodes}
selectedNodeKey={selectedNode?.id ?? nodes[0]?.id ?? ''}
Expand Down Expand Up @@ -324,6 +324,7 @@ const StyledTabs = styled(Tabs)`

const FullWidthTabPanels = styled(TabPanels)`
width: 100%;
padding: 0;

> [role='tabpanel'] {
width: 100%;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import {useMemo} from 'react';
import {css, useTheme} from '@emotion/react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';

import {Flex} from '@sentry/scraps/layout';
import {Link} from '@sentry/scraps/link';
import {Text} from '@sentry/scraps/text';

import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
import Count from 'sentry/components/count';
import usePageFilters from 'sentry/components/pageFilters/usePageFilters';
import Placeholder from 'sentry/components/placeholder';
import {IconChat, IconFire, IconFix} from 'sentry/icons';
import {t} from 'sentry/locale';
import useOrganization from 'sentry/utils/useOrganization';
import {getExploreUrl} from 'sentry/views/explore/utils';
Expand Down Expand Up @@ -78,9 +76,7 @@ export function ConversationSummary({
}: ConversationSummaryProps) {
const organization = useOrganization();
const {selection} = usePageFilters();
const theme = useTheme();
const aggregates = useMemo(() => calculateAggregates(nodes), [nodes]);
const colors = [...theme.chart.getColorPalette(5), theme.colors.red400];

const baseQuery = `gen_ai.conversation.id:${conversationId}`;

Expand All @@ -105,40 +101,28 @@ export function ConversationSummary({
return (
<Flex align="center" gap="lg" flex={1}>
<Flex align="center" gap="sm" flexShrink={0}>
<Text size="lg" bold>
<Text size="sm" bold>
{t('Conversation')}
</Text>
<Text variant="muted" monospace>
<ConversationId size="sm" variant="muted" monospace>
{conversationId.slice(0, 8)}
</Text>
<CopyToClipboardButton
aria-label={t('Copy conversation ID')}
priority="transparent"
size="zero"
text={conversationId}
/>
Comment on lines -114 to -119
Copy link
Member

Choose a reason for hiding this comment

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

wasn't it useful?

</ConversationId>
</Flex>
<Divider />
<Flex align="center" gap="sm" wrap="wrap">
<AggregateItem
icon={<IconChat size="sm" />}
iconColor={colors[2]}
label={t('LLM Calls')}
value={<Count value={aggregates.llmCalls} />}
to={aggregates.llmCalls > 0 ? llmCallsUrl : undefined}
isLoading={isLoading}
/>
<AggregateItem
icon={<IconFix size="sm" />}
iconColor={colors[5]}
label={t('Tool Calls')}
value={<Count value={aggregates.toolCalls} />}
to={aggregates.toolCalls > 0 ? toolCallsUrl : undefined}
isLoading={isLoading}
/>
<AggregateItem
icon={<IconFire size="sm" />}
iconColor={theme.tokens.graphics.danger.vibrant}
label={t('Errors')}
value={<Count value={aggregates.errorCount} />}
to={aggregates.errorCount > 0 ? errorsUrl : undefined}
Expand All @@ -160,30 +144,23 @@ export function ConversationSummary({
}

function AggregateItem({
icon,
iconColor,
label,
value,
to,
isLoading,
}: {
label: string;
value: React.ReactNode;
icon?: React.ReactNode;
iconColor?: string;
isLoading?: boolean;
to?: string;
}) {
const isInteractive = !!to && !isLoading;

const content = (
<AggregateItemContainer align="center" gap="xs" isInteractive={isInteractive}>
{icon && (
<Flex as="span" style={{color: iconColor}}>
{icon}
</Flex>
)}
<Text variant="muted">{label}</Text>
<Text bold size="sm">
{label}
</Text>
{isLoading ? (
<Placeholder width="20px" height="16px" />
) : (
Expand All @@ -199,6 +176,12 @@ function AggregateItem({
return content;
}

// Monospace font has different baseline metrics, nudge up to visually align
const ConversationId = styled(Text)`
position: relative;
top: -1px;
Copy link
Member

Choose a reason for hiding this comment

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

If you’d like the ID to start exactly where the “Conversations” label begins, the value should be -0.2px.

That said, I personally think a centered alignment would work best. I haven’t seen the designs, so just sharing my two cents here.

`;

const Divider = styled('div')`
width: 1px;
/* eslint-disable-next-line @sentry/scraps/use-semantic-token */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,15 @@ export function MessageToolCalls({
});

return (
<Footer direction="row" align="center" gap="xs" wrap="wrap" padding="xs sm">
<Text size="xs" style={{opacity: 0.7}}>
{t('Tools called:')}
</Text>
<Flex direction="row" align="center" gap="xs" wrap="wrap" padding="md">
<CollapsibleTagList items={items} failedCount={failedCount} />
</Footer>
</Flex>
);
}

const Footer = styled(Flex)`
border-top: 1px solid ${p => p.theme.tokens.border.primary};
`;

const ClickableTag = styled(Tag)<{hasError?: boolean; isSelected?: boolean}>`
cursor: pointer;
padding: 0 ${p => p.theme.space.xs};
&:hover {
opacity: 0.8;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ describe('MessagesPanel', () => {
);

expect(screen.getByText('The weather is sunny')).toBeInTheDocument();
expect(screen.getByText('Tools called:')).toBeInTheDocument();

expect(screen.getByText('weather')).toBeInTheDocument();
});

Expand Down Expand Up @@ -430,7 +430,7 @@ describe('MessagesPanel', () => {

// The final message should show all tool calls (weather x2 from the skipped span + calculator)
expect(screen.getByText('Here is the comparison')).toBeInTheDocument();
expect(screen.getByText('Tools called:')).toBeInTheDocument();

// Should have 2 weather tags and 1 calculator tag
expect(screen.getAllByText('weather')).toHaveLength(2);
expect(screen.getByText('calculator')).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {Text} from '@sentry/scraps/text';

import ClippedBox from 'sentry/components/clippedBox';
import EmptyMessage from 'sentry/components/emptyMessage';
import {IconUser} from 'sentry/icons';
import {IconBot} from 'sentry/icons/iconBot';
import {t} from 'sentry/locale';
import getDuration from 'sentry/utils/duration/getDuration';
import {MarkedText} from 'sentry/utils/marked/markedText';
import type {AITraceSpanNode} from 'sentry/views/insights/pages/agents/utils/types';
import {MessageToolCalls} from 'sentry/views/insights/pages/conversations/components/messageToolCalls';
Expand Down Expand Up @@ -63,15 +62,25 @@ export function MessagesPanel({nodes, selectedNodeId, onSelectNode}: MessagesPan

if (messages.length === 0) {
return (
<PanelContainer direction="column">
<Flex
direction="column"
padding="lg lg md lg"
background="secondary"
minHeight="100%"
>
<EmptyMessage>{t('No messages found')}</EmptyMessage>
</PanelContainer>
</Flex>
);
}

return (
<PanelContainer direction="column">
<Stack gap="md">
<Flex
direction="column"
padding="lg lg md lg"
background="secondary"
minHeight="100%"
>
<Stack gap="md" width="100%">
{messages.map((message, index) => {
const isSelected = message.id === effectiveSelectedMessageId;
const isAssistant = message.role === 'assistant';
Expand All @@ -84,23 +93,33 @@ export function MessagesPanel({nodes, selectedNodeId, onSelectNode}: MessagesPan
onClick={isAssistant ? () => handleMessageClick(message) : undefined}
>
<MessageHeader justify={message.role === 'user' ? 'end' : 'start'}>
{message.role === 'user' ? <IconUser size="sm" /> : <IconBot size="sm" />}
<Text bold size="sm">
{message.role === 'user' ? t('User') : t('Assistant')}
</Text>
{message.role === 'user' && message.userEmail && (
<Text size="sm" style={{color: 'inherit', opacity: 0.7}}>
{message.userEmail}
{message.role === 'user' ? (
<Text bold size="sm">
{message.userEmail || t('User')}
</Text>
) : (
<Flex align="baseline" gap="sm" flex={1}>
<Text bold size="sm">
{t('Assistant')}
</Text>
{message.duration !== undefined && message.duration > 0 && (
<Text size="xs" variant="muted">
{getDuration(message.duration, 1, true)}
</Text>
)}
</Flex>
)}
</MessageHeader>
<StyledClippedBox
clipHeight={200}
buttonProps={{priority: 'default', size: 'xs'}}
collapsible
>
<Container padding="sm">
<MessageText size="sm">
<Container padding="md">
<MessageText
size="sm"
align={message.role === 'user' ? 'right' : 'left'}
>
<MarkedText
as={TraceDrawerComponents.MarkdownContainer}
text={message.content}
Expand All @@ -120,22 +139,26 @@ export function MessagesPanel({nodes, selectedNodeId, onSelectNode}: MessagesPan
);
})}
</Stack>
</PanelContainer>
</Flex>
);
}

const PanelContainer = styled(Flex)`
padding: ${p => p.theme.space.md} ${p => p.theme.space.lg};
`;

const MessageHeader = styled('div')<{justify?: 'start' | 'end'}>`
display: flex;
align-items: center;
gap: ${p => p.theme.space.sm};
padding: ${p => p.theme.space.sm} ${p => p.theme.space.md};
justify-content: ${p => (p.justify === 'end' ? 'flex-end' : 'flex-start')};
background-color: ${p => p.theme.tokens.background.secondary};
border-bottom: 1px solid ${p => p.theme.tokens.border.primary};

&::after {
content: '';
position: absolute;
left: ${p => p.theme.space.md};
right: ${p => p.theme.space.md};
bottom: 0;
border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
}
position: relative;
`;

const MessageText = styled(Text)`
Expand All @@ -154,9 +177,9 @@ const MessageBubble = styled('div')<{
width: 90%;
align-self: ${p => (p.role === 'user' ? 'flex-end' : 'flex-start')};
background-color: ${p =>
p.role === 'user'
? p.theme.tokens.background.secondary
: p.theme.tokens.background.primary};
p.role === 'assistant'
? p.theme.tokens.background.primary
: p.theme.tokens.background.secondary};
&::after {
content: '';
position: absolute;
Expand All @@ -173,12 +196,7 @@ const MessageBubble = styled('div')<{
cursor: pointer;
&:hover::after {
border-color: ${p.theme.tokens.border.accent.moderate};
}
&:hover {
background-color: ${p.theme.tokens.interactive.transparent.neutral.background.hover};
}
&:active {
background-color: ${p.theme.tokens.interactive.transparent.neutral.background.active};
border-width: 2px;
}
`}
${p =>
Expand Down
Loading
Loading