import {useCallback, useState} from 'react'; import styled from '@emotion/styled'; import {addSuccessMessage} from 'sentry/actionCreators/indicator'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import EmptyMessage from 'sentry/components/emptyMessage'; import LoadingError from 'sentry/components/loadingError'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import PanelFooter from 'sentry/components/panels/panelFooter'; import PanelHeader from 'sentry/components/panels/panelHeader'; import {IconFile, IconFlag, IconHappy, IconMeh, IconSad} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getAnalyticsDataForEvent} from 'sentry/utils/events'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import {limitedMarked} from 'sentry/utils/marked'; import {useApiQuery} from 'sentry/utils/queryClient'; import {useIsSentryEmployee} from 'sentry/utils/useIsSentryEmployee'; import useOrganization from 'sentry/utils/useOrganization'; import {ExperimentalFeatureBadge} from './experimentalFeatureBadge'; import {SuggestionLoaderMessage} from './suggestionLoaderMessage'; import {useOpenAISuggestionLocalStorage} from './useOpenAISuggestionLocalStorage'; type Props = { event: Event; onHideSuggestion: () => void; projectSlug: Project['slug']; }; function ErrorDescription({ restriction, organizationSlug, onRefetch, onSetIndividualConsent, onHideSuggestion, }: { onHideSuggestion: () => void; onRefetch: () => void; onSetIndividualConsent: (consent: boolean) => void; organizationSlug: string; restriction?: 'subprocessor' | 'individual_consent'; }) { if (restriction === 'subprocessor') { return ( } title={t('OpenAI Subprocessor Acknowledgment')} description={t( 'In order to use this feature, your organization needs to accept the OpenAI Subprocessor Acknowledgment.' )} action={ } /> ); } if (restriction === 'individual_consent') { const activeSuperUser = isActiveSuperuser(); return ( } title={t('We need your consent')} description={t( 'By using this feature, you agree that OpenAI is a subprocessor and may process the data that you’ve chosen to submit. Sentry makes no guarantees as to the accuracy of the feature’s AI-generated recommendations.' )} action={ } /> ); } return ; } export function Suggestion({onHideSuggestion, projectSlug, event}: Props) { const organization = useOrganization(); const [suggestedSolutionLocalConfig, setSuggestedSolutionLocalConfig] = useOpenAISuggestionLocalStorage(); const [piiCertified, setPiiCertified] = useState(false); const isSentryEmployee = useIsSentryEmployee(); const { data, isLoading: dataIsLoading, isError: dataIsError, refetch: dataRefetch, error, } = useApiQuery<{suggestion: string}>( [ `/projects/${organization.slug}/${projectSlug}/events/${event.eventID}/ai-fix-suggest/`, { query: { consent: suggestedSolutionLocalConfig.individualConsent ? 'yes' : undefined, pii_certified: isSentryEmployee ? (piiCertified ? 'yes' : 'no') : undefined, }, }, ], { enabled: isSentryEmployee ? (piiCertified ? true : false) : true, staleTime: Infinity, retry: false, } ); const handleFeedbackClick = useCallback(() => { addSuccessMessage('Thank you for your feedback!'); }, []); if (isSentryEmployee && !piiCertified) { return ( } title={t('PII Certification Required')} description={t( 'Before using this feature, please confirm that there is no personally identifiable information in this event.' )} action={ } /> ); } return (
{t('AI Solution')} <ExperimentalFeatureBadge />
{dataIsLoading ? (
) : dataIsError ? ( setSuggestedSolutionLocalConfig({individualConsent: true}) } restriction={error?.responseJSON?.restriction as any} onHideSuggestion={onHideSuggestion} /> ) : ( )} {!dataIsLoading && !dataIsError && ( {t('Was this helpful?')} )} ); } const Header = styled(PanelHeader)` background: transparent; padding: ${space(1)} ${space(2)}; align-items: center; color: ${p => p.theme.gray300}; `; const Feedback = styled('div')` padding: ${space(1)} ${space(2)}; display: grid; grid-template-columns: 1fr; align-items: center; text-align: left; gap: ${space(1)}; font-size: ${p => p.theme.fontSizeSmall}; @media (min-width: ${p => p.theme.breakpoints.small}) { grid-template-columns: 1fr max-content; text-align: right; gap: ${space(2)}; } `; const SuggestionLoadingError = styled(LoadingError)` margin-bottom: 0; border: none; /* This is just to be consitent with other */ /* padding-top and padding-bottom we are using in the empty state component */ padding-top: ${space(4)}; padding-bottom: ${space(4)}; `; const LoaderWrapper = styled('div')` padding: ${space(4)} 0; text-align: center; gap: ${space(2)}; display: flex; flex-direction: column; `; const Content = styled('div')` padding: ${space(2)}; /* hack until we update backend to send us other heading */ h4 { font-size: ${p => p.theme.fontSizeExtraLarge}; margin-bottom: ${space(1)}; } `; const Title = styled('div')` /* to be consistent with the feature badge size */ height: ${space(2)}; line-height: ${space(2)}; display: flex; align-items: center; `;