solutionsHubDrawer.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import starImage from 'sentry-images/spot/banner-star.svg';
  4. import {SeerIcon} from 'sentry/components/ai/SeerIcon';
  5. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  6. import FeatureBadge from 'sentry/components/badge/featureBadge';
  7. import {Breadcrumbs as NavigationBreadcrumbs} from 'sentry/components/breadcrumbs';
  8. import {Button} from 'sentry/components/button';
  9. import ButtonBar from 'sentry/components/buttonBar';
  10. import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback';
  11. import {AutofixSetupContent} from 'sentry/components/events/autofix/autofixSetupModal';
  12. import {AutofixSteps} from 'sentry/components/events/autofix/autofixSteps';
  13. import {useAiAutofix} from 'sentry/components/events/autofix/useAutofix';
  14. import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
  15. import {GroupSummary, useGroupSummary} from 'sentry/components/group/groupSummary';
  16. import HookOrDefault from 'sentry/components/hookOrDefault';
  17. import Input from 'sentry/components/input';
  18. import LoadingIndicator from 'sentry/components/loadingIndicator';
  19. import {IconDocs} from 'sentry/icons';
  20. import {t, tct} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import type {Event} from 'sentry/types/event';
  23. import type {Group} from 'sentry/types/group';
  24. import type {Project} from 'sentry/types/project';
  25. import {getShortEventId} from 'sentry/utils/events';
  26. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  27. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  28. import {MIN_NAV_HEIGHT} from 'sentry/views/issueDetails/streamline/eventTitle';
  29. import Resources from 'sentry/views/issueDetails/streamline/resources';
  30. import {useAiConfig} from 'sentry/views/issueDetails/streamline/useAiConfig';
  31. interface AutofixStartBoxProps {
  32. groupId: string;
  33. onSend: (message: string) => void;
  34. }
  35. function AutofixStartBox({onSend, groupId}: AutofixStartBoxProps) {
  36. const [message, setMessage] = useState('');
  37. const stars = [
  38. {size: 10, left: 20, top: 5, rotation: 30, opacity: 0.15},
  39. {size: 12, left: 50, top: 8, rotation: 45, opacity: 0.2},
  40. {size: 10, left: 80, top: 12, rotation: 15, opacity: 0.2},
  41. {size: 14, left: 15, top: 20, rotation: 60, opacity: 0.3},
  42. {size: 16, left: 45, top: 25, rotation: 30, opacity: 0.35},
  43. {size: 14, left: 75, top: 22, rotation: 45, opacity: 0.3},
  44. {size: 18, left: 25, top: 35, rotation: 20, opacity: 0.4},
  45. {size: 20, left: 60, top: 38, rotation: 50, opacity: 0.45},
  46. {size: 18, left: 85, top: 42, rotation: 35, opacity: 0.4},
  47. {size: 22, left: 15, top: 55, rotation: 25, opacity: 0.5},
  48. {size: 24, left: 40, top: 58, rotation: 40, opacity: 0.55},
  49. {size: 22, left: 70, top: 52, rotation: 30, opacity: 0.5},
  50. {size: 26, left: 30, top: 70, rotation: 35, opacity: 0.65},
  51. {size: 28, left: 50, top: 75, rotation: 45, opacity: 0.7},
  52. {size: 26, left: 80, top: 72, rotation: 25, opacity: 0.7},
  53. ];
  54. const handleSubmit = (e: React.FormEvent) => {
  55. e.preventDefault();
  56. onSend(message);
  57. };
  58. return (
  59. <StartBox>
  60. <StarTrail>
  61. {stars.map((star, i) => (
  62. <TrailStar
  63. key={i}
  64. src={starImage}
  65. style={{
  66. left: `${star.left}%`,
  67. top: `${star.top}%`,
  68. width: `${star.size}px`,
  69. height: `${star.size}px`,
  70. opacity: star.opacity,
  71. transform: `rotate(${star.rotation}deg)`,
  72. }}
  73. />
  74. ))}
  75. </StarTrail>
  76. <ContentContainer>
  77. <HeaderText>Autofix</HeaderText>
  78. <p>Work together with Autofix to find the root cause and fix the issue.</p>
  79. <form onSubmit={handleSubmit}>
  80. <Row>
  81. <Input
  82. type="text"
  83. value={message}
  84. onChange={e => setMessage(e.target.value)}
  85. placeholder={'(Optional) Share helpful context here...'}
  86. />
  87. <ButtonWithStars>
  88. <StarLarge1 src={starImage} />
  89. <StarLarge2 src={starImage} />
  90. <StarLarge3 src={starImage} />
  91. <Button
  92. type="submit"
  93. priority="primary"
  94. analyticsEventKey={
  95. message
  96. ? 'autofix.give_instructions_clicked'
  97. : 'autofix.start_fix_clicked'
  98. }
  99. analyticsEventName={
  100. message
  101. ? 'Autofix: Give Instructions Clicked'
  102. : 'Autofix: Start Fix Clicked'
  103. }
  104. analyticsParams={{group_id: groupId}}
  105. aria-label="Start Autofix"
  106. >
  107. {t('Start Autofix')}
  108. </Button>
  109. </ButtonWithStars>
  110. </Row>
  111. </form>
  112. </ContentContainer>
  113. </StartBox>
  114. );
  115. }
  116. interface SolutionsHubDrawerProps {
  117. event: Event;
  118. group: Group;
  119. project: Project;
  120. }
  121. const AiSetupDataConsent = HookOrDefault({
  122. hookName: 'component:ai-setup-data-consent',
  123. defaultComponent: () => <div data-test-id="ai-setup-data-consent" />,
  124. });
  125. export function SolutionsHubDrawer({group, project, event}: SolutionsHubDrawerProps) {
  126. const {autofixData, triggerAutofix, reset} = useAiAutofix(group, event);
  127. const {
  128. data: summaryData,
  129. isError,
  130. isPending: isSummaryLoading,
  131. } = useGroupSummary(group, event, project);
  132. const aiConfig = useAiConfig(group, event, project);
  133. useRouteAnalyticsParams({
  134. autofix_status: autofixData?.status ?? 'none',
  135. });
  136. const config = getConfigForIssueType(group, project);
  137. return (
  138. <SolutionsDrawerContainer className="solutions-drawer-container">
  139. <SolutionsDrawerHeader>
  140. <NavigationCrumbs
  141. crumbs={[
  142. {
  143. label: (
  144. <CrumbContainer>
  145. <ProjectAvatar project={project} />
  146. <ShortId>{group.shortId}</ShortId>
  147. </CrumbContainer>
  148. ),
  149. },
  150. {label: getShortEventId(event.id)},
  151. {label: t('Solutions Hub')},
  152. ]}
  153. />
  154. </SolutionsDrawerHeader>
  155. <SolutionsDrawerNavigator>
  156. <Header>{t('Solutions Hub')}</Header>
  157. </SolutionsDrawerNavigator>
  158. <SolutionsDrawerBody>
  159. {config.resources && (
  160. <ResourcesContainer>
  161. <ResourcesHeader>
  162. <IconDocs size="md" />
  163. {t('Resources')}
  164. </ResourcesHeader>
  165. <ResourcesBody>
  166. <Resources
  167. eventPlatform={event?.platform}
  168. group={group}
  169. configResources={config.resources}
  170. />
  171. </ResourcesBody>
  172. </ResourcesContainer>
  173. )}
  174. <HeaderText>
  175. <HeaderContainer>
  176. <SeerIcon size="lg" />
  177. {t('Sentry AI')}
  178. <StyledFeatureBadge
  179. type="beta"
  180. title={tct(
  181. 'This feature is in beta. Try it out and let us know your feedback at [email:autofix@sentry.io].',
  182. {
  183. email: <a href="mailto:autofix@sentry.io" />,
  184. }
  185. )}
  186. />
  187. </HeaderContainer>
  188. {autofixData && (
  189. <ButtonBar gap={1}>
  190. <AutofixFeedback />
  191. <Button
  192. size="xs"
  193. onClick={reset}
  194. title={
  195. autofixData.created_at
  196. ? `Last run at ${autofixData.created_at.split('T')[0]}`
  197. : null
  198. }
  199. >
  200. {t('Start Over')}
  201. </Button>
  202. </ButtonBar>
  203. )}
  204. </HeaderText>
  205. {aiConfig.isAutofixSetupLoading ? (
  206. <div data-test-id="ai-setup-loading-indicator">
  207. <LoadingIndicator />
  208. </div>
  209. ) : aiConfig.needsGenAIConsent ? (
  210. <AiSetupDataConsent groupId={group.id} />
  211. ) : (
  212. <Fragment>
  213. {aiConfig.hasSummary && (
  214. <StyledCard>
  215. <GroupSummary
  216. data={summaryData}
  217. isError={isError}
  218. isPending={isSummaryLoading}
  219. />
  220. </StyledCard>
  221. )}
  222. {aiConfig.hasAutofix && (
  223. <Fragment>
  224. {aiConfig.needsAutofixSetup ? (
  225. <AutofixSetupContent groupId={group.id} projectId={project.id} />
  226. ) : !autofixData ? (
  227. <AutofixStartBox onSend={triggerAutofix} groupId={group.id} />
  228. ) : (
  229. <AutofixSteps
  230. data={autofixData}
  231. groupId={group.id}
  232. runId={autofixData.run_id}
  233. onRetry={reset}
  234. />
  235. )}
  236. </Fragment>
  237. )}
  238. </Fragment>
  239. )}
  240. </SolutionsDrawerBody>
  241. </SolutionsDrawerContainer>
  242. );
  243. }
  244. const ResourcesContainer = styled('div')``;
  245. const ResourcesBody = styled('div')`
  246. padding: 0 ${space(2)} ${space(2)} ${space(2)};
  247. border-bottom: 1px solid ${p => p.theme.border};
  248. margin-bottom: ${space(2)};
  249. `;
  250. const Row = styled('div')`
  251. display: flex;
  252. gap: ${space(1)};
  253. `;
  254. const StartBox = styled('div')`
  255. padding: ${space(2)};
  256. position: absolute;
  257. bottom: ${space(2)};
  258. left: ${space(2)};
  259. right: ${space(2)};
  260. `;
  261. const ContentContainer = styled('div')`
  262. position: relative;
  263. z-index: 1;
  264. margin-top: 80px;
  265. `;
  266. const SolutionsDrawerContainer = styled('div')`
  267. height: 100%;
  268. display: grid;
  269. grid-template-rows: auto auto 1fr;
  270. position: relative;
  271. `;
  272. const SolutionsDrawerHeader = styled(DrawerHeader)`
  273. position: unset;
  274. max-height: ${MIN_NAV_HEIGHT}px;
  275. box-shadow: none;
  276. border-bottom: 1px solid ${p => p.theme.border};
  277. `;
  278. const SolutionsDrawerNavigator = styled('div')`
  279. display: grid;
  280. grid-template-columns: 1fr;
  281. align-items: center;
  282. padding: ${space(0.75)} 24px;
  283. background: ${p => p.theme.background};
  284. z-index: 1;
  285. min-height: ${MIN_NAV_HEIGHT}px;
  286. box-shadow: ${p => p.theme.translucentBorder} 0 1px;
  287. `;
  288. const SolutionsDrawerBody = styled(DrawerBody)`
  289. overflow: auto;
  290. overscroll-behavior: contain;
  291. /* Move the scrollbar to the left edge */
  292. scroll-margin: 0 ${space(2)};
  293. direction: rtl;
  294. * {
  295. direction: ltr;
  296. }
  297. `;
  298. const Header = styled('h3')`
  299. display: block;
  300. font-size: ${p => p.theme.fontSizeExtraLarge};
  301. font-weight: ${p => p.theme.fontWeightBold};
  302. margin: 0;
  303. `;
  304. const NavigationCrumbs = styled(NavigationBreadcrumbs)`
  305. margin: 0;
  306. padding: 0;
  307. `;
  308. const CrumbContainer = styled('div')`
  309. display: flex;
  310. gap: ${space(1)};
  311. align-items: center;
  312. `;
  313. const ShortId = styled('div')`
  314. font-family: ${p => p.theme.text.family};
  315. font-size: ${p => p.theme.fontSizeMedium};
  316. line-height: 1;
  317. `;
  318. const ButtonWithStars = styled('div')`
  319. position: relative;
  320. display: flex;
  321. align-items: center;
  322. `;
  323. const StarLarge = styled('img')`
  324. position: absolute;
  325. z-index: 0;
  326. filter: sepia(1) saturate(3) hue-rotate(290deg);
  327. `;
  328. const StarLarge1 = styled(StarLarge)`
  329. left: 45px;
  330. bottom: -15px;
  331. transform: rotate(90deg);
  332. width: 16px;
  333. height: 16px;
  334. `;
  335. const StarLarge2 = styled(StarLarge)`
  336. left: -5px;
  337. top: -15px;
  338. transform: rotate(-30deg);
  339. width: 24px;
  340. height: 24px;
  341. `;
  342. const StarLarge3 = styled(StarLarge)`
  343. right: -25px;
  344. bottom: 0px;
  345. transform: rotate(20deg);
  346. width: 28px;
  347. height: 28px;
  348. `;
  349. const StyledCard = styled('div')`
  350. background: ${p => p.theme.backgroundElevated};
  351. border-radius: ${p => p.theme.borderRadius};
  352. border: 1px solid ${p => p.theme.border};
  353. overflow: hidden;
  354. box-shadow: ${p => p.theme.dropShadowMedium};
  355. padding: ${space(1.5)} ${space(2)};
  356. `;
  357. const HeaderText = styled('div')`
  358. font-weight: bold;
  359. font-size: ${p => p.theme.fontSizeLarge};
  360. display: flex;
  361. align-items: center;
  362. gap: ${space(0.5)};
  363. padding-bottom: ${space(2)};
  364. justify-content: space-between;
  365. `;
  366. const StyledFeatureBadge = styled(FeatureBadge)`
  367. margin-left: ${space(0.25)};
  368. padding-bottom: 3px;
  369. `;
  370. const ResourcesHeader = styled('div')`
  371. gap: ${space(1)};
  372. font-weight: bold;
  373. font-size: ${p => p.theme.fontSizeLarge};
  374. display: flex;
  375. align-items: center;
  376. padding-bottom: ${space(2)};
  377. `;
  378. const StarTrail = styled('div')`
  379. height: 450px;
  380. width: 100%;
  381. position: absolute;
  382. bottom: 5rem;
  383. left: 0;
  384. right: 0;
  385. z-index: -1;
  386. pointer-events: none;
  387. overflow: hidden;
  388. `;
  389. const TrailStar = styled('img')`
  390. position: absolute;
  391. filter: sepia(1) saturate(3) hue-rotate(290deg);
  392. transition: all 0.2s ease-in-out;
  393. `;
  394. const HeaderContainer = styled('div')`
  395. display: flex;
  396. align-items: center;
  397. gap: ${space(0.5)};
  398. `;