FastGPT/packages/web/hooks/useConfirm.tsx
Archer af669a1cfc
4.14.4 features (#6090)
* perf: zod with app log (#6083)

* perf: safe decode

* perf: zod with app log

* fix: text

* remove log

* rename field

* refactor: improve like/dislike interaction (#6080)

* refactor: improve like/dislike interaction

* button style & merge status

* perf

* fix

* i18n

* feedback ui

* format

* api optimize

* openapi

* read status

---------

Co-authored-by: archer <545436317@qq.com>

* perf: remove empty chat

* perf: delete resource tip

* fix: confirm

* feedback filter

* fix: ts

* perf: linker scroll

* perf: feedback ui

* fix: plugin file input store

* fix: max tokens

* update comment

* fix: condition value type

* fix feedback (#6095)

* fix feedback

* text

* list

* fix: versionid

---------

Co-authored-by: archer <545436317@qq.com>

* fix: chat setting render;export logs filter

* add test

* perf: log list api

* perf: redirect check

* perf: log list

* create ui

* create ui

---------

Co-authored-by: heheer <heheer@sealos.io>
2025-12-15 23:36:54 +08:00

210 lines
5.9 KiB
TypeScript

import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
useDisclosure,
Button,
ModalBody,
ModalFooter,
Input,
VStack,
Box,
type ImageProps
} from '@chakra-ui/react';
import { Trans, useTranslation } from 'next-i18next';
import MyModal from '../components/common/MyModal';
import { useMemoizedFn } from 'ahooks';
import { useMemoEnhance } from './useMemoEnhance';
export const useConfirm = (props?: {
title?: string;
iconSrc?: string | '';
content?: string;
showCancel?: boolean;
type?: 'common' | 'delete';
hideFooter?: boolean;
iconColor?: ImageProps['color'];
inputConfirmText?: string;
}) => {
const { t } = useTranslation();
const map = useMemoEnhance(() => {
const map = {
common: {
title: t('common:action_confirm'),
variant: 'primary',
iconSrc: 'common/confirm/commonTip'
},
delete: {
title: t('common:delete_warning'),
variant: 'dangerFill',
iconSrc: 'common/confirm/deleteTip'
}
};
if (props?.type && map[props.type]) return map[props.type];
return map.common;
}, [props?.type, t]);
const {
title = map?.title || t('common:Warning'),
iconSrc = map?.iconSrc,
iconColor,
content,
showCancel = true,
hideFooter = false,
inputConfirmText: initialInputConfirmText
} = props || {};
const [customContent, setCustomContent] = useState<string | React.ReactNode>(content);
const [customContentInputConfirmText, setCustomContentInputConfirmText] = useState<
string | undefined
>(initialInputConfirmText);
const { isOpen, onOpen, onClose } = useDisclosure();
const confirmCb = useRef<Function>();
const cancelCb = useRef<any>();
const openConfirm = useMemoizedFn(
({
onConfirm,
onCancel,
customContent,
inputConfirmText
}: {
onConfirm?: Function;
onCancel?: any;
customContent?: string | React.ReactNode;
inputConfirmText?: string;
}) => {
confirmCb.current = onConfirm;
cancelCb.current = onCancel;
setCustomContent(customContent || content);
setCustomContentInputConfirmText(inputConfirmText || initialInputConfirmText);
return onOpen;
}
);
const ConfirmModal = useMemoizedFn(
({
closeText = t('common:Cancel'),
confirmText = t('common:Confirm'),
isLoading,
countDown = 0
}: {
closeText?: string;
confirmText?: string;
isLoading?: boolean;
countDown?: number;
}) => {
const isInputDelete = !!customContentInputConfirmText;
const timer = useRef<any>();
const [countDownAmount, setCountDownAmount] = useState(countDown);
const [requesting, setRequesting] = useState(false);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
if (isOpen) {
setCountDownAmount(countDown);
setInputValue('');
timer.current = setInterval(() => {
setCountDownAmount((val) => {
if (val <= 0) {
clearInterval(timer.current);
}
return val - 1;
});
}, 1000);
return () => {
clearInterval(timer.current);
};
}
}, [isOpen]);
const isInputDeleteConfirmValid = !isInputDelete
? true
: !!customContentInputConfirmText && inputValue.trim() === customContentInputConfirmText;
return (
<MyModal
isOpen={isOpen}
iconSrc={iconSrc}
iconColor={iconColor}
title={title}
maxW={['90vw', '400px']}
>
<ModalBody pt={5} whiteSpace={'pre-wrap'} fontSize={'sm'}>
{isInputDelete ? (
<VStack align={'stretch'} spacing={3}>
<Box whiteSpace={'pre-wrap'}>{customContent}</Box>
<Box>
<Trans
i18nKey={'common:confirm_input_delete_tip'}
values={{ confirmText: customContentInputConfirmText }}
components={{
bold: <Box as={'span'} fontWeight={'bold'} userSelect={'all'} />
}}
/>
</Box>
<Input
size={'sm'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t('common:confirm_input_delete_placeholder', {
confirmText: customContentInputConfirmText
})}
/>
</VStack>
) : (
customContent
)}
</ModalBody>
{!hideFooter && (
<ModalFooter>
{showCancel && (
<Button
size={'sm'}
variant={'whiteBase'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
px={5}
>
{closeText}
</Button>
)}
<Button
size={'sm'}
variant={map.variant}
isDisabled={countDownAmount > 0 || (isInputDelete && !isInputDeleteConfirmValid)}
ml={3}
isLoading={isLoading || requesting}
px={5}
onClick={async () => {
setRequesting(true);
try {
typeof confirmCb.current === 'function' && (await confirmCb.current());
onClose();
} catch (error) {}
setRequesting(false);
}}
>
{countDownAmount > 0 ? `${countDownAmount}s` : confirmText}
</Button>
</ModalFooter>
)}
</MyModal>
);
}
);
return {
openConfirm,
onClose,
ConfirmModal
};
};