| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- /*
- Copyright (C) 2025 QuantumNous
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
- For commercial licensing, please contact support@quantumnous.com
- */
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
- import {
- Modal,
- Button,
- Typography,
- Checkbox,
- Input,
- Space,
- } from '@douyinfe/semi-ui';
- import { IconAlertTriangle } from '@douyinfe/semi-icons';
- import { useIsMobile } from '../../../hooks/common/useIsMobile';
- import MarkdownRenderer from '../markdown/MarkdownRenderer';
- const { Text } = Typography;
- const RiskMarkdownBlock = React.memo(function RiskMarkdownBlock({
- markdownContent,
- }) {
- if (!markdownContent) {
- return null;
- }
- return (
- <div
- className='rounded-lg'
- style={{
- border: '1px solid var(--semi-color-warning-light-hover)',
- background:
- 'linear-gradient(180deg, var(--semi-color-warning-light-default) 0%, var(--semi-color-fill-0) 100%)',
- padding: '12px',
- contentVisibility: 'auto',
- }}
- >
- <MarkdownRenderer content={markdownContent} />
- </div>
- );
- });
- const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
- visible,
- title,
- markdownContent = '',
- detailTitle = '',
- detailItems = [],
- checklist = [],
- inputPrompt = '',
- requiredText = '',
- inputPlaceholder = '',
- mismatchText = '',
- cancelText = '',
- confirmText = '',
- onCancel,
- onConfirm,
- }) {
- const isMobile = useIsMobile();
- const [checkedItems, setCheckedItems] = useState([]);
- const [typedText, setTypedText] = useState('');
- useEffect(() => {
- if (!visible) return;
- setCheckedItems(Array(checklist.length).fill(false));
- setTypedText('');
- }, [visible, checklist.length]);
- const allChecked = useMemo(() => {
- if (checklist.length === 0) return true;
- return checkedItems.length === checklist.length && checkedItems.every(Boolean);
- }, [checkedItems, checklist.length]);
- const typedMatched = useMemo(() => {
- if (!requiredText) return true;
- return typedText.trim() === requiredText.trim();
- }, [typedText, requiredText]);
- const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
- const canConfirm = allChecked && typedMatched;
- const handleChecklistChange = useCallback((index, checked) => {
- setCheckedItems((previous) => {
- const next = [...previous];
- next[index] = checked;
- return next;
- });
- }, []);
- return (
- <Modal
- visible={visible}
- title={
- <Space align='center'>
- <IconAlertTriangle style={{ color: 'var(--semi-color-warning)' }} />
- <span>{title}</span>
- </Space>
- }
- width={isMobile ? '100%' : 860}
- centered
- maskClosable={false}
- closeOnEsc={false}
- onCancel={onCancel}
- bodyStyle={{
- maxHeight: isMobile ? '70vh' : '72vh',
- overflowY: 'auto',
- padding: isMobile ? '12px 16px' : '18px 22px',
- }}
- footer={
- <Space>
- <Button onClick={onCancel}>{cancelText}</Button>
- <Button
- theme='solid'
- type='danger'
- disabled={!canConfirm}
- onClick={onConfirm}
- >
- {confirmText}
- </Button>
- </Space>
- }
- >
- <div className='flex flex-col gap-4'>
- <div
- className='rounded-lg'
- style={{
- border: '1px solid var(--semi-color-warning-light-hover)',
- background: 'var(--semi-color-warning-light-default)',
- padding: isMobile ? '10px 12px' : '12px 14px',
- }}
- >
- </div>
- <RiskMarkdownBlock markdownContent={markdownContent} />
- {detailItems.length > 0 ? (
- <div
- className='flex flex-col gap-2 rounded-lg'
- style={{
- border: '1px solid var(--semi-color-warning-light-hover)',
- background: 'var(--semi-color-fill-0)',
- padding: isMobile ? '10px 12px' : '12px 14px',
- }}
- >
- {detailTitle ? <Text strong>{detailTitle}</Text> : null}
- <div className='font-mono text-xs break-all bg-orange-50 border border-orange-200 rounded-md p-2'>
- {detailText}
- </div>
- </div>
- ) : null}
- {checklist.length > 0 ? (
- <div
- className='flex flex-col gap-2 rounded-lg'
- style={{
- border: '1px solid var(--semi-color-border)',
- background: 'var(--semi-color-fill-0)',
- padding: isMobile ? '10px 12px' : '12px 14px',
- }}
- >
- {checklist.map((item, index) => (
- <Checkbox
- key={`risk-check-${index}`}
- checked={!!checkedItems[index]}
- onChange={(event) => {
- handleChecklistChange(index, event.target.checked);
- }}
- >
- {item}
- </Checkbox>
- ))}
- </div>
- ) : null}
- {requiredText ? (
- <div
- className='flex flex-col gap-2 rounded-lg'
- style={{
- border: '1px solid var(--semi-color-danger-light-hover)',
- background: 'var(--semi-color-danger-light-default)',
- padding: isMobile ? '10px 12px' : '12px 14px',
- }}
- >
- {inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
- <div className='font-mono text-xs break-all rounded-md p-2 bg-gray-50 border border-gray-200'>
- {requiredText}
- </div>
- <Input
- value={typedText}
- onChange={setTypedText}
- placeholder={inputPlaceholder}
- autoFocus={visible}
- onCopy={(event) => event.preventDefault()}
- onCut={(event) => event.preventDefault()}
- onPaste={(event) => event.preventDefault()}
- onDrop={(event) => event.preventDefault()}
- />
- {!typedMatched && typedText ? (
- <Text type='danger' size='small'>
- {mismatchText}
- </Text>
- ) : null}
- </div>
- ) : null}
- </div>
- </Modal>
- );
- });
- export default RiskAcknowledgementModal;
|