diff --git a/apps/web/src/api/copilot.ts b/apps/web/src/api/copilot.ts index 358e99c..efe53a2 100644 --- a/apps/web/src/api/copilot.ts +++ b/apps/web/src/api/copilot.ts @@ -54,6 +54,10 @@ export function dismissInsight(id: string) { return client.post(`/copilot/insights/${id}/dismiss`); } +export function listAlerts(params?: { severity?: string; page?: number; page_size?: number }) { + return listInsights({ insight_type: 'anomaly', ...params }); +} + export function listRules(params?: { page?: number; page_size?: number }) { return client.get>>('/copilot/rules', { params }); } diff --git a/apps/web/src/components/Copilot/CopilotAlert.tsx b/apps/web/src/components/Copilot/CopilotAlert.tsx new file mode 100644 index 0000000..343e011 --- /dev/null +++ b/apps/web/src/components/Copilot/CopilotAlert.tsx @@ -0,0 +1,106 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Alert, Badge, List, Button, Space, Typography, Spin } from 'antd'; +import { CheckOutlined } from '@ant-design/icons'; +import { listAlerts, dismissInsight } from '../../api/copilot'; +import type { CopilotInsight } from '../../api/copilot'; + +const severityConfig: Record = { + critical: { type: 'error', label: '危急' }, + warning: { type: 'warning', label: '警告' }, + info: { type: 'info', label: '提示' }, +}; + +export function CopilotAlert() { + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(false); + const [dismissing, setDismissing] = useState(null); + + const fetchAlerts = useCallback(async () => { + setLoading(true); + try { + const res = await listAlerts({ page_size: 50 }); + const result = res.data as unknown as { items: CopilotInsight[]; total: number }; + setAlerts(result.items ?? []); + } catch { + // 静默 + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchAlerts(); + const timer = setInterval(fetchAlerts, 30_000); + return () => clearInterval(timer); + }, [fetchAlerts]); + + const handleDismiss = async (id: string) => { + setDismissing(id); + try { + await dismissInsight(id); + setAlerts((prev) => prev.filter((a) => a.id !== id)); + } finally { + setDismissing(null); + } + }; + + if (!alerts.length && !loading) return null; + + const criticalCount = alerts.filter((a) => a.severity === 'critical').length; + + return ( +
+ {criticalCount > 0 && ( + + )} + {loading && alerts.length === 0 ? ( + + ) : ( + { + const config = severityConfig[item.severity] ?? severityConfig.info; + return ( + } + loading={dismissing === item.id} + onClick={() => handleDismiss(item.id)} + > + 已知悉 + , + ]} + > + + + {item.title} + + } + description={ + item.content?.suggestion ? ( + + {item.content.suggestion as string} + + ) : undefined + } + /> + + ); + }} + /> + )} +
+ ); +} diff --git a/apps/web/src/components/Copilot/index.ts b/apps/web/src/components/Copilot/index.ts index 9f06213..888a35b 100644 --- a/apps/web/src/components/Copilot/index.ts +++ b/apps/web/src/components/Copilot/index.ts @@ -1,4 +1,5 @@ export { CopilotBadge } from './CopilotBadge'; export { CopilotCard } from './CopilotCard'; +export { CopilotAlert } from './CopilotAlert'; export { useCopilotRisk } from './useCopilotRisk'; export { useCopilotInsights } from './useCopilotInsights';