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
1 change: 1 addition & 0 deletions static/app/components/core/badge/tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const Text = styled('div')`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
/* @TODO(jonasbadalic): Some occurrences pass other things than strings into the children prop. */
Comment thread
obostjancic marked this conversation as resolved.
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styled from '@emotion/styled';

import {Tag} from '@sentry/scraps/badge';
import {Button} from '@sentry/scraps/button';
import {InfoText} from '@sentry/scraps/info';
import {Flex} from '@sentry/scraps/layout';
import {Link} from '@sentry/scraps/link';
import {Heading, Text} from '@sentry/scraps/text';
Expand Down Expand Up @@ -45,7 +46,7 @@ interface ConversationSummaryProps {
}

const VISIBLE_TRACE_COUNT = 5;
const VISIBLE_TOOL_COUNT = 5;
const VISIBLE_TOOL_COUNT = 4;

function getTraceUrl(orgSlug: string, traceId: string, spanId: string) {
return normalizeUrl(
Expand Down Expand Up @@ -128,7 +129,7 @@ export function ConversationAggregatesBar({
const errorsUrl = getExploreUrl({
organization,
selection,
query: `gen_ai.conversation.id:"${conversationId.replace(/"/g, '\\"')}" span.status:internal_error`,
query: `gen_ai.conversation.id:"${conversationId.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}" span.status:internal_error`,
});

return (
Expand Down Expand Up @@ -189,24 +190,22 @@ export function ConversationAggregatesBar({
</Tag>
))}
{aggregates.toolNames.length > VISIBLE_TOOL_COUNT && (
<DropdownMenu
<InfoText
size="sm"
triggerLabel={
<Text size="sm" variant="muted">
{t('+%s more', aggregates.toolNames.length - VISIBLE_TOOL_COUNT)}
</Text>
variant="muted"
wrap="nowrap"
title={
<Flex wrap="wrap" gap="xs" paddingTop="xs" paddingBottom="xs">
{aggregates.toolNames.slice(VISIBLE_TOOL_COUNT).map(name => (
<Tag key={name} variant="info">
{name}
</Tag>
))}
</Flex>
}
triggerProps={{
size: 'zero',
variant: 'transparent',
showChevron: false,
}}
items={aggregates.toolNames.slice(VISIBLE_TOOL_COUNT).map(name => ({
key: name,
label: <Tag variant="info">{name}</Tag>,
textValue: name,
}))}
/>
>
{t('+%s more', aggregates.toolNames.length - VISIBLE_TOOL_COUNT)}
</InfoText>
)}
</ToolTagsRow>
)
Expand All @@ -228,7 +227,9 @@ export function ConversationSummary({
}, [nodes]);

