diff --git a/static/app/components/events/autofix/autofixChanges.tsx b/static/app/components/events/autofix/autofixChanges.tsx index 223b0a9790d8a8..014e76a2a5c8cf 100644 --- a/static/app/components/events/autofix/autofixChanges.tsx +++ b/static/app/components/events/autofix/autofixChanges.tsx @@ -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) { @@ -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')``; diff --git a/static/app/components/events/autofix/autofixInsightCards.spec.tsx b/static/app/components/events/autofix/autofixInsightCards.spec.tsx index 864c4f89462e14..8ee14f7e5cee1e 100644 --- a/static/app/components/events/autofix/autofixInsightCards.spec.tsx +++ b/static/app/components/events/autofix/autofixInsightCards.spec.tsx @@ -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'); diff --git a/static/app/components/events/autofix/autofixInsightCards.tsx b/static/app/components/events/autofix/autofixInsightCards.tsx index 52a896e384f74a..1d185dd5d314ff 100644 --- a/static/app/components/events/autofix/autofixInsightCards.tsx +++ b/static/app/components/events/autofix/autofixInsightCards.tsx @@ -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 { @@ -348,15 +362,7 @@ function AutofixInsightCards({ ) ) ) : stepIndex === 0 && !hasStepBelow ? ( - -

Autofix will share its discoveries here.

-

- Autofix is like an AI rubber ducky to help you debug your code. -
- Collaborate with it and share your own knowledge and opinions for the best - results. -

-
+ ) : hasStepBelow ? ( p.theme.subText}; - padding-top: ${space(4)}; `; const EmptyResultsContainer = styled('div')` @@ -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')` @@ -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%; diff --git a/static/app/components/events/autofix/autofixOutputStream.tsx b/static/app/components/events/autofix/autofixOutputStream.tsx new file mode 100644 index 00000000000000..4dff30816dd6bb --- /dev/null +++ b/static/app/components/events/autofix/autofixOutputStream.tsx @@ -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 ( + + + + + {displayedText} + + + + ); +} + +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; +`; diff --git a/static/app/components/events/autofix/autofixRootCause.tsx b/static/app/components/events/autofix/autofixRootCause.tsx index 2a301cef677d0f..d0a95855274952 100644 --- a/static/app/components/events/autofix/autofixRootCause.tsx +++ b/static/app/components/events/autofix/autofixRootCause.tsx @@ -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) { @@ -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)}; diff --git a/static/app/components/events/autofix/autofixSteps.tsx b/static/app/components/events/autofix/autofixSteps.tsx index 37a5879485a631..d6e7d10b3b389f 100644 --- a/static/app/components/events/autofix/autofixSteps.tsx +++ b/static/app/components/events/autofix/autofixSteps.tsx @@ -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, @@ -280,6 +281,9 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { ); })} + {lastStep.output_stream && ( + + )} [ `/issues/${groupId}/autofix/`,