'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, resolveAiTaskCategoryByName, type CategoryGroup } from '@/lib/task-category'; import { clampDevAiEstimateHours, clampTestCaseAiEstimateHours } from '@/lib/ai-estimation-policy'; import { resolveDecomposeDraftRequirementFields } from '@/lib/ai-decompose-adoption'; import { formatReportRequirementLabel } from '@/lib/ai-decompose-report'; import { resolveRecommendedAssignee } from '@/lib/ai-assignee-recommendation'; import type { AgentDecomposeTarget, AgentDecomposeResponse, AgentDevTaskDraft, AgentTestCaseDraft, } from '@ftb/shared'; interface Props { result: AgentDecomposeResponse; version: VersionWithContext; plan: VersionPlan; requirements: Array<{ id: string; code: string; title: string; description?: string }>; target: AgentDecomposeTarget; dedupeSummary?: { removedDevTaskCount: number; removedTestCaseCount: number }; onClose: () => void; onAdopted?: () => void; onRejected?: () => void; } export function DecomposeReportModal({ result, version, plan, requirements, target, dedupeSummary, onClose, onAdopted, onRejected, }: Props) { const { createTask } = useDevTaskStore(); const { createTestCase } = useTestCaseStore(); const { categories, ensureCategory } = useTaskCategoryStore(); const user = useAuthStore((s) => s.user); const { result: data, meta } = result; const showDevDrafts = target !== 'test_cases'; const showTestDrafts = target !== 'dev_tasks'; const filteredDuplicateCount = (dedupeSummary?.removedDevTaskCount ?? 0) + (dedupeSummary?.removedTestCaseCount ?? 0); const prototypeOnlyGroups = data.report.prototypeOnly ?? []; const [selectedDevIdx, setSelectedDevIdx] = useState>( new Set(showDevDrafts ? data.devTaskDrafts.map((_, i) => i) : []), ); const [selectedTcIdx, setSelectedTcIdx] = useState>( new Set(showTestDrafts ? data.testCaseDrafts.map((_, i) => i) : []), ); const [useRecommendedAssignees, setUseRecommendedAssignees] = useState(false); 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 resolveDraftTaskTypeName = ( draft: AgentDevTaskDraft | AgentTestCaseDraft, fallbackGroup: Extract, ) => { const explicitName = draft.taskTypeName?.trim(); if (explicitName) return explicitName; const mappedName = findCategoryByCode(categories, draft.categoryCode)?.name; if (mappedName) return mappedName; if (draft.categoryCode) return draft.categoryCode; return fallbackGroup === 'testing' ? '功能测试' : '开发任务'; }; const resolveDraftCategoryName = ( draft: AgentDevTaskDraft | AgentTestCaseDraft, fallbackGroup: Extract, ) => resolveAiTaskCategoryByName(categories, resolveDraftTaskTypeName(draft, fallbackGroup), fallbackGroup, { allowCreate: fallbackGroup !== 'testing', fallbackCode: draft.categoryCode, }).category.name; 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; }); if (showDevDrafts) { 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 requirementFields = resolveDecomposeDraftRequirementFields({ ...draft, references: refs }, requirements); const taskTypeName = resolveDraftTaskTypeName(draft, 'development'); const categoryId = ensureCategory(taskTypeName, 'development', { fallbackCode: draft.categoryCode }).id; const aiEstimateHours = clampDevAiEstimateHours(draft.categoryCode, draft.aiEstimateHours); const recommendedAssignee = resolveRecommendedAssignee(draft, version.members ?? []); createTask({ versionId: version.id, requirementId: requirementFields.requirementId, requirementName: requirementFields.requirementName, title: draft.title, description: draft.description, categoryId, assigneeId: useRecommendedAssignees ? recommendedAssignee?.name ?? '' : '', reviewerId: undefined, priority: draft.priority, expectedStartAt: '', expectedEndAt: '', estimateHours: undefined, aiEstimateHours, 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); } } if (showTestDrafts) { 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 requirementFields = resolveDecomposeDraftRequirementFields({ ...draft, references: refs }, requirements); const taskTypeName = resolveDraftTaskTypeName(draft, 'testing'); const categoryId = ensureCategory(taskTypeName, 'testing', { allowCreate: false, fallbackCode: draft.categoryCode, }).id; const aiEstimateHours = clampTestCaseAiEstimateHours(draft.categoryCode, draft.aiEstimateHours); const recommendedAssignee = resolveRecommendedAssignee(draft, version.members ?? []); createTestCase({ versionId: version.id, requirementId: requirementFields.requirementId || undefined, requirementName: requirementFields.requirementName, title: draft.title, description: draft.description, categoryId, priority: draft.priority, estimateHours: undefined, aiEstimateHours, assigneeId: useRecommendedAssignees ? recommendedAssignee?.name : undefined, references: refs, aiDraft: true, aiDraftAt: now, createdBy: user?.name || 'AI', } as any); } } setSubmitted(true); setSubmitting(false); // 1.5 秒后自动关闭 setTimeout(() => { onAdopted?.(); onClose(); }, 1500); }; const visibleRecommendationCount = useMemo(() => { const members = version.members ?? []; const devCount = showDevDrafts ? data.devTaskDrafts.filter((draft) => resolveRecommendedAssignee(draft, members)).length : 0; const testCount = showTestDrafts ? data.testCaseDrafts.filter((draft) => resolveRecommendedAssignee(draft, members)).length : 0; return devCount + testCount; }, [data.devTaskDrafts, data.testCaseDrafts, showDevDrafts, showTestDrafts, version.members]); const selectedVisibleCount = (showDevDrafts ? selectedDevIdx.size : 0) + (showTestDrafts ? selectedTcIdx.size : 0); return (
e.stopPropagation()} > {/* Header */}

