feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection
- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru - Auto-detect system language via navigator.languages; fallback to English - Add BCP47 locale mapping for date formatting and sorting - Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each) - Update language selector with native language names - Extend locale key parity test to cover all 7 languages (70 assertions) - Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features - Add credential encryption, host metrics, session backup crypto, and zmodem bridge - Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
1 parent
fea6d8db04
commit
b0d0e3a5f3
301 files changed
+28002
-2705
No files matched your search
@@ -11,9 +11,12 @@
|
||||
* <ConfirmDialogComponent />
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
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 {
|
||||
/** 对话框标题 */
|
||||
@@ -28,11 +31,25 @@ export interface ConfirmOptions {
|
||||
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
|
||||
/** resolve 回调 */
|
||||
resolver: ((value: boolean) => void) | null
|
||||
}
|
||||
|
||||
interface AgentRiskState {
|
||||
isOpen: boolean
|
||||
title: string
|
||||
message: string
|
||||
danger: boolean
|
||||
}
|
||||
|
||||
const initialState: ConfirmState = {
|
||||
@@ -40,9 +57,15 @@ const initialState: ConfirmState = {
|
||||
title: '',
|
||||
message: '',
|
||||
danger: false,
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
resolver: null,
|
||||
confirmText: i18n.t('common:action.confirm'),
|
||||
cancelText: i18n.t('common:action.cancel'),
|
||||
}
|
||||
|
||||
const initialAgentRisk: AgentRiskState = {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
danger: true,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,8 +74,11 @@ const initialState: ConfirmState = {
|
||||
* 返回 confirm 函数和需要渲染的组件
|
||||
*/
|
||||
export function useConfirmDialog() {
|
||||
const { t } = useTranslation(['common', 'ai'])
|
||||
const [state, setState] = useState<ConfirmState>(initialState)
|
||||
const [agentRisk, setAgentRisk] = useState<AgentRiskState>(initialAgentRisk)
|
||||
const resolverRef = useRef<((value: boolean) => void) | null>(null)
|
||||
const agentResolverRef = useRef<((value: AgentRiskChoice) => void) | null>(null)
|
||||
|
||||
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
@@ -61,9 +87,20 @@ export function useConfirmDialog() {
|
||||
...options,
|
||||
isOpen: true,
|
||||
danger: options.danger ?? false,
|
||||
confirmText: options.confirmText ?? '确认',
|
||||
cancelText: options.cancelText ?? '取消',
|
||||
resolver: null,
|
||||
confirmText: options.confirmText ?? i18n.t('common:action.confirm'),
|
||||
cancelText: options.cancelText ?? i18n.t('common:action.cancel'),
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
const confirmAgentRisk = useCallback((options: AgentRiskConfirmOptions): Promise<AgentRiskChoice> => {
|
||||
return new Promise<AgentRiskChoice>((resolve) => {
|
||||
agentResolverRef.current = resolve
|
||||
setAgentRisk({
|
||||
isOpen: true,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
danger: options.danger ?? true,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
@@ -80,27 +117,86 @@ export function useConfirmDialog() {
|
||||
resolverRef.current = null
|
||||
}, [])
|
||||
|
||||
const ConfirmDialogComponent = (
|
||||
<Modal
|
||||
isOpen={state.isOpen}
|
||||
title={state.title}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleClose}
|
||||
confirmText={state.confirmText}
|
||||
cancelText={state.cancelText}
|
||||
isDanger={state.danger}
|
||||
size="sm"
|
||||
icon={
|
||||
state.danger ? (
|
||||
<AlertTriangle className="w-5 h-5 text-red-400" />
|
||||
) : undefined
|
||||
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')
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-content-secondary leading-relaxed">{state.message}</p>
|
||||
</Modal>
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [agentRisk.isOpen, resolveAgentRisk])
|
||||
|
||||
const ConfirmDialogComponent = (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={state.isOpen}
|
||||
title={state.title}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleClose}
|
||||
confirmText={state.confirmText}
|
||||
cancelText={state.cancelText}
|
||||
isDanger={state.danger}
|
||||
size="sm"
|
||||
icon={
|
||||
state.danger ? (
|
||||
<AlertTriangle className="w-5 h-5 text-red-400" />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-content-secondary">{state.message}</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={agentRisk.isOpen}
|
||||
title={agentRisk.title}
|
||||
onClose={() => resolveAgentRisk('reject')}
|
||||
showConfirm={false}
|
||||
size="sm"
|
||||
disableBackdropClose
|
||||
icon={<AlertTriangle className="w-5 h-5 text-red-400" />}
|
||||
footer={
|
||||
<div className="flex w-full flex-wrap items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => resolveAgentRisk('reject')}>
|
||||
{t('ai:confirm.reject')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={agentRisk.danger ? 'danger' : 'primary'}
|
||||
size="sm"
|
||||
onClick={() => resolveAgentRisk('once')}
|
||||
>
|
||||
{t('ai:confirm.once')}
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => resolveAgentRisk('always')}>
|
||||
{t('ai:confirm.always')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-content-secondary">{agentRisk.message}</p>
|
||||
<p className="mt-3 text-[11px] text-txt-faint">{t('ai:confirm.hint')}</p>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
||||
return { confirm, ConfirmDialogComponent }
|
||||
return { confirm, confirmAgentRisk, dismissAgentRisk, ConfirmDialogComponent }
|
||||
}
|
||||
|
||||
export default useConfirmDialog
|
||||
Reference in new issue
Block a user