RiskAcknowledgementModal.jsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  16. import {
  17. Modal,
  18. Button,
  19. Typography,
  20. Checkbox,
  21. Input,
  22. Space,
  23. } from '@douyinfe/semi-ui';
  24. import { IconAlertTriangle } from '@douyinfe/semi-icons';
  25. import { useIsMobile } from '../../../hooks/common/useIsMobile';
  26. import MarkdownRenderer from '../markdown/MarkdownRenderer';
  27. const { Text } = Typography;
  28. const RiskMarkdownBlock = React.memo(function RiskMarkdownBlock({
  29. markdownContent,
  30. }) {
  31. if (!markdownContent) {
  32. return null;
  33. }
  34. return (
  35. <div
  36. className='rounded-lg'
  37. style={{
  38. border: '1px solid var(--semi-color-warning-light-hover)',
  39. padding: '12px',
  40. contentVisibility: 'auto',
  41. }}
  42. >
  43. <MarkdownRenderer content={markdownContent} />
  44. </div>
  45. );
  46. });
  47. const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
  48. visible,
  49. title,
  50. markdownContent = '',
  51. detailTitle = '',
  52. detailItems = [],
  53. checklist = [],
  54. inputPrompt = '',
  55. requiredText = '',
  56. inputPlaceholder = '',
  57. mismatchText = '',
  58. cancelText = '',
  59. confirmText = '',
  60. onCancel,
  61. onConfirm,
  62. }) {
  63. const isMobile = useIsMobile();
  64. const [checkedItems, setCheckedItems] = useState([]);
  65. const [typedText, setTypedText] = useState('');
  66. useEffect(() => {
  67. if (!visible) return;
  68. setCheckedItems(Array(checklist.length).fill(false));
  69. setTypedText('');
  70. }, [visible, checklist.length]);
  71. const allChecked = useMemo(() => {
  72. if (checklist.length === 0) return true;
  73. return checkedItems.length === checklist.length && checkedItems.every(Boolean);
  74. }, [checkedItems, checklist.length]);
  75. const typedMatched = useMemo(() => {
  76. if (!requiredText) return true;
  77. return typedText.trim() === requiredText.trim();
  78. }, [typedText, requiredText]);
  79. const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
  80. const canConfirm = allChecked && typedMatched;
  81. const handleChecklistChange = useCallback((index, checked) => {
  82. setCheckedItems((previous) => {
  83. const next = [...previous];
  84. next[index] = checked;
  85. return next;
  86. });
  87. }, []);
  88. return (
  89. <Modal
  90. visible={visible}
  91. title={
  92. <Space align='center'>
  93. <IconAlertTriangle style={{ color: 'var(--semi-color-warning)' }} />
  94. <span>{title}</span>
  95. </Space>
  96. }
  97. width={isMobile ? '100%' : 860}
  98. centered
  99. maskClosable={false}
  100. closeOnEsc={false}
  101. onCancel={onCancel}
  102. bodyStyle={{
  103. maxHeight: isMobile ? '70vh' : '72vh',
  104. overflowY: 'auto',
  105. padding: isMobile ? '12px 16px' : '18px 22px',
  106. }}
  107. footer={
  108. <Space>
  109. <Button onClick={onCancel}>{cancelText}</Button>
  110. <Button
  111. theme='solid'
  112. type='danger'
  113. disabled={!canConfirm}
  114. onClick={onConfirm}
  115. >
  116. {confirmText}
  117. </Button>
  118. </Space>
  119. }
  120. >
  121. <div className='flex flex-col gap-4'>
  122. <RiskMarkdownBlock markdownContent={markdownContent} />
  123. {detailItems.length > 0 ? (
  124. <div
  125. className='flex flex-col gap-2 rounded-lg'
  126. style={{
  127. border: '1px solid var(--semi-color-warning-light-hover)',
  128. background: 'var(--semi-color-fill-0)',
  129. padding: isMobile ? '10px 12px' : '12px 14px',
  130. }}
  131. >
  132. {detailTitle ? <Text strong>{detailTitle}</Text> : null}
  133. <div className='font-mono text-xs break-all bg-orange-50 border border-orange-200 rounded-md p-2'>
  134. {detailText}
  135. </div>
  136. </div>
  137. ) : null}
  138. {checklist.length > 0 ? (
  139. <div
  140. className='flex flex-col gap-2 rounded-lg'
  141. style={{
  142. border: '1px solid var(--semi-color-border)',
  143. background: 'var(--semi-color-fill-0)',
  144. padding: isMobile ? '10px 12px' : '12px 14px',
  145. }}
  146. >
  147. {checklist.map((item, index) => (
  148. <Checkbox
  149. key={`risk-check-${index}`}
  150. checked={!!checkedItems[index]}
  151. onChange={(event) => {
  152. handleChecklistChange(index, event.target.checked);
  153. }}
  154. >
  155. {item}
  156. </Checkbox>
  157. ))}
  158. </div>
  159. ) : null}
  160. {requiredText ? (
  161. <div
  162. className='flex flex-col gap-2 rounded-lg'
  163. style={{
  164. border: '1px solid var(--semi-color-danger-light-hover)',
  165. background: 'var(--semi-color-danger-light-default)',
  166. padding: isMobile ? '10px 12px' : '12px 14px',
  167. }}
  168. >
  169. {inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
  170. <div className='font-mono text-xs break-all rounded-md p-2 bg-gray-50 border border-gray-200'>
  171. {requiredText}
  172. </div>
  173. <Input
  174. value={typedText}
  175. onChange={setTypedText}
  176. placeholder={inputPlaceholder}
  177. autoFocus={visible}
  178. onCopy={(event) => event.preventDefault()}
  179. onCut={(event) => event.preventDefault()}
  180. onPaste={(event) => event.preventDefault()}
  181. onDrop={(event) => event.preventDefault()}
  182. />
  183. {!typedMatched && typedText ? (
  184. <Text type='danger' size='small'>
  185. {mismatchText}
  186. </Text>
  187. ) : null}
  188. </div>
  189. ) : null}
  190. </div>
  191. </Modal>
  192. );
  193. });
  194. export default RiskAcknowledgementModal;