AI 拆解结果

{meta.model} · 输入 {meta.inputTokens} tok · 输出 {meta.outputTokens} tok ·{' '} {(meta.durationMs / 1000).toFixed(1)}s
{/* Body */}
{filteredDuplicateCount > 0 && (
已自动过滤 {dedupeSummary?.removedDevTaskCount ?? 0} 个重复开发任务、{dedupeSummary?.removedTestCaseCount ?? 0} 个重复测试用例。
)} {/* 对账报告 */}

对账报告

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

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

    {data.report.reqOnly.map((id, i) => { const label = formatReportRequirementLabel(id, requirements); return
  • {label}
  • ; })}
)} {prototypeOnlyGroups.length > 0 && (
无需求ID分组

以下原型批注明确可拆,但未匹配到关联需求;采纳后只进入任务/用例,不进入需求池或关联需求列表:

    {prototypeOnlyGroups.map((item, i) => (
  • 无需求ID · {item.requirementName} ({item.noteIds.join(', ') || '无批注编号'} → 拆出 {item.taskCount} 个任务)
  • ))}
)} {data.report.ambiguous.length > 0 && (
含糊批注
    {data.report.ambiguous.map((a, i) => (
  • {a.noteId}:{a.reason}
  • ))}
)}
{/* DevTask 草案 */} {showDevDrafts && (

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

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

无可生成的任务

) : (
{data.devTaskDrafts.map((d, i) => { const recommendation = resolveRecommendedAssignee(d, version.members ?? []); return ( ); })}
)}
)} {/* TestCase 草案 */} {showTestDrafts && (

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

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

无可生成的用例

) : (
{data.testCaseDrafts.map((d, i) => { const recommendation = resolveRecommendedAssignee(d, version.members ?? []); return ( ); })}
)}
)}
{/* Footer */}
已选 {showDevDrafts ? selectedDevIdx.size : 0} 个任务 + {showTestDrafts ? selectedTcIdx.size : 0} 个用例
{visibleRecommendationCount > 0 && ( )}
); }