Browse Source

ref(suggested-solution): Add new design (#47250)

Priscila Oliveira 1 year ago
parent
commit
992cd3ce86

+ 1 - 0
static/app/components/events/aiSuggestedSolution/banner-background.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="artwork" x="0" y="0" style="enable-background:new 0 0 503.8 81" version="1.1" viewBox="0 0 503.8 81"><style>.st0{fill:#ffe5b8}</style><path d="M503.8 0v77c0 2.2-1.8 4-4 4H20.3L0 18.2 269.9 0h233.9z" class="st0"/><path d="M503.8 77v4h-4c2.2 0 4-1.8 4-4z" class="st0"/></svg>

File diff suppressed because it is too large
+ 0 - 0
static/app/components/events/aiSuggestedSolution/banner-sentaur-dark-mode.svg


File diff suppressed because it is too large
+ 0 - 0
static/app/components/events/aiSuggestedSolution/banner-sentaur-light-mode.svg


File diff suppressed because it is too large
+ 0 - 0
static/app/components/events/aiSuggestedSolution/banner-stars.svg


+ 131 - 0
static/app/components/events/aiSuggestedSolution/banner.tsx

@@ -0,0 +1,131 @@
+import styled from '@emotion/styled';
+
+import {Button} from 'sentry/components/button';
+import {Panel, PanelBody} from 'sentry/components/panels';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
+
+import bannerBackground from './banner-background.svg';
+import bannerSentaurDarkMode from './banner-sentaur-dark-mode.svg';
+import bannerSentaurLightMode from './banner-sentaur-light-mode.svg';
+import bannerStars from './banner-stars.svg';
+import {ExperimentalFeatureBadge} from './experimentalFeatureBadge';
+
+type Props = {
+  darkMode: boolean;
+  onViewSuggestion: () => void;
+};
+
+export function Banner({onViewSuggestion, darkMode}: Props) {
+  return (
+    <Wrapper>
+      <Body withPadding>
+        <div>
+          <Title>
+            {t('AI Solutions')}
+            <ExperimentalFeatureBadge />
+          </Title>
+          <Description>
+            {t('You might get lucky, but again, maybe not\u2026')}
+          </Description>
+        </div>
+        <Action>
+          <Background src={bannerBackground} />
+          <Stars src={bannerStars} />
+          <Sentaur src={darkMode ? bannerSentaurDarkMode : bannerSentaurLightMode} />
+          <ViewSuggestionButton size="xs" onClick={onViewSuggestion}>
+            {t('View Suggestion')}
+          </ViewSuggestionButton>
+        </Action>
+      </Body>
+    </Wrapper>
+  );
+}
+
+const Wrapper = styled(Panel)`
+  margin-bottom: 0;
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    height: 80px;
+  }
+`;
+
+const Body = styled(PanelBody)`
+  display: grid;
+  grid-template-columns: 1fr max-content;
+  align-items: center;
+
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    grid-template-columns: 42% 1fr;
+  }
+`;
+
+const Title = styled('div')`
+  font-size: ${p => p.theme.fontSizeSmall};
+  text-transform: uppercase;
+  color: ${p => p.theme.gray300};
+  display: grid;
+  grid-template-columns: max-content max-content;
+  align-items: center;
+  /* to be consistent with the feature badge size */
+  line-height: 20px;
+`;
+
+const Description = styled(TextBlock)`
+  margin: ${space(1)} 0 0 0;
+`;
+
+const Action = styled('div')`
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+`;
+
+const Sentaur = styled('img')`
+  display: none;
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    display: block;
+    height: 8.563rem;
+    position: absolute;
+    bottom: 0;
+    right: 6.608rem;
+    object-fit: cover;
+    z-index: 1;
+  }
+`;
+
+const Background = styled('img')`
+  display: none;
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    display: block;
+    position: absolute;
+    top: 0;
+    right: 0;
+    object-fit: cover;
+    max-width: 100%;
+    height: 100%;
+    border-radius: ${p => p.theme.panelBorderRadius};
+  }
+`;
+
+const Stars = styled('img')`
+  display: none;
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    display: block;
+    height: 8.563rem;
+    position: absolute;
+    right: -1rem;
+    bottom: -0.125rem;
+    object-fit: cover;
+    /* workaround to remove a  extra svg on the bottom right */
+    border-radius: ${p => p.theme.panelBorderRadius};
+  }
+`;
+
+const ViewSuggestionButton = styled(Button)`
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    position: absolute;
+    right: 1rem;
+    top: 1.5rem;
+  }
+`;

+ 14 - 0
static/app/components/events/aiSuggestedSolution/experimentalFeatureBadge.tsx

@@ -0,0 +1,14 @@
+import FeatureBadge from 'sentry/components/featureBadge';
+import {t} from 'sentry/locale';
+
+export function ExperimentalFeatureBadge() {
+  return (
+    <FeatureBadge
+      size="sm"
+      type="experimental"
+      title={t(
+        'This is an OpenAI generated solution that suggests a fix for this issue. Be aware that this may not be accurate.'
+      )}
+    />
+  );
+}

+ 114 - 0
static/app/components/events/aiSuggestedSolution/index.tsx

@@ -0,0 +1,114 @@
+import {useState} from 'react';
+import styled from '@emotion/styled';
+
+import {Button} from 'sentry/components/button';
+import {EventDataSection} from 'sentry/components/events/eventDataSection';
+import ExternalLink from 'sentry/components/links/externalLink';
+import {t, tct} from 'sentry/locale';
+import ConfigStore from 'sentry/stores/configStore';
+import {useLegacyStore} from 'sentry/stores/useLegacyStore';
+import {Event, Project} from 'sentry/types';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getAnalyticsDataForEvent} from 'sentry/utils/events';
+import useOrganization from 'sentry/utils/useOrganization';
+
+import {Banner} from './banner';
+import {Suggestion} from './suggestion';
+
+type Props = {
+  event: Event;
+  projectSlug: Project['slug'];
+};
+
+export function AiSuggestedSolution({projectSlug, event}: Props) {
+  const organization = useOrganization();
+  const config = useLegacyStore(ConfigStore);
+  const [showDetails, setShowDetails] = useState(true);
+  const [openSuggestion, setOpenSuggestion] = useState(false);
+
+  if (!organization.features.includes('open-ai-suggestion-new-design')) {
+    return null;
+  }
+
+  const darkMode = config.theme === 'dark';
+
+  return (
+    <StyledEventDataSection
+      type="ai-suggested-solution"
+      guideTarget="ai-suggested-solution"
+      title={t('Resources and Maybe Solutions')}
+      help={tct(
+        'This is an OpenAI generated solution that suggests a fix for this issue. Be aware that this may not be accurate. [learnMore:Learn more]',
+        {
+          learnMore: (
+            <ExternalLink href="https://docs.sentry.io/product/issues/issue-details/suggested-fix/" />
+          ),
+        }
+      )}
+      isHelpHoverable
+      actions={
+        <ToggleButton
+          onClick={() => {
+            setShowDetails(!showDetails);
+          }}
+          priority="link"
+        >
+          {showDetails ? t('Hide Details') : t('Show Details')}
+        </ToggleButton>
+      }
+    >
+      {showDetails ? (
+        !openSuggestion ? (
+          <Banner
+            darkMode={darkMode}
+            onViewSuggestion={() => {
+              trackAdvancedAnalyticsEvent(
+                'ai_suggested_solution.view_suggestion_button_clicked',
+                {
+                  organization,
+                  project_id: event.projectID,
+                  group_id: event.groupID,
+                  ...getAnalyticsDataForEvent(event),
+                }
+              );
+              setOpenSuggestion(true);
+            }}
+          />
+        ) : (
+          <Suggestion
+            darkMode={darkMode}
+            projectSlug={projectSlug}
+            event={event}
+            onHideSuggestion={() => {
+              trackAdvancedAnalyticsEvent(
+                'ai_suggested_solution.hide_suggestion_button_clicked',
+                {
+                  organization,
+                  project_id: event.projectID,
+                  group_id: event.groupID,
+                  ...getAnalyticsDataForEvent(event),
+                }
+              );
+              setOpenSuggestion(false);
+            }}
+          />
+        )
+      ) : null}
+    </StyledEventDataSection>
+  );
+}
+
+const ToggleButton = styled(Button)`
+  font-weight: 700;
+  color: ${p => p.theme.subText};
+  &:hover,
+  &:focus {
+    color: ${p => p.theme.textColor};
+  }
+`;
+
+const StyledEventDataSection = styled(EventDataSection)`
+  > *:first-child {
+    z-index: 1;
+  }
+`;

BIN
static/app/components/events/aiSuggestedSolution/suggestion-wheel-of-fortune-dark-mode.mp4


BIN
static/app/components/events/aiSuggestedSolution/suggestion-wheel-of-fortune-light-mode.mp4


+ 321 - 0
static/app/components/events/aiSuggestedSolution/suggestion.tsx

@@ -0,0 +1,321 @@
+import {useCallback} from 'react';
+import {InjectedRouter} from 'react-router';
+import {css} from '@emotion/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, PanelBody, PanelFooter, PanelHeader} from 'sentry/components/panels';
+import {IconFile, IconFlag, IconHappy, IconMeh, IconSad} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {Event, Project} from 'sentry/types';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getAnalyticsDataForEvent} from 'sentry/utils/events';
+import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
+import marked from 'sentry/utils/marked';
+import {useApiQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+import useRouter from 'sentry/utils/useRouter';
+
+import {ExperimentalFeatureBadge} from './experimentalFeatureBadge';
+import suggestionWheelOfFortuneDarkMode from './suggestion-wheel-of-fortune-dark-mode.mp4';
+import suggestionWheelOfFortuneLightMode from './suggestion-wheel-of-fortune-light-mode.mp4';
+import {SuggestionLoaderMessage} from './suggestionLoaderMessage';
+import {useOpenAISuggestionLocalStorage} from './useOpenAISuggestionLocalStorage';
+
+type Props = {
+  darkMode: boolean;
+  event: Event;
+  onHideSuggestion: () => void;
+  projectSlug: Project['slug'];
+};
+
+function ErrorDescription({
+  restriction,
+  organizationSlug,
+  router,
+  onRefetch,
+  onSetIndividualConsent,
+}: {
+  onRefetch: () => void;
+  onSetIndividualConsent: (consent: boolean) => void;
+  organizationSlug: string;
+  router: InjectedRouter;
+  restriction?: 'subprocessor' | 'individual_consent';
+}) {
+  if (restriction === 'subprocessor') {
+    return (
+      <EmptyState
+        icon={<IconFile size="xl" />}
+        title={t('OpenAI Subprocessor Acknowledgment')}
+        description={t(
+          'In order to use this feature, your organization needs to accept the OpenAI Subprocessor Acknowledgment.'
+        )}
+        action={
+          <ButtonBar gap={2}>
+            <Button
+              to={{
+                pathname: router.location.pathname,
+                query: {...router.location.query, showSuggestedFix: undefined},
+              }}
+            >
+              {t('Dismiss')}
+            </Button>
+            <Button priority="primary" to={`/settings/${organizationSlug}/legal/`}>
+              {t('Accept in Settings')}
+            </Button>
+          </ButtonBar>
+        }
+      />
+    );
+  }
+
+  if (restriction === 'individual_consent') {
+    const activeSuperUser = isActiveSuperuser();
+    return (
+      <EmptyState
+        icon={<IconFlag size="xl" />}
+        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={
+          <ButtonBar gap={2}>
+            <Button
+              to={{
+                pathname: router.location.pathname,
+                query: {...router.location.query, showSuggestedFix: undefined},
+              }}
+            >
+              {t('Dismiss')}
+            </Button>
+            <Button
+              priority="primary"
+              onClick={() => {
+                onSetIndividualConsent(true);
+                onRefetch();
+              }}
+              disabled={activeSuperUser}
+              title={
+                activeSuperUser ? t("Superusers can't consent to policies") : undefined
+              }
+            >
+              {t('Confirm')}
+            </Button>
+          </ButtonBar>
+        }
+      />
+    );
+  }
+
+  return <SuggestionLoadingError onRetry={onRefetch} />;
+}
+
+export function Suggestion({onHideSuggestion, projectSlug, event, darkMode}: Props) {
+  const organization = useOrganization();
+  const router = useRouter();
+  const [individualConsent, setIndividualConsent] = useOpenAISuggestionLocalStorage();
+
+  const {
+    data,
+    isLoading: dataIsLoading,
+    isError: dataIsError,
+    refetch: dataRefetch,
+    error,
+  } = useApiQuery<{suggestion: string}>(
+    [
+      `/projects/${organization.slug}/${projectSlug}/events/${event.eventID}/ai-fix-suggest/`,
+      {query: {consent: individualConsent ? 'yes' : undefined}},
+    ],
+    {
+      staleTime: Infinity,
+      retry: false,
+    }
+  );
+
+  const handleFeedbackClick = useCallback(() => {
+    addSuccessMessage('Thank you for your feedback!');
+    onHideSuggestion();
+  }, [onHideSuggestion]);
+
+  return (
+    <Wrapper darkMode={darkMode}>
+      <Header>
+        <div>
+          {t('AI Solution')}
+          <ExperimentalFeatureBadge />
+        </div>
+        <Button size="xs" onClick={onHideSuggestion}>
+          {t('Hide Suggestion')}
+        </Button>
+      </Header>
+      <PanelBody>
+        {dataIsLoading ? (
+          <LoaderWrapper>
+            <video height="309px" autoPlay loop muted>
+              <source
+                src={
+                  darkMode
+                    ? suggestionWheelOfFortuneDarkMode
+                    : suggestionWheelOfFortuneLightMode
+                }
+                type="video/mp4"
+              />
+              {t('Your browser does not support the video tag.')}
+            </video>
+            <SuggestionLoaderMessage />
+          </LoaderWrapper>
+        ) : dataIsError ? (
+          <ErrorDescription
+            onRefetch={dataRefetch}
+            organizationSlug={organization.slug}
+            router={router}
+            onSetIndividualConsent={setIndividualConsent}
+            restriction={error?.responseJSON?.restriction}
+          />
+        ) : (
+          <Content
+            dangerouslySetInnerHTML={{
+              __html: marked(data.suggestion, {
+                gfm: true,
+                breaks: true,
+              }),
+            }}
+          />
+        )}
+      </PanelBody>
+      <PanelFooter>
+        <Feedback>
+          <strong>{t('Was this helpful?')}</strong>
+          <ButtonBar gap={1}>
+            <Button
+              icon={<IconSad color="red300" size="xs" />}
+              size="xs"
+              onClick={() => {
+                trackAdvancedAnalyticsEvent(
+                  'ai_suggested_solution.feedback_helpful_nope_button_clicked',
+                  {
+                    organization,
+                    project_id: event.projectID,
+                    group_id: event.groupID,
+                    ...getAnalyticsDataForEvent(event),
+                  }
+                );
+
+                handleFeedbackClick();
+              }}
+            >
+              {t('Nope')}
+            </Button>
+            <Button
+              icon={<IconMeh color="yellow300" size="xs" />}
+              size="xs"
+              onClick={() => {
+                trackAdvancedAnalyticsEvent(
+                  'ai_suggested_solution.feedback_helpful_kinda_button_clicked',
+                  {
+                    organization,
+                    project_id: event.projectID,
+                    group_id: event.groupID,
+                    ...getAnalyticsDataForEvent(event),
+                  }
+                );
+
+                handleFeedbackClick();
+              }}
+            >
+              {t('Kinda')}
+            </Button>
+            <Button
+              icon={<IconHappy color="green300" size="xs" />}
+              size="xs"
+              onClick={() => {
+                trackAdvancedAnalyticsEvent(
+                  'ai_suggested_solution.feedback_helpful_yes_button_clicked',
+                  {
+                    organization,
+                    project_id: event.projectID,
+                    group_id: event.groupID,
+                    ...getAnalyticsDataForEvent(event),
+                  }
+                );
+
+                handleFeedbackClick();
+              }}
+            >
+              {t('Yes, Surprisingly\u2026')}
+            </Button>
+          </ButtonBar>
+        </Feedback>
+      </PanelFooter>
+    </Wrapper>
+  );
+}
+
+const Wrapper = styled(Panel)<{darkMode: boolean}>`
+  ${p =>
+    p.darkMode &&
+    css`
+      /* hack until we update the background of the loading video */
+      background: #2b2031;
+    `}
+`;
+
+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 this panel */
+  padding-top: ${space(2)};
+  padding-bottom: ${space(2)};
+`;
+
+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 EmptyState = styled(EmptyMessage)`
+  /* This is just to be consitent with other */
+  /* padding-top and padding-bottom we are using in this panel */
+  padding-top: ${space(2)};
+  padding-bottom: ${space(2)};
+`;

Some files were not shown because too many files changed in this diff