solutionsSectionCtaButton.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import {useEffect, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {Button} from 'sentry/components/button';
  5. import {Chevron} from 'sentry/components/chevron';
  6. import {
  7. AutofixStatus,
  8. type AutofixStep,
  9. AutofixStepType,
  10. } from 'sentry/components/events/autofix/types';
  11. import {useAiAutofix, useAutofixData} from 'sentry/components/events/autofix/useAutofix';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import Placeholder from 'sentry/components/placeholder';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Event} from 'sentry/types/event';
  17. import type {Group} from 'sentry/types/group';
  18. import type {Project} from 'sentry/types/project';
  19. import {useOpenSolutionsDrawer} from 'sentry/views/issueDetails/streamline/sidebar/solutionsHubDrawer';
  20. interface Props {
  21. aiConfig: {
  22. hasAutofix: boolean | null | undefined;
  23. hasResources: boolean;
  24. hasSummary: boolean;
  25. isAutofixSetupLoading: boolean;
  26. needsGenAIConsent: boolean;
  27. };
  28. event: Event;
  29. group: Group;
  30. hasStreamlinedUI: boolean;
  31. project: Project;
  32. }
  33. export function SolutionsSectionCtaButton({
  34. aiConfig,
  35. event,
  36. group,
  37. project,
  38. hasStreamlinedUI,
  39. }: Props) {
  40. const openButtonRef = useRef<HTMLButtonElement>(null);
  41. const {isPending: isAutofixPending} = useAutofixData({groupId: group.id});
  42. const {autofixData} = useAiAutofix(group, event);
  43. const openSolutionsDrawer = useOpenSolutionsDrawer(
  44. group,
  45. project,
  46. event,
  47. openButtonRef
  48. );
  49. const isDrawerOpenRef = useRef(false);
  50. // Keep track of previous steps to detect state transitions and notify the user
  51. const prevStepsRef = useRef<AutofixStep[]>();
  52. useEffect(() => {
  53. if (isDrawerOpenRef.current) {
  54. return;
  55. }
  56. if (!autofixData?.steps || !prevStepsRef.current) {
  57. prevStepsRef.current = autofixData?.steps;
  58. return;
  59. }
  60. const prevSteps = prevStepsRef.current;
  61. const currentSteps = autofixData.steps;
  62. // Find the most recent step
  63. const processingStep = currentSteps.findLast(
  64. step => step.type === AutofixStepType.DEFAULT
  65. );
  66. if (processingStep && processingStep.status === AutofixStatus.COMPLETED) {
  67. // Check if this is a new completion (wasn't completed in previous state)
  68. const prevProcessingStep = prevSteps.findLast(
  69. step => step.type === AutofixStepType.DEFAULT
  70. );
  71. if (prevProcessingStep && prevProcessingStep.status !== AutofixStatus.COMPLETED) {
  72. if (currentSteps.find(step => step.type === AutofixStepType.CHANGES)) {
  73. addSuccessMessage(t('Autofix has finished coding.'));
  74. } else if (currentSteps.find(step => step.type === AutofixStepType.SOLUTION)) {
  75. addSuccessMessage(t('Autofix has found a solution.'));
  76. } else if (
  77. currentSteps.find(step => step.type === AutofixStepType.ROOT_CAUSE_ANALYSIS)
  78. ) {
  79. addSuccessMessage(t('Autofix has found the root cause.'));
  80. }
  81. }
  82. }
  83. prevStepsRef.current = autofixData?.steps;
  84. }, [autofixData?.steps]);
  85. // Update drawer state when opening
  86. const handleOpenDrawer = () => {
  87. isDrawerOpenRef.current = true;
  88. openSolutionsDrawer();
  89. };
  90. // Listen for drawer close events
  91. useEffect(() => {
  92. const handleClickOutside = () => {
  93. isDrawerOpenRef.current = false;
  94. };
  95. document.addEventListener('click', handleClickOutside);
  96. return () => {
  97. document.removeEventListener('click', handleClickOutside);
  98. };
  99. }, []);
  100. const showCtaButton =
  101. aiConfig.needsGenAIConsent ||
  102. aiConfig.hasAutofix ||
  103. (aiConfig.hasSummary && aiConfig.hasResources);
  104. const isButtonLoading = aiConfig.isAutofixSetupLoading || isAutofixPending;
  105. const lastStep = autofixData?.steps?.[autofixData.steps.length - 1];
  106. const isAutofixInProgress = lastStep?.status === AutofixStatus.PROCESSING;
  107. const isAutofixCompleted = lastStep?.status === AutofixStatus.COMPLETED;
  108. const isAutofixWaitingForUser =
  109. autofixData?.status === AutofixStatus.WAITING_FOR_USER_RESPONSE;
  110. const hasStepType = (type: AutofixStepType) =>
  111. autofixData?.steps?.some(step => step.type === type);
  112. const getButtonText = () => {
  113. if (aiConfig.needsGenAIConsent) {
  114. return t('Set Up Autofix');
  115. }
  116. if (!aiConfig.hasAutofix) {
  117. return t('Open Resources');
  118. }
  119. if (!lastStep) {
  120. return t('Find Root Cause');
  121. }
  122. if (isAutofixWaitingForUser) {
  123. return t('Waiting for Your Input');
  124. }
  125. if (isAutofixInProgress) {
  126. if (!hasStepType(AutofixStepType.ROOT_CAUSE_ANALYSIS)) {
  127. return t('Finding Root Cause');
  128. }
  129. if (!hasStepType(AutofixStepType.SOLUTION)) {
  130. return t('Finding Solution');
  131. }
  132. if (!hasStepType(AutofixStepType.CHANGES)) {
  133. return t('Writing Code');
  134. }
  135. }
  136. if (isAutofixCompleted) {
  137. if (lastStep.type === AutofixStepType.ROOT_CAUSE_ANALYSIS) {
  138. return t('View Root Cause');
  139. }
  140. if (lastStep.type === AutofixStepType.SOLUTION) {
  141. return t('View Solution');
  142. }
  143. if (lastStep.type === AutofixStepType.CHANGES) {
  144. return t('View Code Changes');
  145. }
  146. }
  147. return t('Find Root Cause');
  148. };
  149. if (isButtonLoading) {
  150. return <ButtonPlaceholder />;
  151. }
  152. if (!showCtaButton) {
  153. return null;
  154. }
  155. return (
  156. <StyledButton
  157. ref={openButtonRef}
  158. onClick={handleOpenDrawer}
  159. analyticsEventKey="issue_details.solutions_hub_opened"
  160. analyticsEventName="Issue Details: Solutions Hub Opened"
  161. analyticsParams={{
  162. has_streamlined_ui: hasStreamlinedUI,
  163. }}
  164. >
  165. {getButtonText()}
  166. <ChevronContainer>
  167. {isAutofixInProgress ? (
  168. <StyledLoadingIndicator mini size={14} hideMessage />
  169. ) : (
  170. <Chevron direction="right" size="large" />
  171. )}
  172. </ChevronContainer>
  173. </StyledButton>
  174. );
  175. }
  176. const StyledButton = styled(Button)`
  177. margin-top: ${space(1)};
  178. width: 100%;
  179. background: ${p => p.theme.background}
  180. linear-gradient(to right, ${p => p.theme.background}, ${p => p.theme.pink400}20);
  181. color: ${p => p.theme.pink400};
  182. `;
  183. const ChevronContainer = styled('div')`
  184. margin-left: ${space(0.5)};
  185. height: 16px;
  186. width: 16px;
  187. display: flex;
  188. align-items: center;
  189. justify-content: center;
  190. `;
  191. const StyledLoadingIndicator = styled(LoadingIndicator)`
  192. position: relative;
  193. top: 5px;
  194. color: ${p => p.theme.pink400};
  195. .loading-indicator {
  196. border-color: ${p => p.theme.pink100};
  197. border-left-color: ${p => p.theme.pink400};
  198. }
  199. `;
  200. const ButtonPlaceholder = styled(Placeholder)`
  201. width: 100%;
  202. height: 38px;
  203. border-radius: ${p => p.theme.borderRadius};
  204. margin-top: ${space(1)};
  205. `;