'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { CalendarClock, ShieldCheck, Sparkles, TriangleAlert } from 'lucide-react'; import { RouteGuard, useHasPermission } from '@/components/auth/Guard'; import { XiaobaoWarningCard } from '@/components/xiaobao-warning/XiaobaoWarningCard'; import { useXiaobaoWarningRisks } from '@/hooks/useXiaobaoWarningRisks'; import type { XiaobaoRiskLevel, XiaobaoVersionRisk } from '@/lib/xiaobao-risk'; import { buildRiskInsightSignature, findPreviousRiskSnapshot, getReusableInsight, requestRiskInsight, shouldRequestRiskInsightWithCacheGate } from '@/lib/xiaobao-risk-ai'; import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot } from '@/lib/xiaobao-risk-trend'; import { filterXiaobaoRiskWarnings, formatRemainingWork, sanitizeRiskInsight } from '@/lib/xiaobao-warning-view'; import { formatDateTime } from '@/lib/format'; const RISK_LEVEL_LABEL: Record = { on_track: '按期', attention: '关注', at_risk: '有风险', likely_delayed: '大概率延期', blocked: '阻塞', }; const RISK_LEVEL_STYLE: Record = { on_track: 'border-emerald-200 bg-emerald-50 text-emerald-700', attention: 'border-amber-200 bg-amber-50 text-amber-700', at_risk: 'border-orange-200 bg-orange-50 text-orange-700', likely_delayed: 'border-red-200 bg-red-50 text-red-700', blocked: 'border-zinc-900 bg-zinc-900 text-white', }; type FilterOption = { id: string; label: string }; export default function XiaobaoWarningPage() { return ( ); } function XiaobaoWarningContent() { const router = useRouter(); const canManage = useHasPermission('xiaobao.warning:manage'); const { risks, snapshots, insights, riskDataLoaded, saveSnapshot, saveInsight, today } = useXiaobaoWarningRisks({ loadRiskCache: true }); const [selectedRiskId, setSelectedRiskId] = useState(null); const [selectedProductId, setSelectedProductId] = useState(''); const [selectedProjectId, setSelectedProjectId] = useState(''); const savedSnapshotKeysRef = useRef(new Set()); const requestedInsightKeysRef = useRef(new Set()); useEffect(() => { risks.forEach((risk) => { const snapshot = { ...risk.currentSnapshot, createdAt: new Date().toISOString() }; const previousToday = findLatestDailySnapshot(snapshots, snapshot.versionId, snapshot.date); if (!shouldSaveRiskSnapshot(snapshot, previousToday)) return; const key = `${snapshot.versionId}:${snapshot.date}:${buildRiskSignature(snapshot)}`; if (savedSnapshotKeysRef.current.has(key)) return; savedSnapshotKeysRef.current.add(key); saveSnapshot(snapshot).catch(() => { savedSnapshotKeysRef.current.delete(key); }); }); }, [risks, saveSnapshot, snapshots]); useEffect(() => { risks.forEach((risk) => { const previous = findPreviousRiskSnapshot(snapshots, risk.versionId, today); if (!shouldRequestRiskInsightWithCacheGate(riskDataLoaded, insights, risk, previous)) return; const signature = buildRiskInsightSignature(risk); const key = `${risk.versionId}:${signature}`; if (requestedInsightKeysRef.current.has(key)) return; requestedInsightKeysRef.current.add(key); requestRiskInsight(risk).then((response) => { if (!response.ok) return; saveInsight({ versionId: risk.versionId, riskSignature: signature, insight: sanitizeRiskInsight(response.result), generatedAt: new Date().toISOString(), providerInfo: { model: response.meta.model }, }).catch(() => {}); }).catch(() => {}); }); }, [insights, riskDataLoaded, risks, saveInsight, snapshots, today]); const risksWithInsight = useMemo(() => risks.map((risk) => { const cached = getReusableInsight(insights, risk); return cached ? { ...risk, aiInsight: sanitizeRiskInsight(cached.insight) } : risk; }), [insights, risks]); const warningRisks = useMemo(() => filterXiaobaoRiskWarnings(risksWithInsight), [risksWithInsight]); const productOptions = useMemo(() => buildProductOptions(warningRisks), [warningRisks]); const projectOptions = useMemo( () => buildProjectOptions(warningRisks, selectedProductId), [selectedProductId, warningRisks], ); useEffect(() => { if (selectedProductId && !productOptions.some((option) => option.id === selectedProductId)) { setSelectedProductId(''); setSelectedProjectId(''); } }, [productOptions, selectedProductId]); useEffect(() => { if (selectedProjectId && !projectOptions.some((option) => option.id === selectedProjectId)) { setSelectedProjectId(''); } }, [projectOptions, selectedProjectId]); const filteredRisks = useMemo(() => filterXiaobaoRiskWarnings(risksWithInsight, { productId: selectedProductId || undefined, projectId: selectedProjectId || undefined, }), [risksWithInsight, selectedProductId, selectedProjectId]); useEffect(() => { if (filteredRisks.length === 0) { setSelectedRiskId(null); return; } if (!selectedRiskId || !filteredRisks.some((risk) => risk.versionId === selectedRiskId)) { setSelectedRiskId(filteredRisks[0].versionId); } }, [filteredRisks, selectedRiskId]); const selectedRisk = selectedRiskId ? filteredRisks.find((risk) => risk.versionId === selectedRiskId) ?? null : null; const avgConfidence = filteredRisks.length > 0 ? Math.round(filteredRisks.reduce((sum, risk) => sum + risk.confidence, 0) / filteredRisks.length) : 0; return (

