fix(web): Phase 6.5c+ enhance [Y/n] tactile feedback & diagnostics
- Add active:scale-95 active:bg-neutral-800 for physical click feedback - Add disabled:opacity-30 for clearer disabled state - Add tooltip "大腦分析中..." when proposalId is missing - Add comprehensive console.log diagnostics for authorization flow - Add reason parameter "Authorized via WarRoom" for audit trail - Implement optimistic UI with immediate loading state transition Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,25 +57,39 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
/**
|
||||
* 處理簽核 (Y 按鈕)
|
||||
* 呼叫 POST /api/v1/approvals/{id}/sign
|
||||
* Phase 6.5c+: 樂觀更新 + 物理回饋強化
|
||||
*/
|
||||
const handleApprove = useCallback(async () => {
|
||||
if (!proposalId || buttonState === 'loading') return
|
||||
// 樂觀更新: 立刻切換 loading,不等待 fetch 啟動
|
||||
console.log('🚀 指令授權中:', proposalId)
|
||||
|
||||
if (!proposalId || buttonState === 'loading') {
|
||||
console.warn('⚠️ 授權阻斷: proposalId=', proposalId, 'buttonState=', buttonState)
|
||||
return
|
||||
}
|
||||
|
||||
// 關鍵: 立即進入 loading,消除 300ms 感知延遲
|
||||
setButtonState('loading')
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const result = await apiClient.signApproval(proposalId, 'commander')
|
||||
const result = await apiClient.signApproval(proposalId, 'commander', 'Authorized via WarRoom')
|
||||
|
||||
console.log('✅ 簽核回應:', result)
|
||||
|
||||
if (result.status === 'approved' || result.status === 'APPROVED') {
|
||||
setButtonState('approved')
|
||||
console.log('🎯 授權成功,觸發 onApprovalChange')
|
||||
onApprovalChange?.(proposalId, 'approved')
|
||||
} else {
|
||||
// Multi-sig: 還需要更多簽核
|
||||
console.log('🔐 Multi-sig 等待中,目前簽核:', result.current_signatures, '/', result.required_signatures)
|
||||
setButtonState('idle')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DualStateIncidentCard] Approve failed:', error)
|
||||
console.error('❌ 授權失敗:', error)
|
||||
console.error(' proposalId:', proposalId)
|
||||
console.error(' API URL:', `${process.env.NEXT_PUBLIC_API_URL}/approvals/${proposalId}/sign`)
|
||||
setButtonState('error')
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Unknown error')
|
||||
// 3 秒後恢復
|
||||
@@ -86,19 +100,27 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
/**
|
||||
* 處理拒絕 (n 按鈕)
|
||||
* 呼叫 POST /api/v1/approvals/{id}/reject
|
||||
* Phase 6.5c+: 樂觀更新 + 物理回饋強化
|
||||
*/
|
||||
const handleReject = useCallback(async () => {
|
||||
if (!proposalId || buttonState === 'loading') return
|
||||
console.log('🛑 指令拒絕中:', proposalId)
|
||||
|
||||
if (!proposalId || buttonState === 'loading') {
|
||||
console.warn('⚠️ 拒絕阻斷: proposalId=', proposalId, 'buttonState=', buttonState)
|
||||
return
|
||||
}
|
||||
|
||||
// 關鍵: 立即進入 loading,消除 300ms 感知延遲
|
||||
setButtonState('loading')
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
await apiClient.rejectApproval(proposalId, 'commander')
|
||||
await apiClient.rejectApproval(proposalId, 'Rejected via WarRoom')
|
||||
console.log('✅ 拒絕成功')
|
||||
setButtonState('rejected')
|
||||
onApprovalChange?.(proposalId, 'rejected')
|
||||
} catch (error) {
|
||||
console.error('[DualStateIncidentCard] Reject failed:', error)
|
||||
console.error('❌ 拒絕失敗:', error)
|
||||
setButtonState('error')
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Unknown error')
|
||||
setTimeout(() => setButtonState('idle'), 3000)
|
||||
@@ -137,11 +159,12 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<div className="flex gap-1 items-center">
|
||||
<button
|
||||
onClick={handleApprove}
|
||||
disabled={!proposalId}
|
||||
className="px-2 py-1 bg-neutral-900 text-white text-xs hover:bg-green-700 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-2 py-1 bg-neutral-900 text-white text-xs hover:bg-green-700 active:scale-95 active:bg-neutral-800 transition-all duration-100 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
title={!proposalId ? '大腦分析中...' : '授權執行'}
|
||||
>
|
||||
Y
|
||||
</button>
|
||||
@@ -149,10 +172,14 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
<button
|
||||
onClick={handleReject}
|
||||
disabled={!proposalId}
|
||||
className="px-2 py-1 bg-neutral-900 text-white text-xs hover:bg-red-700 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-2 py-1 bg-neutral-900 text-white text-xs hover:bg-red-700 active:scale-95 active:bg-neutral-800 transition-all duration-100 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
title={!proposalId ? '大腦分析中...' : '拒絕提案'}
|
||||
>
|
||||
n
|
||||
</button>
|
||||
{!proposalId && (
|
||||
<span className="text-[10px] text-amber-500 ml-1 animate-pulse">⏳</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user