Files
awoooi/apps/web/src/components/ai/ai-command-panel.tsx
OG T 2e9ccf4a26 fix(lint): 清理所有 ESLint 警告 (61→0)
- 修復未使用變數 (prefix with _)
- 修復 type-only imports
- 修復 react-hooks/exhaustive-deps (useMemo + 依賴補齊)
- 修復 no-explicit-any (eslint-disable 標記)
- 移除未使用的 imports

涉及組件:
- demo/page, layout, page (主頁面)
- ai/* (OpenClaw, HITL, ThinkingStream)
- approval/* (ApprovalCard, LiveApprovalPanel)
- dashboard/* (HostCard, LiveDashboard, ConnectionStatus)
- incident/* (DualStateIncidentCard, ThinkingTerminal)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 17:06:58 +08:00

120 lines
3.9 KiB
TypeScript

'use client'
/**
* AICommandPanel - 戰情室右側 AI 指揮面板
* ==========================================
* Phase 1: OpenClaw + HITL 授權卡片整合
*
* Features:
* - OpenClaw AI 視覺化 (頂部)
* - 待授權卡片列表 (底部)
* - **Phase 15: SSE 即時更新** (取代輪詢)
* - Nothing.tech 純白極簡風格
*
* i18n: 100% next-intl
*/
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import { OpenClawPanel } from './openclaw-panel'
import { ApprovalCard } from '@/components/approval/approval-card'
import {
useApprovalStore,
usePendingApprovals,
toFrontendApproval,
} from '@/stores/approval.store'
import { useApprovalSSE } from '@/hooks/useApprovalSSE'
import { ShieldCheck, Bell } from 'lucide-react'
// =============================================================================
// Props
// =============================================================================
interface AICommandPanelProps {
className?: string
}
// =============================================================================
// Component
// =============================================================================
export function AICommandPanel({ className }: AICommandPanelProps) {
const _t = useTranslations()
const tApproval = useTranslations('approval')
// Store
const { fetchPending, signApproval, rejectApproval } = useApprovalStore()
const pendingApprovals = usePendingApprovals()
// Phase 15: SSE 即時更新 (取代輪詢)
useApprovalSSE({ autoConnect: true })
// Handle approval
const handleApprove = async (id: string) => {
await signApproval(id, 'demo-user', 'War Room User', 'Approved via Command Center')
await fetchPending()
}
// Handle rejection
const handleReject = async (id: string) => {
await rejectApproval(id, 'demo-user', 'War Room User', 'Rejected via Command Center')
await fetchPending()
}
return (
<div className={cn('flex flex-col gap-4', className)}>
{/* OpenClaw AI Section */}
<OpenClawPanel status="patrolling" />
{/* Pending Approvals Section */}
{pendingApprovals.length > 0 && (
<div className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-claw-blue" />
<span className="font-mono text-xs text-nothing-gray-600">
{tApproval('pendingApprovals')}
</span>
</div>
<div className="flex items-center gap-1.5">
<Bell className="w-3 h-3 text-status-warning animate-pulse" />
<span className="font-mono text-xs font-bold text-status-warning">
{pendingApprovals.length}
</span>
</div>
</div>
{/* Approval Cards */}
<div className="space-y-3 max-h-[400px] overflow-y-auto pr-1 scrollbar-thin">
{pendingApprovals.map((approval) => {
const frontendApproval = toFrontendApproval(approval)
return (
<ApprovalCard
key={approval.id}
request={frontendApproval}
onApprove={handleApprove}
onReject={handleReject}
holdDuration={approval.risk_level === 'critical' ? 2000 : 1000}
/>
)
})}
</div>
</div>
)}
{/* Empty State - No pending approvals */}
{pendingApprovals.length === 0 && (
<div className="flex flex-col items-center justify-center py-6 text-center">
<ShieldCheck className="w-8 h-8 text-nothing-gray-300 mb-2" />
<p className="font-mono text-xs text-nothing-gray-400">
{tApproval('noApprovals')}
</p>
</div>
)}
</div>
)
}
export default AICommandPanel