Skip to content

Commit 9ab1ef3

Browse files
feat(replay): Update design of Replay Summaries (#94569)
Updates the Replay Summaries tab to be a little more aligned with Jesse's designs. For now, I have the chapter title bolded when the chapter is "active" closed: <img width="669" alt="image" src="https://github.com/user-attachments/assets/8ffad263-704a-4b80-be84-bccb0297d4ae" /> opened: <img width="674" alt="image" src="https://github.com/user-attachments/assets/c2654939-ecf2-4961-9c99-b90efda6ff40" /> multiple: <img width="664" alt="image" src="https://github.com/user-attachments/assets/e7a7cad1-977a-49eb-8976-30ff04fe90eb" /> --------- Co-authored-by: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com>
1 parent e73096b commit 9ab1ef3

File tree

5 files changed

+528
-329
lines changed

5 files changed

+528
-329
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import styled from '@emotion/styled';
2+
3+
import {Alert} from 'sentry/components/core/alert';
4+
import {Badge} from 'sentry/components/core/badge';
5+
import {Button} from 'sentry/components/core/button';
6+
import {Flex} from 'sentry/components/core/layout';
7+
import ErrorBoundary from 'sentry/components/errorBoundary';
8+
import LoadingIndicator from 'sentry/components/loadingIndicator';
9+
import {useReplayContext} from 'sentry/components/replays/replayContext';
10+
import {IconSync, IconThumb} from 'sentry/icons';
11+
import {t} from 'sentry/locale';
12+
import {space} from 'sentry/styles/space';
13+
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
14+
import useOrganization from 'sentry/utils/useOrganization';
15+
import useProjectFromId from 'sentry/utils/useProjectFromId';
16+
import {ChapterList} from 'sentry/views/replays/detail/ai/chapterList';
17+
import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
18+
import TabItemContainer from 'sentry/views/replays/detail/tabItemContainer';
19+
20+
import {useFetchReplaySummary} from './useFetchReplaySummary';
21+
22+
export default function Ai() {
23+
return (
24+
<PaddedFluidHeight>
25+
<SummaryTabItemContainer data-test-id="replay-details-ai-summary-tab">
26+
<ErrorBoundary mini>
27+
<AiContent />
28+
</ErrorBoundary>
29+
</SummaryTabItemContainer>
30+
</PaddedFluidHeight>
31+
);
32+
}
33+
34+
function AiContent() {
35+
const organization = useOrganization();
36+
const {replay} = useReplayContext();
37+
const replayRecord = replay?.getReplay();
38+
const project = useProjectFromId({project_id: replayRecord?.project_id});
39+
const {
40+
data: summaryData,
41+
isPending,
42+
isError,
43+
isRefetching,
44+
refetch,
45+
} = useFetchReplaySummary({
46+
staleTime: 0,
47+
enabled: Boolean(
48+
replayRecord?.id &&
49+
project?.slug &&
50+
organization.features.includes('replay-ai-summaries')
51+
),
52+
retry: false,
53+
});
54+
55+
const openForm = useFeedbackForm();
56+
57+
const feedbackButton = ({type}: {type: 'positive' | 'negative'}) => {
58+
return openForm ? (
59+
<Button
60+
aria-label={t('Give feedback on the AI summary section')}
61+
icon={<IconThumb direction={type === 'positive' ? 'up' : 'down'} />}
62+
title={type === 'positive' ? t('I like this') : t(`I don't like this`)}
63+
size={'xs'}
64+
onClick={() =>
65+
openForm({
66+
messagePlaceholder:
67+
type === 'positive'
68+
? t('What did you like about the AI summary and chapters?')
69+
: t('How can we make the AI summary and chapters work better for you?'),
70+
tags: {
71+
['feedback.source']: 'replay_ai_summary',
72+
['feedback.owner']: 'replay',
73+
['feedback.type']: type,
74+
},
75+
})
76+
}
77+
/>
78+
) : null;
79+
};
80+
81+
if (!organization.features.includes('replay-ai-summaries')) {
82+
return (
83+
<SummaryContainer>
84+
<Alert type="info">
85+
{t('Replay AI summary is not available for this organization.')}
86+
</Alert>
87+
</SummaryContainer>
88+
);
89+
}
90+
91+
if (replayRecord?.project_id && !project) {
92+
return (
93+
<SummaryContainer>
94+
<Alert type="error">{t('Project not found. Unable to load AI summary.')}</Alert>
95+
</SummaryContainer>
96+
);
97+
}
98+
99+
if (isPending || isRefetching) {
100+
return (
101+
<LoadingContainer>
102+
<LoadingIndicator />
103+
</LoadingContainer>
104+
);
105+
}
106+
107+
if (isError) {
108+
return (
109+
<SummaryContainer>
110+
<Alert type="error">{t('Failed to load AI summary')}</Alert>
111+
</SummaryContainer>
112+
);
113+
}
114+
115+
if (!summaryData) {
116+
return (
117+
<SummaryContainer>
118+
<Alert type="info">{t('No summary available for this replay.')}</Alert>
119+
</SummaryContainer>
120+
);
121+
}
122+
123+
return (
124+
<ErrorBoundary mini>
125+
<SplitContainer>
126+
<Summary>
127+
<SummaryLeft>
128+
<SummaryLeftTitle>
129+
{t('Replay Summary')}
130+
<Badge type="internal">{t('Internal')}</Badge>
131+
</SummaryLeftTitle>
132+
<SummaryText>{summaryData.data.summary}</SummaryText>
133+
</SummaryLeft>
134+
<SummaryRight>
135+
<Flex gap={space(0.5)}>
136+
{feedbackButton({type: 'positive'})}
137+
{feedbackButton({type: 'negative'})}
138+
</Flex>
139+
<Button
140+
priority="default"
141+
type="button"
142+
size="xs"
143+
onClick={() => refetch()}
144+
icon={<IconSync size="xs" />}
145+
>
146+
{t('Regenerate')}
147+
</Button>
148+
</SummaryRight>
149+
</Summary>
150+
<ChapterList summaryData={summaryData} />
151+
</SplitContainer>
152+
</ErrorBoundary>
153+
);
154+
}
155+
156+
const SummaryTabItemContainer = styled(TabItemContainer)`
157+
.beforeHoverTime:last-child {
158+
border-bottom-color: transparent;
159+
}
160+
.beforeCurrentTime:last-child {
161+
border-bottom-color: transparent;
162+
}
163+
details.beforeHoverTime + details.afterHoverTime,
164+
details.beforeCurrentTime + details.afterCurrentTime {
165+
border-top-color: transparent;
166+
}
167+
`;
168+
169+
const SplitContainer = styled('div')`
170+
display: flex;
171+
flex-direction: column;
172+
overflow: auto;
173+
`;
174+
175+
const PaddedFluidHeight = styled(FluidHeight)`
176+
padding-top: ${space(1)};
177+
`;
178+
179+
const LoadingContainer = styled('div')`
180+
display: flex;
181+
justify-content: center;
182+
padding: ${space(4)};
183+
`;
184+
185+
const SummaryContainer = styled('div')`
186+
padding: ${space(2)};
187+
overflow: auto;
188+
`;
189+
190+
const Summary = styled('div')`
191+
display: flex;
192+
align-items: center;
193+
padding: ${space(1)} ${space(1.5)};
194+
border-bottom: 1px solid ${p => p.theme.border};
195+
gap: ${space(4)};
196+
`;
197+
198+
const SummaryLeft = styled('div')`
199+
display: flex;
200+
flex-direction: column;
201+
gap: ${space(0.5)};
202+
justify-content: space-between;
203+
font-size: ${p => p.theme.fontSize.lg};
204+
font-weight: ${p => p.theme.fontWeight.bold};
205+
`;
206+
207+
const SummaryRight = styled('div')`
208+
display: flex;
209+
flex-direction: column;
210+
gap: ${space(1)};
211+
align-items: flex-end;
212+
`;
213+
214+
const SummaryLeftTitle = styled('div')`
215+
display: flex;
216+
align-items: center;
217+
gap: ${space(1)};
218+
`;
219+
220+
const SummaryText = styled('p')`
221+
line-height: 1.6;
222+
white-space: pre-wrap;
223+
margin: 0;
224+
font-size: ${p => p.theme.fontSize.md};
225+
color: ${p => p.theme.subText};
226+
font-weight: ${p => p.theme.fontWeight.normal};
227+
`;

0 commit comments

Comments
 (0)