小宝预警

{canManage ? '管理视角:全部未结束版本' : '个人视角:我参与的未结束版本'}

{canManage && ( 管理权限 )}

平均置信

{avgConfidence}%

{selectedRisk ? ( router.push(`/versions/${selectedRisk.versionId}`)} /> ) : (
选择左侧风险版本查看详情
)}
); } function XiaobaoWarningDetailPanel({ risk, onNavigate }: { risk: XiaobaoVersionRisk; onNavigate: () => void }) { const [detailTab, setDetailTab] = useState<'reasons' | 'silent' | 'evidence'>('reasons'); const evidence = risk.dailyEvidence; const evidenceItems = [ ...(evidence?.todayDeliveries ?? []), ...(evidence?.todayProgress ?? []), ...(evidence?.todayRisks ?? []), ...(evidence?.progressNotes ?? []), ]; return (

{risk.productName ?? '-'} / {risk.projectName ?? '-'}

{risk.versionName}

= 75 ? 'danger' : risk.riskScore >= 55 ? 'warn' : 'ok'} /> 0 ? 'warn' : 'ok'} /> 0 ? `${risk.delayDays}天` : '0天'} tone={risk.delayDays > 0 ? 'danger' : 'ok'} />
{risk.aiInsight ? (

{risk.aiInsight.summary}

{risk.aiInsight.forecast}

{risk.aiInsight.recommendedReleaseWindow && (

{risk.aiInsight.recommendedReleaseWindow}

)}
{risk.aiInsight.suggestedActions.map((action) => (
{action}
))}
) : ( )}

期望发版:{formatDateTime(risk.expectedReleaseDate)}

预测可发:{formatDateTime(risk.forecastReleaseDate)}

预计延期:{risk.delayDays > 0 ? `${risk.delayDays}天` : '0天'}

{risk.trend.summary}

setDetailTab('reasons')}> 风险原因 {risk.reasons.length} setDetailTab('silent')}> 静默风险 {risk.silentRisks.length} setDetailTab('evidence')}> 日报与活动证据 {evidenceItems.length}
{detailTab === 'reasons' && (
{risk.reasons.length === 0 ? : risk.reasons.map((reason) => (

{reason.title}

{reason.severity}

{reason.detail}

))}
)} {detailTab === 'silent' && (
{risk.silentRisks.length === 0 ? : risk.silentRisks.map((item, index) => (

{item.title}

{item.detail}

))}
)} {detailTab === 'evidence' && (
{evidenceItems.length === 0 ? : evidenceItems.map((item) => (

{item.title}

{item.summary}

{formatDateTime(item.occurredAt)}

))}
)}
); } function RiskLevelBadge({ level }: { level: XiaobaoRiskLevel }) { return ( {RISK_LEVEL_LABEL[level]} ); } function DetailMetric({ label, value, tone }: { label: string; value: string; tone: 'ok' | 'warn' | 'danger' }) { const toneClass = tone === 'danger' ? 'text-red-600' : tone === 'warn' ? 'text-amber-600' : 'text-emerald-600'; return (

{label}

{value}

); } function SelectFilter({ label, value, allLabel, options, onChange, }: { label: string; value: string; allLabel: string; options: FilterOption[]; onChange: (value: string) => void; }) { return ( ); } function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { return ( ); } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function Empty({ text }: { text: string }) { return (
{text}
); } function buildProductOptions(risks: XiaobaoVersionRisk[]): FilterOption[] { return uniqueOptions(risks.map((risk) => ({ id: risk.productId ?? '', label: risk.productName ?? '未关联产品', }))); } function buildProjectOptions(risks: XiaobaoVersionRisk[], productId: string): FilterOption[] { const scoped = productId ? risks.filter((risk) => risk.productId === productId) : risks; return uniqueOptions(scoped.map((risk) => ({ id: risk.projectId ?? '', label: risk.projectName ?? '未关联项目', }))); } function uniqueOptions(options: FilterOption[]): FilterOption[] { const map = new Map(); options.forEach((option) => { if (!option.id || map.has(option.id)) return; map.set(option.id, option.label); }); return Array.from(map, ([id, label]) => ({ id, label })).sort((a, b) => a.label.localeCompare(b.label)); }