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')}
{dataIsLoading ? (
) : dataIsError ? (
setSuggestedSolutionLocalConfig({individualConsent: true})
}
restriction={error?.responseJSON?.restriction as any}
onHideSuggestion={onHideSuggestion}
/>
) : (
)}
{!dataIsLoading && !dataIsError && (
{t('Was this helpful?')}
}
size="xs"
onClick={() => {
trackAnalytics(
'ai_suggested_solution.feedback_helpful_nope_button_clicked',
{
organization,
project_id: event.projectID,
group_id: event.groupID,
...getAnalyticsDataForEvent(event),
}
);
handleFeedbackClick();
}}
>
{t('Nope')}
}
size="xs"
onClick={() => {
trackAnalytics(
'ai_suggested_solution.feedback_helpful_kinda_button_clicked',
{
organization,
project_id: event.projectID,
group_id: event.groupID,
...getAnalyticsDataForEvent(event),
}
);
handleFeedbackClick();
}}
>
{t('Kinda')}
}
size="xs"
onClick={() => {
trackAnalytics(
'ai_suggested_solution.feedback_helpful_yes_button_clicked',
{
organization,
project_id: event.projectID,
group_id: event.groupID,
...getAnalyticsDataForEvent(event),
}
);
handleFeedbackClick();
}}
>
{t('Yes, Surprisingly\u2026')}
)}
);
}
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;
`;