openAIFixSuggestionPanel.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {Button} from 'sentry/components/button';
  5. import FeatureBadge from 'sentry/components/featureBadge';
  6. import {feedbackClient} from 'sentry/components/featureFeedback/feedbackModal';
  7. import LoadingError from 'sentry/components/loadingError';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {Panel, PanelBody, PanelFooter, PanelHeader} from 'sentry/components/panels';
  10. import {IconHappy, IconMeh, IconSad} from 'sentry/icons';
  11. import {IconChevron} from 'sentry/icons/iconChevron';
  12. import {t} from 'sentry/locale';
  13. import ConfigStore from 'sentry/stores/configStore';
  14. import {space} from 'sentry/styles/space';
  15. import marked from 'sentry/utils/marked';
  16. import {useQuery} from 'sentry/utils/queryClient';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import useRouter from 'sentry/utils/useRouter';
  19. import {useOpenAISuggestionLocalStorage} from 'sentry/views/issueDetails/openAIFixSuggestion/useOpenAISuggestionLocalStorage';
  20. import {experimentalFeatureTooltipDesc} from 'sentry/views/issueDetails/openAIFixSuggestion/utils';
  21. enum OpenAISatisfactoryLevel {
  22. HELPED = 'helped',
  23. PARTIALLY_HELPED = 'partially_helped',
  24. DID_NOT_HELP = 'did_not_help',
  25. }
  26. const openAIFeedback = {
  27. [OpenAISatisfactoryLevel.HELPED]: t('It helped me solve the issue'),
  28. [OpenAISatisfactoryLevel.PARTIALLY_HELPED]: t('It partially helped me solve the issue'),
  29. [OpenAISatisfactoryLevel.DID_NOT_HELP]: t('It did not help me solve the issue'),
  30. };
  31. type Props = {
  32. eventID: string;
  33. projectSlug: string;
  34. };
  35. export function OpenAIFixSuggestionPanel({eventID, projectSlug}: Props) {
  36. const user = ConfigStore.get('user');
  37. const organization = useOrganization();
  38. const router = useRouter();
  39. const showSuggestedFix = !!router.location.query.showSuggestedFix;
  40. const hasSignedDPA = false;
  41. const [agreedForwardDataToOpenAI] = useOpenAISuggestionLocalStorage();
  42. const [expandedSuggestedFix, setExpandedSuggestedFix] = useState(showSuggestedFix);
  43. useEffect(() => {
  44. setExpandedSuggestedFix(!!router.location.query.showSuggestedFix);
  45. }, [router.location.query.showSuggestedFix]);
  46. const handleOpenAISuggestionFeedback = useCallback(
  47. (openAISatisfactoryLevel: OpenAISatisfactoryLevel) => {
  48. feedbackClient.captureEvent({
  49. request: {
  50. url: window.location.href, // gives the full url (origin + pathname)
  51. },
  52. tags: {
  53. featureName: 'open-ai-suggestion',
  54. },
  55. user,
  56. level: 'info',
  57. message: `OpenAI Suggestion Feedback - ${openAIFeedback[openAISatisfactoryLevel]}`,
  58. });
  59. addSuccessMessage('Thank you for your feedback!');
  60. setExpandedSuggestedFix(false);
  61. },
  62. [user]
  63. );
  64. const {
  65. data,
  66. isLoading: dataIsLoading,
  67. isError: dataIsError,
  68. refetch: dataRefetch,
  69. } = useQuery<{suggestion: string}>(
  70. [`/projects/${organization.slug}/${projectSlug}/events/${eventID}/ai-fix-suggest/`],
  71. {
  72. staleTime: Infinity,
  73. enabled: (hasSignedDPA || agreedForwardDataToOpenAI) && showSuggestedFix,
  74. }
  75. );
  76. if (!organization.features.includes('open-ai-suggestion')) {
  77. return null;
  78. }
  79. if (!showSuggestedFix) {
  80. return null;
  81. }
  82. return (
  83. <FixSuggestionPanel>
  84. <FixSuggestionPanelHeader isExpanded={expandedSuggestedFix}>
  85. <HeaderDescription>
  86. {t('Suggested Fix')}
  87. <FeatureBadgeNotUppercase
  88. type="experimental"
  89. title={experimentalFeatureTooltipDesc}
  90. />
  91. </HeaderDescription>
  92. <Button
  93. size="xs"
  94. title={t('Toggle Suggested Fix Panel')}
  95. aria-label={t('Toggle Suggested Fix Panel')}
  96. icon={<IconChevron direction={expandedSuggestedFix ? 'up' : 'down'} />}
  97. onClick={() => setExpandedSuggestedFix(!expandedSuggestedFix)}
  98. />
  99. </FixSuggestionPanelHeader>
  100. {expandedSuggestedFix && (
  101. <Fragment>
  102. <PanelBody withPadding>
  103. {dataIsLoading ? (
  104. <LoadingIndicator />
  105. ) : dataIsError ? (
  106. <LoadingErrorWithoutMarginBottom onRetry={dataRefetch} />
  107. ) : (
  108. <div
  109. dangerouslySetInnerHTML={{
  110. __html: marked(data.suggestion, {
  111. gfm: true,
  112. breaks: true,
  113. }),
  114. }}
  115. />
  116. )}
  117. </PanelBody>
  118. {!dataIsLoading && !dataIsError && (
  119. <PanelFooter>
  120. <Feedback>
  121. <strong>{t('Was this helpful?')}</strong>
  122. <div>
  123. <Button
  124. title={openAIFeedback[OpenAISatisfactoryLevel.DID_NOT_HELP]}
  125. aria-label={openAIFeedback[OpenAISatisfactoryLevel.DID_NOT_HELP]}
  126. icon={<IconSad color="red300" />}
  127. size="xs"
  128. borderless
  129. onClick={() =>
  130. handleOpenAISuggestionFeedback(OpenAISatisfactoryLevel.DID_NOT_HELP)
  131. }
  132. />
  133. <Button
  134. title={openAIFeedback[OpenAISatisfactoryLevel.PARTIALLY_HELPED]}
  135. aria-label={openAIFeedback[OpenAISatisfactoryLevel.PARTIALLY_HELPED]}
  136. icon={<IconMeh color="yellow300" />}
  137. size="xs"
  138. borderless
  139. onClick={() =>
  140. handleOpenAISuggestionFeedback(
  141. OpenAISatisfactoryLevel.PARTIALLY_HELPED
  142. )
  143. }
  144. />
  145. <Button
  146. title={openAIFeedback[OpenAISatisfactoryLevel.HELPED]}
  147. aria-label={openAIFeedback[OpenAISatisfactoryLevel.HELPED]}
  148. icon={<IconHappy color="green300" />}
  149. size="xs"
  150. borderless
  151. onClick={() =>
  152. handleOpenAISuggestionFeedback(OpenAISatisfactoryLevel.HELPED)
  153. }
  154. />
  155. </div>
  156. </Feedback>
  157. </PanelFooter>
  158. )}
  159. </Fragment>
  160. )}
  161. </FixSuggestionPanel>
  162. );
  163. }
  164. const FixSuggestionPanel = styled(Panel)`
  165. margin-top: ${space(1.5)};
  166. margin-bottom: 0;
  167. overflow: hidden;
  168. `;
  169. const FixSuggestionPanelHeader = styled(PanelHeader)<{isExpanded: boolean}>`
  170. border-bottom: ${p => (p.isExpanded ? 'inherit' : 'none')};
  171. `;
  172. const HeaderDescription = styled('div')`
  173. display: flex;
  174. align-items: center;
  175. `;
  176. const Feedback = styled('div')`
  177. padding: ${space(1)} ${space(2)};
  178. display: grid;
  179. grid-template-columns: 1fr max-content max-content max-content;
  180. align-items: center;
  181. text-align: right;
  182. gap: ${space(1)};
  183. `;
  184. const LoadingErrorWithoutMarginBottom = styled(LoadingError)`
  185. margin-bottom: 0;
  186. `;
  187. const FeatureBadgeNotUppercase = styled(FeatureBadge)`
  188. text-transform: capitalize;
  189. `;