solutionsSection.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import {useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {FeatureBadge} from 'sentry/components/core/badge/featureBadge';
  5. import {GroupSummary} from 'sentry/components/group/groupSummary';
  6. import {GroupSummaryWithAutofix} from 'sentry/components/group/groupSummaryWithAutofix';
  7. import Placeholder from 'sentry/components/placeholder';
  8. import {IconMegaphone} from 'sentry/icons';
  9. import {t, tct} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Event} from 'sentry/types/event';
  12. import type {Group} from 'sentry/types/group';
  13. import type {Project} from 'sentry/types/project';
  14. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  15. import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
  16. import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
  17. import {SidebarFoldSection} from 'sentry/views/issueDetails/streamline/foldSection';
  18. import {useAiConfig} from 'sentry/views/issueDetails/streamline/hooks/useAiConfig';
  19. import Resources from 'sentry/views/issueDetails/streamline/sidebar/resources';
  20. import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
  21. import {SolutionsSectionCtaButton} from './solutionsSectionCtaButton';
  22. function SolutionsHubFeedbackButton({hidden}: {hidden: boolean}) {
  23. const openFeedbackForm = useFeedbackForm();
  24. if (hidden) {
  25. return null;
  26. }
  27. return (
  28. <Button
  29. aria-label={t('Give feedback on the solutions hub')}
  30. icon={<IconMegaphone />}
  31. size="xs"
  32. onClick={() =>
  33. openFeedbackForm?.({
  34. messagePlaceholder: t('How can we make Issue Summary better for you?'),
  35. tags: {
  36. ['feedback.source']: 'issue_details_ai_autofix',
  37. ['feedback.owner']: 'ml-ai',
  38. },
  39. })
  40. }
  41. />
  42. );
  43. }
  44. function SolutionsSectionContent({
  45. group,
  46. project,
  47. event,
  48. }: {
  49. event: Event | undefined;
  50. group: Group;
  51. project: Project;
  52. }) {
  53. const aiConfig = useAiConfig(group, event, project);
  54. if (!event) {
  55. return <Placeholder height="160px" />;
  56. }
  57. if (aiConfig.hasSummary) {
  58. if (aiConfig.hasAutofix) {
  59. return (
  60. <Summary>
  61. <GroupSummaryWithAutofix
  62. group={group}
  63. event={event}
  64. project={project}
  65. preview
  66. />
  67. </Summary>
  68. );
  69. }
  70. return (
  71. <Summary>
  72. <GroupSummary group={group} event={event} project={project} preview />
  73. </Summary>
  74. );
  75. }
  76. return null;
  77. }
  78. export default function SolutionsSection({
  79. group,
  80. project,
  81. event,
  82. }: {
  83. event: Event | undefined;
  84. group: Group;
  85. project: Project;
  86. }) {
  87. const hasStreamlinedUI = useHasStreamlinedUI();
  88. // We don't use this on the streamlined UI, since the section folds.
  89. const [isExpanded, setIsExpanded] = useState(false);
  90. const aiConfig = useAiConfig(group, event, project);
  91. const issueTypeConfig = getConfigForIssueType(group, project);
  92. const showCtaButton =
  93. aiConfig.needsGenAIConsent ||
  94. aiConfig.hasAutofix ||
  95. (aiConfig.hasSummary && aiConfig.hasResources);
  96. const titleComponent = (
  97. <HeaderContainer>
  98. {t('Solutions Hub')}
  99. {aiConfig.hasSummary && (
  100. <StyledFeatureBadge
  101. type="beta"
  102. tooltipProps={{
  103. title: tct(
  104. 'This feature is in beta. Try it out and let us know your feedback at [email:autofix@sentry.io].',
  105. {
  106. email: <a href="mailto:autofix@sentry.io" />,
  107. }
  108. ),
  109. }}
  110. />
  111. )}
  112. </HeaderContainer>
  113. );
  114. return (
  115. <SidebarFoldSection
  116. title={titleComponent}
  117. sectionKey={SectionKey.SOLUTIONS_HUB}
  118. actions={<SolutionsHubFeedbackButton hidden={!aiConfig.hasSummary} />}
  119. preventCollapse={!hasStreamlinedUI}
  120. >
  121. <SolutionsSectionContainer>
  122. {aiConfig.needsGenAIConsent ? (
  123. <Summary>
  124. {t('Explore potential root causes and solutions with Autofix.')}
  125. </Summary>
  126. ) : aiConfig.hasAutofix || aiConfig.hasSummary ? (
  127. <SolutionsSectionContent group={group} project={project} event={event} />
  128. ) : issueTypeConfig.resources ? (
  129. <ResourcesWrapper isExpanded={hasStreamlinedUI ? true : isExpanded}>
  130. <ResourcesContent isExpanded={hasStreamlinedUI ? true : isExpanded}>
  131. <Resources
  132. configResources={issueTypeConfig.resources}
  133. eventPlatform={event?.platform}
  134. group={group}
  135. />
  136. </ResourcesContent>
  137. {!hasStreamlinedUI && (
  138. <ExpandButton onClick={() => setIsExpanded(!isExpanded)} size="zero">
  139. {isExpanded ? t('SHOW LESS') : t('READ MORE')}
  140. </ExpandButton>
  141. )}
  142. </ResourcesWrapper>
  143. ) : null}
  144. {event && showCtaButton && (
  145. <SolutionsSectionCtaButton
  146. aiConfig={aiConfig}
  147. event={event}
  148. group={group}
  149. project={project}
  150. hasStreamlinedUI={hasStreamlinedUI}
  151. />
  152. )}
  153. </SolutionsSectionContainer>
  154. </SidebarFoldSection>
  155. );
  156. }
  157. const SolutionsSectionContainer = styled('div')`
  158. display: flex;
  159. flex-direction: column;
  160. `;
  161. const Summary = styled('div')`
  162. margin-bottom: ${space(0.5)};
  163. position: relative;
  164. `;
  165. const ResourcesWrapper = styled('div')<{isExpanded: boolean}>`
  166. position: relative;
  167. margin-bottom: ${space(1)};
  168. `;
  169. const ResourcesContent = styled('div')<{isExpanded: boolean}>`
  170. position: relative;
  171. max-height: ${p => (p.isExpanded ? 'none' : '68px')};
  172. overflow: hidden;
  173. padding-bottom: ${p => (p.isExpanded ? space(2) : 0)};
  174. ${p =>
  175. !p.isExpanded &&
  176. `
  177. &::after {
  178. content: '';
  179. position: absolute;
  180. bottom: 0;
  181. left: 0;
  182. right: 0;
  183. height: 40px;
  184. background: linear-gradient(transparent, ${p.theme.background});
  185. }
  186. `}
  187. `;
  188. const ExpandButton = styled(Button)`
  189. position: absolute;
  190. bottom: -${space(1)};
  191. right: 0;
  192. font-size: ${p => p.theme.fontSizeExtraSmall};
  193. color: ${p => p.theme.gray300};
  194. border: none;
  195. box-shadow: none;
  196. &:hover {
  197. color: ${p => p.theme.gray400};
  198. }
  199. `;
  200. const HeaderContainer = styled('div')`
  201. font-size: ${p => p.theme.fontSizeMedium};
  202. display: flex;
  203. align-items: center;
  204. gap: ${space(0.25)};
  205. `;
  206. const StyledFeatureBadge = styled(FeatureBadge)`
  207. margin-bottom: 3px;
  208. `;