const handleCopyConversationId = () => {
trackAnalytics('conversations.detail.copy-conversation-id', {organization});
trackAnalytics('conversations.detail.copy-conversation-id', {
organization,
});
copyToClipboard(conversationId, {
successMessage: t('Copied conversation ID to clipboard'),
});
Expand Down Expand Up @@ -331,7 +332,9 @@ export function ConversationSummary({
isLoading={isLoading}
lastMessageDate={lastMessageDate}
onErrorsLinkClick={() =>
trackAnalytics('conversations.detail.click-errors-link', {organization})
trackAnalytics('conversations.detail.click-errors-link', {
organization,
})
}
/>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import {css} from '@emotion/react';
import {useTheme} from '@emotion/react';

import {Tag} from '@sentry/scraps/badge';
import {Container, Flex} from '@sentry/scraps/layout';
Expand All @@ -19,25 +20,42 @@ interface MessageToolCallsProps {
toolCalls: ToolCall[];
}

const hoverStyle = css`
&:hover {
opacity: 0.85;
}
`;

export function MessageToolCalls({
toolCalls,
selectedNodeId,
nodeMap,
onSelectNode,
}: MessageToolCallsProps) {
const organization = useOrganization();
const theme = useTheme();

return (
<Flex direction="column" gap="xs" padding="sm md xs md">
{toolCalls.map(tool => {
const toolNode = nodeMap.get(tool.nodeId);
const isToolSelected = tool.nodeId === selectedNodeId;
return (
<ToolCallLine
<Container
key={tool.nodeId}
background="tertiary"
radius="sm"
padding="xs sm"
cursor="pointer"
css={hoverStyle}
style={
isToolSelected
? {
outline: `2px solid ${tool.hasError ? theme.tokens.content.danger : theme.tokens.focus.default}`,
outlineOffset: '-2px',
}
: undefined
}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
trackAnalytics('conversations.message.click-tool-call', {organization});
Expand All @@ -52,17 +70,15 @@ export function MessageToolCalls({
{t('Called tool')}
</Text>
</Container>
<ClickableTag
<Tag
variant={tool.hasError ? 'danger' : 'info'}
icon={tool.hasError ? <IconFire /> : undefined}
hasError={tool.hasError}
isSelected={isToolSelected}
>
{tool.name}
</ClickableTag>
</Tag>
{toolNode && <ToolInputPreview node={toolNode} />}
</Flex>
</ToolCallLine>
</Container>
);
})}
</Flex>
Expand All @@ -80,20 +96,3 @@ function ToolInputPreview({node}: {node: AITraceSpanNode}) {
</Text>
);
}

const ToolCallLine = styled(Container)`
&:hover {
opacity: 0.85;
}
`;

const ClickableTag = styled(Tag)<{hasError?: boolean; isSelected?: boolean}>`
cursor: pointer;
padding: 0 ${p => p.theme.space.xs};
${p =>
p.isSelected &&
`
outline: 2px solid ${p.hasError ? p.theme.tokens.content.danger : p.theme.tokens.focus.default};
outline-offset: -2px;
`}
`;
18 changes: 15 additions & 3 deletions static/app/views/explore/conversations/components/toolTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,29 @@ export function ToolTags({toolNames}: ToolTagsProps) {
const [hiddenCount, setHiddenCount] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const tagRefs = useRef(new Map<number, HTMLElement>());
const toggleButtonRef = useRef<HTMLDivElement>(null);

// Calculate how many tags are hidden (overflow beyond 2 rows)
// Calculate how many tags are hidden (overflow beyond 2 rows, or overlapped by the "+N more" button)
useEffect(() => {
const container = containerRef.current;
if (expanded || !container) {
return;
}

const calculateHidden = () => {
const buttonWidth = toggleButtonRef.current?.offsetWidth ?? 0;
const containerWidth = container.offsetWidth;

let hidden = 0;
tagRefs.current.forEach(tagEl => {
if (tagEl.offsetTop >= TWO_ROW_HEIGHT) {
hidden++;
} else if (
buttonWidth > 0 &&
tagEl.offsetLeft + tagEl.offsetWidth > containerWidth - buttonWidth
) {
// Tag on the last visible row is partially covered by the "+N more" button
hidden++;
}
});
setHiddenCount(hidden);
Expand All @@ -45,7 +55,8 @@ export function ToolTags({toolNames}: ToolTagsProps) {
cancelAnimationFrame(rafId);
observer.disconnect();
};
}, [toolNames, expanded]);
// hiddenCount is included so we re-check after the button appears (it only renders when hiddenCount > 0)
}, [toolNames, expanded, hiddenCount]);

return (
<ToolTagsContainer ref={containerRef} expanded={expanded}>
Expand All @@ -60,12 +71,13 @@ export function ToolTags({toolNames}: ToolTagsProps) {
}
}}
variant="info"
style={{maxWidth: '100%', minWidth: 0}}
>
{toolName}
</Tag>
))}
{hiddenCount > 0 && !expanded && (
<ToggleButtonWrapper>
<ToggleButtonWrapper ref={toggleButtonRef}>
<ToggleButton
variant="link"
size="xs"
Expand Down
Loading