Files
ftb-project-management/apps/web/components/xiaobao-warning/XiaobaoWarningDrawer.tsx
2026-06-30 14:55:13 +08:00

171 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { CalendarClock, Loader2, Sparkles, X } from 'lucide-react';
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
import { formatDateTime } from '@/lib/format';
import { formatRemainingWork } from '@/lib/xiaobao-warning-view';
import { buildRuleBasedRiskInsight } from '@/lib/xiaobao-risk-suggestion';
export function XiaobaoWarningDrawer({
risk,
onClose,
onNavigate,
}: {
risk: XiaobaoVersionRisk;
onClose: () => void;
onNavigate: () => void;
}) {
const evidence = risk.dailyEvidence;
const evidenceItems = [
...(evidence?.todayDeliveries ?? []),
...(evidence?.todayProgress ?? []),
...(evidence?.todayCreations ?? []),
...(evidence?.todayRisks ?? []),
...(evidence?.progressNotes ?? []),
];
const aiInsight = risk.aiInsight ?? buildRuleBasedRiskInsight(risk);
return (
<div className="fixed inset-0 z-50 flex justify-end bg-black/40">
<div className="flex h-full w-full max-w-xl flex-col border-l border-[var(--line)] bg-[var(--bg)] shadow-2xl">
<div className="border-b border-[var(--line)] bg-[var(--bg-subtle)] px-5 py-2 text-[11px] text-[var(--ink-muted)]">
{risk.productName ?? '-'} / {risk.projectName ?? '-'} / {risk.versionName}
</div>
<header className="flex h-14 shrink-0 items-center gap-3 border-b border-[var(--line)] bg-[var(--bg-card)] px-5">
<div className="min-w-0 flex-1">
<h2 className="truncate text-[15px] font-semibold text-[var(--ink)]"></h2>
<p className="text-[11px] text-[var(--ink-muted)]">AI </p>
</div>
<button
type="button"
onClick={onClose}
className="flex h-8 w-8 items-center justify-center rounded-lg text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
title="关闭"
>
<X className="h-4 w-4" />
</button>
</header>
<div className="flex-1 overflow-y-auto px-5 py-5">
<div className="mb-5 grid grid-cols-2 gap-3">
<Summary label="风险分" value={`${risk.riskScore}`} />
<Summary label="置信度" value={`${risk.confidence}%`} />
<Summary label="剩余工作量" value={formatRemainingWork(risk.remainingWorkHours)} />
<Summary label="预计延期" value={risk.delayDays > 0 ? `${risk.delayDays}` : '0天'} />
</div>
<Section title="发版预测">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-3 text-[12px] leading-5 text-[var(--ink-soft)]">
<p className="flex items-center gap-1.5">
<CalendarClock className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
: {formatDateTime(risk.expectedReleaseDate)}
</p>
<p className="mt-1">: {formatDateTime(risk.forecastReleaseDate)}</p>
<p className="mt-1">{risk.trend.summary}</p>
</div>
</Section>
<Section title="风险原因">
{risk.reasons.length === 0 ? <Empty text="暂无风险原因" /> : risk.reasons.map((reason) => (
<div key={reason.key} className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-3">
<div className="flex items-center justify-between gap-3">
<p className="text-[12px] font-medium text-[var(--ink)]">{reason.title}</p>
<span className="rounded-full bg-[var(--bg-subtle)] px-2 py-0.5 text-[10px] text-[var(--ink-muted)]">{reason.severity}</span>
</div>
<p className="mt-1 text-[12px] leading-5 text-[var(--ink-soft)]">{reason.detail}</p>
</div>
))}
</Section>
<Section title="日报与活动证据">
{evidenceItems.length === 0 ? <Empty text="暂无近期日报或活动证据" /> : evidenceItems.map((item) => (
<div key={item.id} className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-3">
<p className="text-[12px] font-medium text-[var(--ink)]">{item.title}</p>
<p className="mt-1 text-[12px] leading-5 text-[var(--ink-soft)]">{item.summary}</p>
<p className="mt-1 text-[10px] text-[var(--ink-muted)]">{formatDateTime(item.occurredAt)}</p>
</div>
))}
</Section>
<Section title="静默风险">
{risk.silentRisks.length === 0 ? <Empty text="暂无静默风险" /> : risk.silentRisks.map((item, index) => (
<div key={`${item.key}-${item.itemId ?? index}`} className="rounded-lg border border-amber-200 bg-amber-50 p-3">
<p className="text-[12px] font-medium text-amber-900">{item.title}</p>
<p className="mt-1 text-[12px] leading-5 text-amber-800">{item.detail}</p>
</div>
))}
</Section>
<Section title="小宝建议">
{risk.aiInsightUpdating && <InsightUpdatingNotice source={risk.aiInsightSource} />}
{aiInsight ? (
<div className="space-y-2">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-3">
<p className="flex items-start gap-1.5 text-[12px] font-medium text-[var(--ink)]">
<Sparkles className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[var(--accent)]" />
<span>{aiInsight.summary}</span>
</p>
<p className="mt-2 text-[12px] leading-5 text-[var(--ink-soft)]">{aiInsight.forecast}</p>
</div>
{aiInsight.suggestedActions.map((action) => (
<div key={action} className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-3 text-[12px] leading-5 text-[var(--ink-soft)]">
{action}
</div>
))}
</div>
) : (
<Empty text="规则预警已生成AI 解读会在触发条件满足时自动补充" />
)}
</Section>
<button
type="button"
onClick={onNavigate}
className="mt-1 flex h-9 w-full items-center justify-center rounded-lg bg-[var(--accent)] text-[13px] font-medium text-white hover:bg-[var(--accent-hover)]"
>
</button>
</div>
</div>
</div>
);
}
function Summary({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-3">
<p className="text-[11px] text-[var(--ink-muted)]">{label}</p>
<p className="mt-1 text-[18px] font-semibold tabular-nums text-[var(--ink)]">{value}</p>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="mb-5">
<h3 className="mb-2 text-[12px] font-semibold text-[var(--ink)]">{title}</h3>
<div className="space-y-2">{children}</div>
</section>
);
}
function InsightUpdatingNotice({ source }: { source?: XiaobaoVersionRisk['aiInsightSource'] }) {
const detail = source === 'previous_ai'
? '当前先显示上一条建议,生成完成后会自动替换。'
: '当前先显示规则建议,生成完成后会自动替换。';
return (
<div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] leading-5 text-amber-800">
<Loader2 className="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin" />
<span>{detail}</span>
</div>
);
}
function Empty({ text }: { text: string }) {
return (
<div className="rounded-lg border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-3 text-[12px] text-[var(--ink-muted)]">
{text}
</div>
);
}