RiskAcknowledgementModal.jsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. background:
  40. 'linear-gradient(180deg, var(--semi-color-warning-light-default) 0%, var(--semi-color-fill-0) 100%)',
  41. padding: '12px',
  42. contentVisibility: 'auto',
  43. }}
  44. >
  45. <MarkdownRenderer content={markdownContent} />
  46. </div>
  47. );
  48. });
  49. const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
  50. visible,
  51. title,
  52. markdownContent = '',
  53. detailTitle = '',
  54. detailItems = [],
  55. checklist = [],
  56. inputPrompt = '',
  57. requiredText = '',
  58. inputPlaceholder = '',
  59. mismatchText = '',
  60. cancelText = '',
  61. confirmText = '',
  62. onCancel,
  63. onConfirm,
  64. }) {
  65. const isMobile = useIsMobile();
  66. const [checkedItems, setCheckedItems] = useState([]);
  67. const [typedText, setTypedText] = useState('');
  68. useEffect(() => {
  69. if (!visible) return;
  70. setCheckedItems(Array(checklist.length).fill(false));
  71. setTypedText('');
  72. }, [visible, checklist.length]);
  73. const allChecked = useMemo(() => {
  74. if (checklist.length === 0) return true;
  75. return checkedItems.length === checklist.length && checkedItems.every(Boolean);
  76. }, [checkedItems, checklist.length]);
  77. const typedMatched = useMemo(() => {
  78. if (!requiredText) return true;
  79. return typedText.trim() === requiredText.trim();
  80. }, [typedText, requiredText]);
  81. const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
  82. const canConfirm = allChecked && typedMatched;
  83. const handleChecklistChange = useCallback((index, checked) => {
  84. setCheckedItems((previous) => {
  85. const next = [...previous];
  86. next[index] = checked;
  87. return next;
  88. });
  89. }, []);
  90. return (
  91. <Modal
  92. visible={visible}
  93. title={
  94. <Space align='center'>
  95. <IconAlertTriangle style={{ color: 'var(--semi-color-warning)' }} />
  96. <span>{title}</span>
  97. </Space>
  98. }
  99. width={isMobile ? '100%' : 860}
  100. centered
  101. maskClosable={false}
  102. closeOnEsc={false}
  103. onCancel={onCancel}
  104. bodyStyle={{
  105. maxHeight: isMobile ? '70vh' : '72vh',
  106. overflowY: 'auto',
  107. padding: isMobile ? '12px 16px' : '18px 22px',
  108. }}
  109. footer={
  110. <Space>
  111. <Button onClick={onCancel}>{cancelText}</Button>
  112. <Button
  113. theme='solid'
  114. type='danger'
  115. disabled={!canConfirm}
  116. onClick={onConfirm}
  117. >
  118. {confirmText}
  119. </Button>
  120. </Space>
  121. }
  122. >
  123. <div className='flex flex-col gap-4'>
  124. <div
  125. className='rounded-lg'
  126. style={{
  127. border: '1px solid var(--semi-color-warning-light-hover)',
  128. background: 'var(--semi-color-warning-light-default)',
  129. padding: isMobile ? '10px 12px' : '12px 14px',
  130. }}
  131. >
  132. </div>
  133. <RiskMarkdownBlock markdownContent={markdownContent} />
  134. {detailItems.length > 0 ? (
  135. <div
  136. className='flex flex-col gap-2 rounded-lg'
  137. style={{
  138. border: '1px solid var(--semi-color-warning-light-hover)',
  139. background: 'var(--semi-color-fill-0)',
  140. padding: isMobile ? '10px 12px' : '12px 14px',
  141. }}
  142. >
  143. {detailTitle ? <Text strong>{detailTitle}</Text> : null}
  144. <div className='font-mono text-xs break-all bg-orange-50 border border-orange-200 rounded-md p-2'>
  145. {detailText}
  146. </div>
  147. </div>
  148. ) : null}
  149. {checklist.length > 0 ? (
  150. <div
  151. className='flex flex-col gap-2 rounded-lg'
  152. style={{
  153. border: '1px solid var(--semi-color-border)',
  154. background: 'var(--semi-color-fill-0)',
  155. padding: isMobile ? '10px 12px' : '12px 14px',
  156. }}
  157. >
  158. {checklist.map((item, index) => (
  159. <Checkbox
  160. key={`risk-check-${index}`}
  161. checked={!!checkedItems[index]}
  162. onChange={(event) => {
  163. handleChecklistChange(index, event.target.checked);
  164. }}
  165. >
  166. {item}
  167. </Checkbox>
  168. ))}
  169. </div>
  170. ) : null}
  171. {requiredText ? (
  172. <div
  173. className='flex flex-col gap-2 rounded-lg'
  174. style={{
  175. border: '1px solid var(--semi-color-danger-light-hover)',
  176. background: 'var(--semi-color-danger-light-default)',
  177. padding: isMobile ? '10px 12px' : '12px 14px',
  178. }}
  179. >
  180. {inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
  181. <div className='font-mono text-xs break-all rounded-md p-2 bg-gray-50 border border-gray-200'>
  182. {requiredText}
  183. </div>
  184. <Input
  185. value={typedText}
  186. onChange={setTypedText}
  187. placeholder={inputPlaceholder}
  188. autoFocus={visible}
  189. onCopy={(event) => event.preventDefault()}
  190. onCut={(event) => event.preventDefault()}
  191. onPaste={(event) => event.preventDefault()}
  192. onDrop={(event) => event.preventDefault()}
  193. />
  194. {!typedMatched && typedText ? (
  195. <Text type='danger' size='small'>
  196. {mismatchText}
  197. </Text>
  198. ) : null}
  199. </div>
  200. ) : null}
  201. </div>
  202. </Modal>
  203. );
  204. });
  205. export default RiskAcknowledgementModal;