import {useCallback, useEffect, useState} from 'react'; import {createPortal} from 'react-dom'; import {usePopper} from 'react-popper'; import styled from '@emotion/styled'; import {AnimatePresence, type AnimationProps, motion} from 'framer-motion'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {Button} from 'sentry/components/button'; import { replaceHeadersWithBold, SuggestedFixSnippet, } from 'sentry/components/events/autofix/autofixRootCause'; import type { AutofixInsight, AutofixRepository, BreadcrumbContext, } from 'sentry/components/events/autofix/types'; import {makeAutofixQueryKey} from 'sentry/components/events/autofix/useAutofix'; import BreadcrumbItemContent from 'sentry/components/events/breadcrumbs/breadcrumbItemContent'; import { BreadcrumbIcon, BreadcrumbLevel, getBreadcrumbColorConfig, getBreadcrumbTitle, } from 'sentry/components/events/breadcrumbs/utils'; import Input from 'sentry/components/input'; import StructuredEventData from 'sentry/components/structuredEventData'; import Timeline from 'sentry/components/timeline'; import { IconArrow, IconChevron, IconCode, IconFire, IconRefresh, IconSpan, IconUser, } from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs'; import {singleLineRenderer} from 'sentry/utils/marked'; import {useMutation, useQueryClient} from 'sentry/utils/queryClient'; import testableTransition from 'sentry/utils/testableTransition'; import useApi from 'sentry/utils/useApi'; interface AutofixBreadcrumbSnippetProps { breadcrumb: BreadcrumbContext; } function AutofixBreadcrumbSnippet({breadcrumb}: AutofixBreadcrumbSnippetProps) { const type = BreadcrumbType[breadcrumb.category.toUpperCase()]; const level = BreadcrumbLevelType[breadcrumb.level.toUpperCase()]; const rawCrumb = { message: breadcrumb.body, category: breadcrumb.category, type, level, }; return (
{getBreadcrumbTitle(rawCrumb)}
{level} } colorConfig={getBreadcrumbColorConfig(type)} icon={} isActive showLastLine >
); } export function ExpandableInsightContext({ children, title, icon, rounded, expandByDefault = false, }: { children: React.ReactNode; title: string; expandByDefault?: boolean; icon?: React.ReactNode; rounded?: boolean; }) { const [expanded, setExpanded] = useState(expandByDefault); const toggleExpand = () => { setExpanded(oldState => !oldState); }; return ( {icon} {title} {expanded && {children}} ); } const animationProps: AnimationProps = { 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 { groupId: string; hasCardAbove: boolean; hasCardBelow: boolean; index: number; insight: AutofixInsight; repos: AutofixRepository[]; runId: string; stepIndex: number; isLastInsightInStep?: boolean; shouldHighlightRethink?: boolean; } function AutofixInsightCard({ insight, hasCardBelow, hasCardAbove, repos, index, stepIndex, groupId, runId, shouldHighlightRethink, isLastInsightInStep, }: AutofixInsightCardProps) { const isUserMessage = insight.justification === 'USER'; const [expanded, setExpanded] = useState(false); const toggleExpand = () => { setExpanded(oldState => !oldState); }; return ( {hasCardAbove && ( )} {!isUserMessage && ( {expanded && (

{insight.stacktrace_context && insight.stacktrace_context.length > 0 && (

{t( 'Stacktrace%s and Variables:', insight.stacktrace_context.length > 1 ? 's' : '' )} {insight.stacktrace_context .map((stacktrace, i) => { let vars: any = {}; try { vars = JSON.parse(stacktrace.vars_as_json); } catch { vars = {vars: stacktrace.vars_as_json}; } return (
); }) .reverse()}
)} {insight.breadcrumb_context && insight.breadcrumb_context.length > 0 && (
{t( 'Breadcrumb%s:', insight.breadcrumb_context.length > 1 ? 's' : '' )} {insight.breadcrumb_context .map((breadcrumb, i) => { return ( ); }) .reverse()}
)} {insight.codebase_context && insight.codebase_context.length > 0 && (
{t( 'Code Snippet%s:', insight.codebase_context.length > 1 ? 's' : '' )} {insight.codebase_context .map((code, i) => { return ( ); }) .reverse()}
)}
)}
)} {isUserMessage && ( )} {hasCardBelow && ( )}
); } interface AutofixInsightCardsProps { groupId: string; hasStepAbove: boolean; hasStepBelow: boolean; insights: AutofixInsight[]; repos: AutofixRepository[]; runId: string; stepIndex: number; shouldHighlightRethink?: boolean; } function AutofixInsightCards({ insights, repos, hasStepBelow, hasStepAbove, stepIndex, groupId, runId, shouldHighlightRethink, }: AutofixInsightCardsProps) { return ( {insights.length > 0 ? ( insights.map((insight, index) => !insight ? null : ( ) ) ) : stepIndex === 0 && !hasStepBelow ? ( ) : hasStepBelow ? ( ) : null} ); } export function useUpdateInsightCard({groupId, runId}: {groupId: string; runId: string}) { const api = useApi({persistInFlight: true}); const queryClient = useQueryClient(); return useMutation({ mutationFn: (params: { message: string; retain_insight_card_index: number | null; step_index: number; }) => { return api.requestPromise(`/issues/${groupId}/autofix/update/`, { method: 'POST', data: { run_id: runId, payload: { type: 'restart_from_point_with_feedback', message: params.message, step_index: params.step_index, retain_insight_card_index: params.retain_insight_card_index, }, }, }); }, onSuccess: _ => { queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)}); addSuccessMessage(t('Thanks, rethinking this...')); }, onError: () => { addErrorMessage(t('Something went wrong when sending Autofix your message.')); }, }); } function ChainLink({ groupId, runId, stepIndex, insightCardAboveIndex, isHighlighted, isLastCard, }: { groupId: string; insightCardAboveIndex: number | null; runId: string; stepIndex: number; isHighlighted?: boolean; isLastCard?: boolean; }) { const [showOverlay, setShowOverlay] = useState(false); const [referenceElement, setReferenceElement] = useState< HTMLAnchorElement | HTMLButtonElement | null >(null); const [popperElement, setPopperElement] = useState(null); const [comment, setComment] = useState(''); const {mutate: send} = useUpdateInsightCard({groupId, runId}); const {styles, attributes} = usePopper(referenceElement, popperElement, { placement: 'left-start', modifiers: [ { name: 'offset', options: { offset: [-16, 8], }, }, { name: 'flip', options: { fallbackPlacements: ['right-start', 'bottom-start'], }, }, ], }); const handleClickOutside = useCallback( (event: MouseEvent) => { if ( referenceElement?.contains(event.target as Node) || popperElement?.contains(event.target as Node) ) { return; } setShowOverlay(false); }, [popperElement, referenceElement] ); useEffect(() => { if (showOverlay) { document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showOverlay, handleClickOutside]); return ( {isLastCard && isHighlighted && ( Not satisfied? )} } size="zero" className="rethink-button" title={t('Rethink from here')} aria-label={t('Rethink from here')} onClick={() => setShowOverlay(true)} isHighlighted={isHighlighted} /> {showOverlay && createPortal(
{ e.preventDefault(); e.stopPropagation(); setShowOverlay(false); setComment(''); send({ message: comment, step_index: stepIndex, retain_insight_card_index: insightCardAboveIndex, }); }} className="row-form" onClick={e => e.stopPropagation()} id="autofix-rethink-input" > setComment(e.target.value)} size="md" autoFocus id="autofix-rethink-input" />