/** * ConfirmDialog — 基于 Promise 的确认对话框 * * 用于替代 window.confirm(),支持异步等待结果 * * @example * const { confirm, ConfirmDialogComponent } = useConfirmDialog() * const confirmed = await confirm({ title: '确认删除', message: '确定删除?', danger: true }) * * // 在 JSX 中渲染: * */ import { useState, useCallback, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import Modal from './Modal' import Button from './Button' import { AlertTriangle } from 'lucide-react' import i18n from '../../i18n' export interface ConfirmOptions { /** 对话框标题 */ title: string /** 对话框内容 */ message: string /** 是否为危险操作(红色确认按钮) */ danger?: boolean /** 确认按钮文本 */ confirmText?: string /** 取消按钮文本 */ cancelText?: string } /** Agent 高风险确认:Reject / Once / Always(Always 仅会话内存) */ export type AgentRiskChoice = 'reject' | 'once' | 'always' export interface AgentRiskConfirmOptions { title: string message: string /** 高风险时 Once/Always 用危险样式 */ danger?: boolean } interface ConfirmState extends ConfirmOptions { isOpen: boolean } interface AgentRiskState { isOpen: boolean title: string message: string danger: boolean } const initialState: ConfirmState = { isOpen: false, title: '', message: '', danger: false, confirmText: i18n.t('common:action.confirm'), cancelText: i18n.t('common:action.cancel'), } const initialAgentRisk: AgentRiskState = { isOpen: false, title: '', message: '', danger: true, } /** * ConfirmDialog 组件 + Hook * * 返回 confirm 函数和需要渲染的组件 */ export function useConfirmDialog() { const { t } = useTranslation(['common', 'ai']) const [state, setState] = useState(initialState) const [agentRisk, setAgentRisk] = useState(initialAgentRisk) const resolverRef = useRef<((value: boolean) => void) | null>(null) const agentResolverRef = useRef<((value: AgentRiskChoice) => void) | null>(null) const confirm = useCallback((options: ConfirmOptions): Promise => { return new Promise((resolve) => { resolverRef.current = resolve setState({ ...options, isOpen: true, danger: options.danger ?? false, confirmText: options.confirmText ?? i18n.t('common:action.confirm'), cancelText: options.cancelText ?? i18n.t('common:action.cancel'), }) }) }, []) const confirmAgentRisk = useCallback((options: AgentRiskConfirmOptions): Promise => { return new Promise((resolve) => { agentResolverRef.current = resolve setAgentRisk({ isOpen: true, title: options.title, message: options.message, danger: options.danger ?? true, }) }) }, []) const handleClose = useCallback(() => { setState((prev) => ({ ...prev, isOpen: false })) resolverRef.current?.(false) resolverRef.current = null }, []) const handleConfirm = useCallback(() => { setState((prev) => ({ ...prev, isOpen: false })) resolverRef.current?.(true) resolverRef.current = null }, []) const resolveAgentRisk = useCallback((choice: AgentRiskChoice) => { setAgentRisk((prev) => ({ ...prev, isOpen: false })) agentResolverRef.current?.(choice) agentResolverRef.current = null }, []) /** 停止 Agent 时关闭待确认对话框(等同 Reject,不计入用户决策) */ const dismissAgentRisk = useCallback(() => { if (!agentResolverRef.current) return resolveAgentRisk('reject') }, [resolveAgentRisk]) useEffect(() => { if (!agentRisk.isOpen) return const onKey = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault() resolveAgentRisk('reject') } else if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault() resolveAgentRisk('once') } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [agentRisk.isOpen, resolveAgentRisk]) const ConfirmDialogComponent = ( <> ) : undefined } >

{state.message}

resolveAgentRisk('reject')} showConfirm={false} size="sm" disableBackdropClose icon={} footer={
} >

{agentRisk.message}

{t('ai:confirm.hint')}

) return { confirm, confirmAgentRisk, dismissAgentRisk, ConfirmDialogComponent } } export default useConfirmDialog