autofixMessageBox.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. import {type FormEvent, Fragment, useEffect, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {AnimatePresence, type AnimationProps, motion} from 'framer-motion';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import {openModal} from 'sentry/actionCreators/modal';
  6. import {Button, LinkButton} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import AutofixActionSelector from 'sentry/components/events/autofix/autofixActionSelector';
  9. import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback';
  10. import {AutofixSetupWriteAccessModal} from 'sentry/components/events/autofix/autofixSetupWriteAccessModal';
  11. import {
  12. type AutofixCodebaseChange,
  13. AutofixStatus,
  14. type AutofixStep,
  15. AutofixStepType,
  16. } from 'sentry/components/events/autofix/types';
  17. import {
  18. makeAutofixQueryKey,
  19. useAutofixData,
  20. } from 'sentry/components/events/autofix/useAutofix';
  21. import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup';
  22. import Input from 'sentry/components/input';
  23. import LoadingIndicator from 'sentry/components/loadingIndicator';
  24. import {ScrollCarousel} from 'sentry/components/scrollCarousel';
  25. import {
  26. IconChat,
  27. IconCheckmark,
  28. IconChevron,
  29. IconClose,
  30. IconCopy,
  31. IconFatal,
  32. IconOpen,
  33. IconSad,
  34. } from 'sentry/icons';
  35. import {t} from 'sentry/locale';
  36. import {space} from 'sentry/styles/space';
  37. import {singleLineRenderer} from 'sentry/utils/marked';
  38. import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
  39. import testableTransition from 'sentry/utils/testableTransition';
  40. import useApi from 'sentry/utils/useApi';
  41. import useCopyToClipboard from 'sentry/utils/useCopyToClipboard';
  42. function useSendMessage({groupId, runId}: {groupId: string; runId: string}) {
  43. const api = useApi({persistInFlight: true});
  44. const queryClient = useQueryClient();
  45. return useMutation({
  46. mutationFn: (params: {message: string}) => {
  47. return api.requestPromise(`/issues/${groupId}/autofix/update/`, {
  48. method: 'POST',
  49. data: {
  50. run_id: runId,
  51. payload: {
  52. type: 'user_message',
  53. text: params.message,
  54. },
  55. },
  56. });
  57. },
  58. onSuccess: _ => {
  59. queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)});
  60. addSuccessMessage('Thanks for the input.');
  61. },
  62. onError: () => {
  63. addErrorMessage(t('Something went wrong when sending Autofix your message.'));
  64. },
  65. });
  66. }
  67. interface AutofixMessageBoxProps {
  68. actionText: string;
  69. allowEmptyMessage: boolean;
  70. displayText: string;
  71. groupId: string;
  72. onSend: ((message: string, isCustom?: boolean) => void) | null;
  73. responseRequired: boolean;
  74. runId: string;
  75. step: AutofixStep | null;
  76. isChangesStep?: boolean;
  77. isRootCauseSelectionStep?: boolean;
  78. primaryAction?: boolean;
  79. scrollIntoView?: (() => void) | null;
  80. scrollText?: string;
  81. }
  82. function CreatePRsButton({
  83. changes,
  84. groupId,
  85. }: {
  86. changes: AutofixCodebaseChange[];
  87. groupId: string;
  88. }) {
  89. const autofixData = useAutofixData({groupId});
  90. const api = useApi();
  91. const queryClient = useQueryClient();
  92. const [hasClickedCreatePr, setHasClickedCreatePr] = useState(false);
  93. const createPRs = () => {
  94. setHasClickedCreatePr(true);
  95. for (const change of changes) {
  96. createPr({change});
  97. }
  98. };
  99. const {mutate: createPr} = useMutation({
  100. mutationFn: ({change}: {change: AutofixCodebaseChange}) => {
  101. return api.requestPromise(`/issues/${groupId}/autofix/update/`, {
  102. method: 'POST',
  103. data: {
  104. run_id: autofixData?.run_id,
  105. payload: {
  106. type: 'create_pr',
  107. repo_external_id: change.repo_external_id,
  108. },
  109. },
  110. });
  111. },
  112. onSuccess: () => {
  113. addSuccessMessage(t('Created pull requests.'));
  114. queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)});
  115. },
  116. onError: () => {
  117. setHasClickedCreatePr(false);
  118. addErrorMessage(t('Failed to create a pull request'));
  119. },
  120. });
  121. return (
  122. <Button
  123. priority="primary"
  124. onClick={createPRs}
  125. icon={
  126. hasClickedCreatePr && <ProcessingStatusIndicator size={14} mini hideMessage />
  127. }
  128. busy={hasClickedCreatePr}
  129. analyticsEventName="Autofix: Create PR Clicked"
  130. analyticsEventKey="autofix.create_pr_clicked"
  131. analyticsParams={{group_id: groupId}}
  132. >
  133. Draft PR{changes.length > 1 ? 's' : ''}
  134. </Button>
  135. );
  136. }
  137. function CreateBranchButton({
  138. changes,
  139. groupId,
  140. }: {
  141. changes: AutofixCodebaseChange[];
  142. groupId: string;
  143. }) {
  144. const autofixData = useAutofixData({groupId});
  145. const api = useApi();
  146. const queryClient = useQueryClient();
  147. const [hasClickedPushToBranch, setHasClickedPushToBranch] = useState(false);
  148. const pushToBranch = () => {
  149. setHasClickedPushToBranch(true);
  150. for (const change of changes) {
  151. createBranch({change});
  152. }
  153. };
  154. const {mutate: createBranch} = useMutation({
  155. mutationFn: ({change}: {change: AutofixCodebaseChange}) => {
  156. return api.requestPromise(`/issues/${groupId}/autofix/update/`, {
  157. method: 'POST',
  158. data: {
  159. run_id: autofixData?.run_id,
  160. payload: {
  161. type: 'create_branch',
  162. repo_external_id: change.repo_external_id,
  163. },
  164. },
  165. });
  166. },
  167. onSuccess: () => {
  168. addSuccessMessage(t('Pushed to branches.'));
  169. queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)});
  170. },
  171. onError: () => {
  172. setHasClickedPushToBranch(false);
  173. addErrorMessage(t('Failed to push to branches.'));
  174. },
  175. });
  176. return (
  177. <Button
  178. onClick={pushToBranch}
  179. icon={
  180. hasClickedPushToBranch && <ProcessingStatusIndicator size={14} mini hideMessage />
  181. }
  182. busy={hasClickedPushToBranch}
  183. analyticsEventName="Autofix: Push to Branch Clicked"
  184. analyticsEventKey="autofix.push_to_branch_clicked"
  185. analyticsParams={{group_id: groupId}}
  186. >
  187. Check Out Locally
  188. </Button>
  189. );
  190. }
  191. function SetupAndCreatePRsButton({
  192. changes,
  193. groupId,
  194. }: {
  195. changes: AutofixCodebaseChange[];
  196. groupId: string;
  197. }) {
  198. const {data: setupData} = useAutofixSetup({groupId, checkWriteAccess: true});
  199. if (
  200. !changes.every(
  201. change =>
  202. setupData?.githubWriteIntegration?.repos?.find(
  203. repo => `${repo.owner}/${repo.name}` === change.repo_name
  204. )?.ok
  205. )
  206. ) {
  207. return (
  208. <Button
  209. priority="primary"
  210. onClick={() => {
  211. openModal(deps => <AutofixSetupWriteAccessModal {...deps} groupId={groupId} />);
  212. }}
  213. analyticsEventName="Autofix: Create PR Setup Clicked"
  214. analyticsEventKey="autofix.create_pr_setup_clicked"
  215. analyticsParams={{group_id: groupId}}
  216. title={t('Enable write access to create pull requests')}
  217. >
  218. {t('Draft PR')}
  219. </Button>
  220. );
  221. }
  222. return <CreatePRsButton changes={changes} groupId={groupId} />;
  223. }
  224. function SetupAndCreateBranchButton({
  225. changes,
  226. groupId,
  227. }: {
  228. changes: AutofixCodebaseChange[];
  229. groupId: string;
  230. }) {
  231. const {data: setupData} = useAutofixSetup({groupId, checkWriteAccess: true});
  232. if (
  233. !changes.every(
  234. change =>
  235. setupData?.githubWriteIntegration?.repos?.find(
  236. repo => `${repo.owner}/${repo.name}` === change.repo_name
  237. )?.ok
  238. )
  239. ) {
  240. return (
  241. <Button
  242. onClick={() => {
  243. openModal(deps => <AutofixSetupWriteAccessModal {...deps} groupId={groupId} />);
  244. }}
  245. analyticsEventName="Autofix: Create PR Setup Clicked"
  246. analyticsEventKey="autofix.create_pr_setup_clicked"
  247. analyticsParams={{group_id: groupId}}
  248. title={t('Enable write access to create branches')}
  249. >
  250. {t('Check Out Locally')}
  251. </Button>
  252. );
  253. }
  254. return <CreateBranchButton changes={changes} groupId={groupId} />;
  255. }
  256. interface RootCauseAndFeedbackInputAreaProps {
  257. actionText: string;
  258. changesMode: 'give_feedback' | 'add_tests' | 'create_prs' | null;
  259. groupId: string;
  260. handleSend: (e: FormEvent<HTMLFormElement>) => void;
  261. isRootCauseSelectionStep: boolean;
  262. message: string;
  263. primaryAction: boolean;
  264. responseRequired: boolean;
  265. rootCauseMode: 'suggested_root_cause' | 'custom_root_cause' | null;
  266. setMessage: (message: string) => void;
  267. }
  268. function RootCauseAndFeedbackInputArea({
  269. handleSend,
  270. isRootCauseSelectionStep,
  271. message,
  272. rootCauseMode,
  273. responseRequired,
  274. setMessage,
  275. groupId,
  276. actionText,
  277. primaryAction,
  278. changesMode,
  279. }: RootCauseAndFeedbackInputAreaProps) {
  280. return (
  281. <form onSubmit={handleSend}>
  282. <InputArea>
  283. {!responseRequired ? (
  284. <Fragment>
  285. <NormalInput
  286. type="text"
  287. value={message}
  288. onChange={e => setMessage(e.target.value)}
  289. placeholder={
  290. !isRootCauseSelectionStep
  291. ? 'Share helpful context or directions...'
  292. : rootCauseMode === 'suggested_root_cause'
  293. ? '(Optional) Provide any instructions for the fix...'
  294. : 'Propose your own root cause...'
  295. }
  296. />
  297. {isRootCauseSelectionStep ? (
  298. <Button
  299. type="submit"
  300. priority={primaryAction ? 'primary' : 'default'}
  301. analyticsEventKey="autofix.create_fix_clicked"
  302. analyticsEventName="Autofix: Create Fix Clicked"
  303. analyticsParams={{
  304. group_id: groupId,
  305. type:
  306. rootCauseMode === 'suggested_root_cause'
  307. ? message
  308. ? 'suggested_with_instructions'
  309. : 'suggested'
  310. : 'custom',
  311. }}
  312. >
  313. {actionText}
  314. </Button>
  315. ) : (
  316. <Button
  317. type="submit"
  318. priority={primaryAction ? 'primary' : 'default'}
  319. analyticsEventKey="autofix.feedback_provided"
  320. analyticsEventName="Autofix: Feedback Provided"
  321. analyticsParams={{
  322. group_id: groupId,
  323. type:
  324. changesMode === 'give_feedback'
  325. ? 'feedback_for_changes'
  326. : 'interjection',
  327. }}
  328. >
  329. {actionText}
  330. </Button>
  331. )}
  332. </Fragment>
  333. ) : (
  334. <Fragment>
  335. <RequiredInput
  336. type="text"
  337. value={message}
  338. onChange={e => setMessage(e.target.value)}
  339. placeholder={'Please answer to continue...'}
  340. />
  341. <Button type="submit" priority={'primary'}>
  342. {actionText}
  343. </Button>
  344. </Fragment>
  345. )}
  346. </InputArea>
  347. </form>
  348. );
  349. }
  350. function StepIcon({step}: {step: AutofixStep}) {
  351. if (step.type === AutofixStepType.CHANGES) {
  352. if (step.changes?.length === 0) {
  353. return <IconSad size="sm" color="gray300" />;
  354. }
  355. if (step.changes.every(change => change.pull_request)) {
  356. return <IconCheckmark size="sm" color="green300" isCircled />;
  357. }
  358. return null;
  359. }
  360. if (step.type === AutofixStepType.ROOT_CAUSE_ANALYSIS) {
  361. if (step.causes?.length === 0) {
  362. return <IconSad size="sm" color="gray300" />;
  363. }
  364. return step.selection ? <IconCheckmark size="sm" color="green300" isCircled /> : null;
  365. }
  366. switch (step.status) {
  367. case AutofixStatus.WAITING_FOR_USER_RESPONSE:
  368. return <IconChat size="sm" color="gray300" />;
  369. case AutofixStatus.PROCESSING:
  370. return <ProcessingStatusIndicator size={14} mini hideMessage />;
  371. case AutofixStatus.CANCELLED:
  372. return <IconClose size="sm" isCircled color="gray300" />;
  373. case AutofixStatus.ERROR:
  374. return <IconFatal size="sm" color="red300" />;
  375. case AutofixStatus.COMPLETED:
  376. return <IconCheckmark size="sm" color="green300" isCircled />;
  377. default:
  378. return null;
  379. }
  380. }
  381. const animationProps: AnimationProps = {
  382. exit: {opacity: 0},
  383. initial: {opacity: 0, y: -20},
  384. animate: {opacity: 1, y: 0},
  385. transition: testableTransition({duration: 0.3}),
  386. };
  387. function AutofixMessageBox({
  388. displayText = '',
  389. step = null,
  390. primaryAction = false,
  391. responseRequired = false,
  392. onSend,
  393. actionText = 'Send',
  394. allowEmptyMessage = false,
  395. groupId,
  396. runId,
  397. scrollIntoView = null,
  398. scrollText = t('View'),
  399. isRootCauseSelectionStep = false,
  400. isChangesStep = false,
  401. }: AutofixMessageBoxProps) {
  402. const [message, setMessage] = useState('');
  403. const {mutate: send} = useSendMessage({groupId, runId});
  404. const [height, setHeight] = useState<number | 'auto'>('auto');
  405. const contentRef = useRef<HTMLDivElement>(null);
  406. const [rootCauseMode, setRootCauseMode] = useState<
  407. 'suggested_root_cause' | 'custom_root_cause' | null
  408. >(null);
  409. const [changesMode, setChangesMode] = useState<
  410. 'give_feedback' | 'add_tests' | 'create_prs' | null
  411. >(null);
  412. const changes =
  413. isChangesStep && step?.type === AutofixStepType.CHANGES ? step.changes : [];
  414. const prsMade =
  415. step?.status === AutofixStatus.COMPLETED &&
  416. changes.length >= 1 &&
  417. changes.every(change => change.pull_request);
  418. const branchesMade =
  419. !prsMade &&
  420. step?.status === AutofixStatus.COMPLETED &&
  421. changes.length >= 1 &&
  422. changes.every(change => change.branch_name);
  423. const isDisabled =
  424. step?.status === AutofixStatus.ERROR ||
  425. (step?.type === AutofixStepType.ROOT_CAUSE_ANALYSIS && step.causes?.length === 0) ||
  426. (step?.type === AutofixStepType.CHANGES && changes.length === 0);
  427. useEffect(() => {
  428. if (contentRef.current) {
  429. setHeight(contentRef.current.scrollHeight);
  430. }
  431. }, [displayText, step, isRootCauseSelectionStep, rootCauseMode]);
  432. const handleSend = (e: FormEvent<HTMLFormElement>) => {
  433. e.preventDefault();
  434. if (isRootCauseSelectionStep && onSend) {
  435. if (rootCauseMode === 'custom_root_cause' && message.trim() !== '') {
  436. onSend?.(message, true);
  437. setMessage('');
  438. } else if (rootCauseMode === 'suggested_root_cause') {
  439. onSend?.(message, false);
  440. setMessage('');
  441. }
  442. return;
  443. }
  444. let text = message;
  445. if (isChangesStep && changesMode === 'add_tests') {
  446. text =
  447. 'Please write a unit test that reproduces the issue to make sure it is fixed. Put it in the appropriate test file in the codebase. If there is none, create one.';
  448. }
  449. if (text.trim() !== '' || allowEmptyMessage) {
  450. if (onSend != null) {
  451. onSend(text);
  452. } else {
  453. send({
  454. message: text,
  455. });
  456. }
  457. setMessage('');
  458. }
  459. };
  460. function BranchButton({change}: {change: AutofixCodebaseChange}) {
  461. const {onClick} = useCopyToClipboard({
  462. text: `git fetch --all && git switch ${change.branch_name}`,
  463. successMessage: t('Command copied. Next stop: your terminal.'),
  464. });
  465. return (
  466. <Button
  467. key={`${change.repo_external_id}-${Math.random()}`}
  468. size="xs"
  469. priority="primary"
  470. onClick={onClick}
  471. aria-label={t('Check out in %s', change.repo_name)}
  472. title={t('git fetch --all && git switch %s', change.branch_name)}
  473. icon={<IconCopy size="xs" />}
  474. >
  475. {t('Check out in %s', change.repo_name)}
  476. </Button>
  477. );
  478. }
  479. return (
  480. <Container>
  481. <AnimatedContent animate={{height}} transition={{duration: 0.3, ease: 'easeInOut'}}>
  482. <ContentWrapper ref={contentRef}>
  483. <ContentArea>
  484. {step && (
  485. <StepHeader>
  486. <StepTitle
  487. dangerouslySetInnerHTML={{
  488. __html: singleLineRenderer(step.title),
  489. }}
  490. />
  491. <StepIconContainer>
  492. <StepIcon step={step} />
  493. </StepIconContainer>
  494. <StepHeaderRightSection>
  495. {scrollIntoView !== null && (
  496. <ScrollIntoViewButtonWrapper>
  497. <AnimatePresence initial>
  498. <motion.div key="content" {...animationProps}>
  499. <Button
  500. onClick={scrollIntoView}
  501. size="xs"
  502. priority="primary"
  503. icon={<IconChevron direction="down" size="xs" />}
  504. aria-label={t('Jump to content')}
  505. >
  506. {t('%s', scrollText)}
  507. </Button>
  508. </motion.div>
  509. </AnimatePresence>
  510. </ScrollIntoViewButtonWrapper>
  511. )}
  512. <AutofixFeedback />
  513. </StepHeaderRightSection>
  514. </StepHeader>
  515. )}
  516. <Message
  517. dangerouslySetInnerHTML={{
  518. __html: singleLineRenderer(displayText),
  519. }}
  520. />
  521. </ContentArea>
  522. {!isDisabled && (
  523. <InputSection>
  524. {isRootCauseSelectionStep ? (
  525. <AutofixActionSelector
  526. options={[
  527. {key: 'custom_root_cause', label: t('Propose your own root cause')},
  528. {
  529. key: 'suggested_root_cause',
  530. label: t('Use suggested root cause'),
  531. active: true,
  532. },
  533. ]}
  534. selected={rootCauseMode}
  535. onSelect={value => setRootCauseMode(value)}
  536. onBack={() => setRootCauseMode(null)}
  537. >
  538. {option => (
  539. <RootCauseAndFeedbackInputArea
  540. handleSend={handleSend}
  541. isRootCauseSelectionStep={isRootCauseSelectionStep}
  542. message={message}
  543. rootCauseMode={option.key}
  544. responseRequired={responseRequired}
  545. setMessage={setMessage}
  546. actionText={actionText}
  547. primaryAction={primaryAction}
  548. changesMode={changesMode}
  549. groupId={groupId}
  550. />
  551. )}
  552. </AutofixActionSelector>
  553. ) : isChangesStep && !prsMade && !branchesMade ? (
  554. <AutofixActionSelector
  555. options={[
  556. {key: 'add_tests', label: t('Add tests')},
  557. {key: 'give_feedback', label: t('Iterate')},
  558. {
  559. key: 'create_prs',
  560. label: t('Use this code'),
  561. active: true,
  562. },
  563. ]}
  564. selected={changesMode}
  565. onSelect={value => setChangesMode(value)}
  566. onBack={() => setChangesMode(null)}
  567. >
  568. {option => (
  569. <Fragment>
  570. {option.key === 'give_feedback' && (
  571. <RootCauseAndFeedbackInputArea
  572. handleSend={handleSend}
  573. isRootCauseSelectionStep={isRootCauseSelectionStep}
  574. message={message}
  575. rootCauseMode={rootCauseMode}
  576. responseRequired={responseRequired}
  577. setMessage={setMessage}
  578. actionText={actionText}
  579. primaryAction
  580. changesMode={option.key}
  581. groupId={groupId}
  582. />
  583. )}
  584. {option.key === 'add_tests' && (
  585. <form onSubmit={handleSend}>
  586. <InputArea>
  587. <StaticMessage>
  588. Write unit tests to make sure the issue is fixed?
  589. </StaticMessage>
  590. <Button type="submit" priority="primary">
  591. Add Tests
  592. </Button>
  593. </InputArea>
  594. </form>
  595. )}
  596. {option.key === 'create_prs' && (
  597. <InputArea>
  598. <StaticMessage>
  599. Push the above changes to{' '}
  600. {changes.length > 1
  601. ? `${changes.length} branches`
  602. : 'a branch'}
  603. ?
  604. </StaticMessage>
  605. <ButtonBar gap={1}>
  606. <SetupAndCreateBranchButton
  607. changes={changes}
  608. groupId={groupId}
  609. />
  610. <SetupAndCreatePRsButton
  611. changes={changes}
  612. groupId={groupId}
  613. />
  614. </ButtonBar>
  615. </InputArea>
  616. )}
  617. </Fragment>
  618. )}
  619. </AutofixActionSelector>
  620. ) : isChangesStep && prsMade ? (
  621. <StyledScrollCarousel aria-label={t('View pull requests')}>
  622. {changes.map(
  623. change =>
  624. change.pull_request?.pr_url && (
  625. <LinkButton
  626. key={`${change.repo_external_id}-${Math.random()}`}
  627. size="xs"
  628. priority="primary"
  629. icon={<IconOpen size="xs" />}
  630. href={change.pull_request.pr_url}
  631. external
  632. >
  633. View PR in {change.repo_name}
  634. </LinkButton>
  635. )
  636. )}
  637. </StyledScrollCarousel>
  638. ) : isChangesStep && branchesMade ? (
  639. <StyledScrollCarousel aria-label={t('Check out branches')}>
  640. {changes.map(
  641. change =>
  642. change.branch_name && (
  643. <BranchButton
  644. key={`${change.repo_external_id}-${Math.random()}`}
  645. change={change}
  646. />
  647. )
  648. )}
  649. </StyledScrollCarousel>
  650. ) : (
  651. <RootCauseAndFeedbackInputArea
  652. handleSend={handleSend}
  653. isRootCauseSelectionStep={isRootCauseSelectionStep}
  654. message={message}
  655. rootCauseMode={rootCauseMode}
  656. responseRequired={responseRequired}
  657. setMessage={setMessage}
  658. actionText={actionText}
  659. primaryAction={primaryAction}
  660. changesMode={changesMode}
  661. groupId={groupId}
  662. />
  663. )}
  664. </InputSection>
  665. )}
  666. {isDisabled && <Placeholder />}
  667. </ContentWrapper>
  668. </AnimatedContent>
  669. </Container>
  670. );
  671. }
  672. const Placeholder = styled('div')`
  673. padding: ${space(1)};
  674. `;
  675. const ScrollIntoViewButtonWrapper = styled('div')`
  676. position: absolute;
  677. top: -2rem;
  678. right: 50%;
  679. transform: translateX(50%);
  680. `;
  681. const Container = styled('div')`
  682. position: absolute;
  683. bottom: 0;
  684. left: 0;
  685. right: 0;
  686. background: ${p => p.theme.backgroundElevated};
  687. z-index: 100;
  688. border-top: 1px solid ${p => p.theme.border};
  689. box-shadow: ${p => p.theme.dropShadowHeavy};
  690. display: flex;
  691. flex-direction: column;
  692. `;
  693. const StyledScrollCarousel = styled(ScrollCarousel)`
  694. padding: 0 ${space(1)};
  695. `;
  696. const AnimatedContent = styled(motion.div)`
  697. overflow: hidden;
  698. `;
  699. const ContentWrapper = styled('div')`
  700. display: flex;
  701. flex-direction: column;
  702. `;
  703. const ContentArea = styled('div')`
  704. padding: ${space(3)} ${space(2)} ${space(1)} ${space(2)};
  705. `;
  706. const Message = styled('div')`
  707. padding: 0 ${space(1)} 0 ${space(1)};
  708. `;
  709. const StepTitle = styled('div')`
  710. font-weight: ${p => p.theme.fontWeightBold};
  711. white-space: nowrap;
  712. display: flex;
  713. align-items: center;
  714. span {
  715. margin-right: ${space(1)};
  716. }
  717. `;
  718. const StepHeaderRightSection = styled('div')`
  719. display: flex;
  720. align-items: center;
  721. gap: ${space(1)};
  722. `;
  723. const StepIconContainer = styled('div')`
  724. display: flex;
  725. align-items: center;
  726. margin-right: auto;
  727. `;
  728. const StepHeader = styled('div')`
  729. display: flex;
  730. align-items: center;
  731. justify-content: space-between;
  732. padding: 0 ${space(1)} ${space(1)} ${space(1)};
  733. font-size: ${p => p.theme.fontSizeMedium};
  734. font-family: ${p => p.theme.text.family};
  735. gap: ${space(1)};
  736. `;
  737. const InputArea = styled('div')`
  738. display: flex;
  739. `;
  740. const StaticMessage = styled('p')`
  741. flex-grow: 1;
  742. margin-right: 8px;
  743. padding-top: ${space(1)};
  744. padding-left: ${space(1)};
  745. margin-bottom: 0;
  746. border-top: 1px solid ${p => p.theme.border};
  747. `;
  748. const NormalInput = styled(Input)`
  749. flex-grow: 1;
  750. margin-right: 8px;
  751. `;
  752. const RequiredInput = styled(Input)`
  753. flex-grow: 1;
  754. margin-right: 8px;
  755. border-color: ${p => p.theme.errorFocus};
  756. box-shadow: 0 0 0 1px ${p => p.theme.errorFocus};
  757. `;
  758. const ProcessingStatusIndicator = styled(LoadingIndicator)`
  759. && {
  760. margin: 0;
  761. height: 14px;
  762. width: 14px;
  763. }
  764. `;
  765. const InputSection = styled('div')`
  766. padding: ${space(0.5)} ${space(2)} ${space(2)};
  767. `;
  768. export default AutofixMessageBox;