Skip to content
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

feat(autofix): Add support for streamed output #82024

Merged
merged 3 commits into from
Dec 13, 2024
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
26 changes: 21 additions & 5 deletions static/app/components/events/autofix/autofixChanges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,24 @@ function AutofixRepoChange({
}

const cardAnimationProps: AnimationProps = {
exit: {opacity: 0},
initial: {opacity: 0, y: 20},
animate: {opacity: 1, y: 0},
transition: testableTransition({duration: 0.3}),
exit: {opacity: 0, height: 0, scale: 0.8, y: -20},
initial: {opacity: 0, height: 0, scale: 0.8},
animate: {opacity: 1, height: 'auto', scale: 1},
transition: testableTransition({
duration: 1.0,
height: {
type: 'spring',
bounce: 0.2,
},
scale: {
type: 'spring',
bounce: 0.2,
},
y: {
type: 'tween',
ease: 'easeOut',
},
}),
};

export function AutofixChanges({step, groupId, runId}: AutofixChangesProps) {
Expand Down Expand Up @@ -116,7 +130,9 @@ const PreviewContent = styled('div')`
margin-top: ${space(2)};
`;

const AnimationWrapper = styled(motion.div)``;
const AnimationWrapper = styled(motion.div)`
transform-origin: top center;
`;

const PrefixText = styled('span')``;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,6 @@ describe('AutofixInsightCards', () => {
expect(userMessage.closest('div')).toHaveStyle('color: inherit');
});

it('renders "No insights yet" message when there are no insights', () => {
renderComponent({insights: []});
expect(
screen.getByText(/Autofix will share its discoveries here./)
).toBeInTheDocument();
});

it('toggles context expansion correctly', async () => {
renderComponent();
const contextButton = screen.getByText('Sample insight 1');
Expand Down
65 changes: 47 additions & 18 deletions static/app/components/events/autofix/autofixInsightCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,24 @@ export function ExpandableInsightContext({
}

const animationProps: AnimationProps = {
exit: {opacity: 0},
initial: {opacity: 0, y: 20},
animate: {opacity: 1, y: 0},
transition: testableTransition({duration: 0.3}),
exit: {opacity: 0, height: 0, scale: 0.8, y: -20},
initial: {opacity: 0, height: 0, scale: 0.8},
animate: {opacity: 1, height: 'auto', scale: 1},
transition: testableTransition({
duration: 1.0,
height: {
type: 'spring',
bounce: 0.2,
},
scale: {
type: 'spring',
bounce: 0.2,
},
y: {
type: 'tween',
ease: 'easeOut',
},
}),
};

interface AutofixInsightCardProps {
Expand Down Expand Up @@ -348,15 +362,7 @@ function AutofixInsightCards({
)
)
) : stepIndex === 0 && !hasStepBelow ? (
<NoInsightsYet>
<p>Autofix will share its discoveries here.</p>
<p>
Autofix is like an AI rubber ducky to help you debug your code.
<br />
Collaborate with it and share your own knowledge and opinions for the best
results.
</p>
</NoInsightsYet>
<NoInsightsYet />
) : hasStepBelow ? (
<EmptyResultsContainer>
<ChainLink
Expand Down Expand Up @@ -590,11 +596,7 @@ const NoInsightsYet = styled('div')`
display: flex;
justify-content: center;
flex-direction: column;
padding-left: ${space(4)};
padding-right: ${space(4)};
text-align: center;
color: ${p => p.theme.subText};
padding-top: ${space(4)};
`;

const EmptyResultsContainer = styled('div')`
Expand All @@ -611,6 +613,18 @@ const InsightContainer = styled(motion.div)`
box-shadow: ${p => p.theme.dropShadowMedium};
margin-left: ${space(2)};
margin-right: ${space(2)};
animation: fadeFromActive 1.2s ease-out;

@keyframes fadeFromActive {
from {
background-color: ${p => p.theme.active};
border-color: ${p => p.theme.active};
}
to {
background-color: ${p => p.theme.background};
border-color: ${p => p.theme.innerBorder};
}
}
`;

const ArrowContainer = styled('div')`
Expand Down Expand Up @@ -789,7 +803,22 @@ const StyledStructuredEventData = styled(StructuredEventData)`
border-top-right-radius: 0;
`;

const AnimationWrapper = styled(motion.div)``;
const AnimationWrapper = styled(motion.div)`
transform-origin: top center;

&.new-insight {
animation: textFadeFromActive 1.2s ease-out;
}

@keyframes textFadeFromActive {
from {
color: ${p => p.theme.white};
}
to {
color: inherit;
}
}
`;

const StyledIconChevron = styled(IconChevron)`
width: 5%;
Expand Down
137 changes: 137 additions & 0 deletions static/app/components/events/autofix/autofixOutputStream.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {useEffect, useRef, useState} from 'react';
import {keyframes} from '@emotion/react';
import styled from '@emotion/styled';
import {AnimatePresence, motion} from 'framer-motion';

import {IconArrow} from 'sentry/icons';
import {space} from 'sentry/styles/space';
import testableTransition from 'sentry/utils/testableTransition';

interface Props {
stream: string;
}

const shimmer = keyframes`
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
`;

export function AutofixOutputStream({stream}: Props) {
const [displayedText, setDisplayedText] = useState('');
const previousText = useRef('');
const currentIndexRef = useRef(0);

useEffect(() => {
const newText = stream;

// Reset animation if the new text is completely different
if (!newText.startsWith(displayedText)) {
previousText.current = newText;
currentIndexRef.current = 0;
setDisplayedText('');
}

const interval = window.setInterval(() => {
if (currentIndexRef.current < newText.length) {
setDisplayedText(newText.slice(0, currentIndexRef.current + 1));
currentIndexRef.current++;
} else {
window.clearInterval(interval);
}
}, 15);

return () => {
window.clearInterval(interval);
};
}, [displayedText, stream]);

return (
<AnimatePresence mode="wait">
<Wrapper
key="output-stream"
initial={{opacity: 0, height: 0, scale: 0.8}}
animate={{opacity: 1, height: 'auto', scale: 1}}
exit={{opacity: 0, height: 0, scale: 0.8, y: -20}}
transition={testableTransition({
duration: 1.0,
height: {
type: 'spring',
bounce: 0.2,
},
scale: {
type: 'spring',
bounce: 0.2,
},
y: {
type: 'tween',
ease: 'easeOut',
},
})}
style={{
transformOrigin: 'top center',
}}
>
<StyledArrow direction="down" size="sm" />
<StreamContainer layout>
<StreamContent>{displayedText}</StreamContent>
</StreamContainer>
</Wrapper>
</AnimatePresence>
);
}

const Wrapper = styled(motion.div)`
display: flex;
flex-direction: column;
align-items: center;
margin: ${space(1)} ${space(4)};
gap: ${space(1)};
overflow: hidden;
`;

const StreamContainer = styled(motion.div)`
position: relative;
width: 100%;
border-radius: ${p => p.theme.borderRadius};
background: ${p => p.theme.background};
border: 1px dashed ${p => p.theme.border};
height: 5rem;
overflow: hidden;

&:before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
${p => p.theme.active}20,
transparent
);
background-size: 2000px 100%;
animation: ${shimmer} 2s infinite linear;
pointer-events: none;
}
`;

const StreamContent = styled('div')`
margin: 0;
padding: ${space(2)};
white-space: pre-wrap;
word-break: break-word;
font-size: ${p => p.theme.fontSizeSmall};
color: ${p => p.theme.subText};
height: 5rem;
overflow-y: auto;
display: flex;
flex-direction: column-reverse;
`;

const StyledArrow = styled(IconArrow)`
color: ${p => p.theme.subText};
opacity: 0.5;
`;
26 changes: 21 additions & 5 deletions static/app/components/events/autofix/autofixRootCause.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,24 @@ function AutofixRootCauseDisplay({
}

const cardAnimationProps: AnimationProps = {
exit: {opacity: 0},
initial: {opacity: 0, y: 20},
animate: {opacity: 1, y: 0},
transition: testableTransition({duration: 0.3}),
exit: {opacity: 0, height: 0, scale: 0.8, y: -20},
initial: {opacity: 0, height: 0, scale: 0.8},
animate: {opacity: 1, height: 'auto', scale: 1},
transition: testableTransition({
duration: 1.0,
height: {
type: 'spring',
bounce: 0.2,
},
scale: {
type: 'spring',
bounce: 0.2,
},
y: {
type: 'tween',
ease: 'easeOut',
},
}),
};

export function AutofixRootCause(props: AutofixRootCauseProps) {
Expand Down Expand Up @@ -627,7 +641,9 @@ const ContentWrapper = styled(motion.div)<{selected: boolean}>`
}
`;

const AnimationWrapper = styled(motion.div)``;
const AnimationWrapper = styled(motion.div)`
transform-origin: top center;
`;

const CustomRootCausePadding = styled('div')`
padding: ${space(2)} ${space(2)} ${space(2)} ${space(2)};
Expand Down
4 changes: 4 additions & 0 deletions static/app/components/events/autofix/autofixSteps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AutofixInsightCards, {
useUpdateInsightCard,
} from 'sentry/components/events/autofix/autofixInsightCards';
import AutofixMessageBox from 'sentry/components/events/autofix/autofixMessageBox';
import {AutofixOutputStream} from 'sentry/components/events/autofix/autofixOutputStream';
import {
AutofixRootCause,
useSelectCause,
Expand Down Expand Up @@ -280,6 +281,9 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) {
</div>
);
})}
{lastStep.output_stream && (
<AutofixOutputStream stream={lastStep.output_stream} />
)}
</StepsContainer>

<AutofixMessageBox
Expand Down
1 change: 1 addition & 0 deletions static/app/components/events/autofix/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ interface BaseStep {
title: string;
type: AutofixStepType;
completedMessage?: string;
output_stream?: string | null;
}

export type CodeSnippetContext = {
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/events/autofix/useAutofix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type AutofixResponse = {
autofix: AutofixData | null;
};

const POLL_INTERVAL = 1000;
const POLL_INTERVAL = 500;

export const makeAutofixQueryKey = (groupId: string): ApiQueryKey => [
`/issues/${groupId}/autofix/`,
Expand Down
Loading