'use client'; import { useState, useMemo } from 'react'; import { X, CheckCircle2, AlertTriangle, HelpCircle, ListChecks } from 'lucide-react'; import type { VersionPlan } from '@/lib/version-plan'; import type { VersionWithContext } from '@/lib/derive'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { findCategoryByCode, resolveCategoryIdFromCode } from '@/lib/task-category'; import { addWorkHours } from '@/lib/work-hours'; import { clampDevEstimateHours, clampTestCaseEstimateHours } from '@/lib/ai-estimation-policy'; import type { AgentDecomposeResponse, AgentDevTaskDraft, AgentTestCaseDraft, } from '@ftb/shared'; interface Props { result: AgentDecomposeResponse; version: VersionWithContext; plan: VersionPlan; requirements: Array<{ id: string; code: string; title: string; description?: string }>; onClose: () => void; } export function DecomposeReportModal({ result, version, plan, requirements, onClose }: Props) { const { createTask } = useDevTaskStore(); const { createTestCase } = useTestCaseStore(); const { categories } = useTaskCategoryStore(); const user = useAuthStore((s) => s.user); const { result: data, meta } = result; const [selectedDevIdx, setSelectedDevIdx] = useState>( new Set(data.devTaskDrafts.map((_, i) => i)), ); const [selectedTcIdx, setSelectedTcIdx] = useState>( new Set(data.testCaseDrafts.map((_, i) => i)), ); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const reqByCode = useMemo(() => { const m = new Map(); for (const r of requirements) m.set(r.code, r); return m; }, [requirements]); const reqByInternalId = useMemo(() => { const m = new Map(); for (const r of requirements) m.set(r.id, r); return m; }, [requirements]); const toggleDev = (i: number) => { const n = new Set(selectedDevIdx); if (n.has(i)) n.delete(i); else n.add(i); setSelectedDevIdx(n); }; const toggleTc = (i: number) => { const n = new Set(selectedTcIdx); if (n.has(i)) n.delete(i); else n.add(i); setSelectedTcIdx(n); }; const handleAdopt = () => { if (submitting) return; setSubmitting(true); const now = new Date().toISOString(); // 把每个 reference 的 requirement 类型 id(AI 给的是 code)映射回内部 id const normalizeRefs = (refs: AgentDevTaskDraft['references'] | AgentTestCaseDraft['references']) => refs.map((ref) => { if (ref.type === 'requirement') { const r = reqByCode.get(ref.id) ?? reqByInternalId.get(ref.id); if (r) return { ...ref, id: r.id, label: ref.label || `${r.code} ${r.title}` }; } return ref; }); let devCount = 0; for (let i = 0; i < data.devTaskDrafts.length; i++) { if (!selectedDevIdx.has(i)) continue; const draft = data.devTaskDrafts[i]; const refs = normalizeRefs(draft.references); const reqRef = refs.find((r) => r.type === 'requirement'); const requirementId = reqRef?.id ?? requirements[0]?.id ?? ''; const categoryId = resolveCategoryIdFromCode(categories, draft.categoryCode, 'development'); const estimateHours = clampDevEstimateHours(draft.categoryCode, draft.estimateHours); const startISO = new Date().toISOString(); const endISO = addWorkHours(startISO, estimateHours); createTask({ requirementId, title: draft.title, description: draft.description, categoryId, assigneeId: '', reviewerId: undefined, priority: draft.priority, expectedStartAt: startISO, expectedEndAt: endISO, estimateHours, actualStartAt: undefined, actualEndAt: undefined, status: 'todo', blockReason: undefined, blockedById: undefined, predecessorIds: undefined, riskLevel: undefined, delayReason: undefined, overdueVersionReason: undefined, references: refs, aiDraft: true, aiDraftAt: now, createdBy: user?.name || 'AI', } as any); devCount++; } let tcCount = 0; for (let i = 0; i < data.testCaseDrafts.length; i++) { if (!selectedTcIdx.has(i)) continue; const draft = data.testCaseDrafts[i]; const refs = normalizeRefs(draft.references); const reqRef = refs.find((r) => r.type === 'requirement'); const categoryId = resolveCategoryIdFromCode(categories, draft.categoryCode, 'testing'); const estimateHours = clampTestCaseEstimateHours(draft.categoryCode, draft.estimateHours); createTestCase({ versionId: version.id, requirementId: reqRef?.id, title: draft.title, description: draft.description, categoryId, priority: draft.priority, estimateHours, assigneeId: undefined, references: refs, aiDraft: true, aiDraftAt: now, createdBy: user?.name || 'AI', } as any); tcCount++; } setSubmitted(true); setSubmitting(false); // 1.5 秒后自动关闭 setTimeout(() => onClose(), 1500); }; return (
e.stopPropagation()} > {/* Header */}

AI 拆解结果

{meta.model} · 输入 {meta.inputTokens} tok · 输出 {meta.outputTokens} tok ·{' '} {(meta.durationMs / 1000).toFixed(1)}s
{/* Body */}
{/* 对账报告 */}

对账报告

{data.report.matched.length > 0 && (
完美对应({data.report.matched.length})
    {data.report.matched.map((m, i) => (
  • {m.reqId} ↔ {m.noteIds.join(', ') || '(仅需求驱动)'} → 拆出 {m.taskCount} 个任务
  • ))}
)} {data.report.reqOnly.length > 0 && (
仅需求未见原型

以下需求在原型上未找到对应批注,已按需求文字拆解,请人工核对:

    {data.report.reqOnly.map((id, i) =>
  • {id}
  • )}
)} {data.report.noteOnly.length > 0 && (
仅原型未见需求

以下原型批注在需求清单中未提及,已跳过拆解(可能是漏录需求):

    {data.report.noteOnly.map((id, i) =>
  • {id}
  • )}
)} {data.report.ambiguous.length > 0 && (
含糊批注
    {data.report.ambiguous.map((a, i) => (
  • {a.noteId}:{a.reason}
  • ))}
)}
{/* DevTask 草案 */}

开发任务草案({data.devTaskDrafts.length})

{data.devTaskDrafts.length === 0 ? (

无可生成的任务

) : (
{data.devTaskDrafts.map((d, i) => ( ))}
)}
{/* TestCase 草案 */}

测试用例草案({data.testCaseDrafts.length})

{data.testCaseDrafts.length === 0 ? (

无可生成的用例

) : (
{data.testCaseDrafts.map((d, i) => ( ))}
)}
{/* Footer */}
已选 {selectedDevIdx.size} 个任务 + {selectedTcIdx.size} 个用例
); }