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:
OG T
2026-03-23 13:07:10 +08:00
parent 7db5108a1f
commit c01742ef82

View File

@@ -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>
)
}