feat(ai): 优化拆解重跑与结果展示
This commit is contained in:
@@ -9,9 +9,10 @@ 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 { clampDevAiEstimateHours, clampTestCaseAiEstimateHours } from '@/lib/ai-estimation-policy';
|
||||
import { formatReportRequirementLabel } from '@/lib/ai-decompose-report';
|
||||
import type {
|
||||
AgentDecomposeTarget,
|
||||
AgentDecomposeResponse,
|
||||
AgentDevTaskDraft,
|
||||
AgentTestCaseDraft,
|
||||
@@ -22,16 +23,22 @@ interface Props {
|
||||
version: VersionWithContext;
|
||||
plan: VersionPlan;
|
||||
requirements: Array<{ id: string; code: string; title: string; description?: string }>;
|
||||
target: AgentDecomposeTarget;
|
||||
dedupeSummary?: { removedDevTaskCount: number; removedTestCaseCount: number };
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DecomposeReportModal({ result, version, plan, requirements, onClose }: Props) {
|
||||
export function DecomposeReportModal({ result, version, plan, requirements, target, dedupeSummary, onClose }: Props) {
|
||||
const { createTask } = useDevTaskStore();
|
||||
const { createTestCase } = useTestCaseStore();
|
||||
const { categories } = 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 [selectedDevIdx, setSelectedDevIdx] = useState<Set<number>>(
|
||||
new Set(data.devTaskDrafts.map((_, i) => i)),
|
||||
@@ -90,9 +97,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
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);
|
||||
const aiEstimateHours = clampDevAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
|
||||
|
||||
createTask({
|
||||
requirementId,
|
||||
@@ -102,9 +107,10 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
assigneeId: '',
|
||||
reviewerId: undefined,
|
||||
priority: draft.priority,
|
||||
expectedStartAt: startISO,
|
||||
expectedEndAt: endISO,
|
||||
estimateHours,
|
||||
expectedStartAt: '',
|
||||
expectedEndAt: '',
|
||||
estimateHours: undefined,
|
||||
aiEstimateHours,
|
||||
actualStartAt: undefined,
|
||||
actualEndAt: undefined,
|
||||
status: 'todo',
|
||||
@@ -129,7 +135,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
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);
|
||||
const aiEstimateHours = clampTestCaseAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
|
||||
|
||||
createTestCase({
|
||||
versionId: version.id,
|
||||
@@ -138,7 +144,8 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
description: draft.description,
|
||||
categoryId,
|
||||
priority: draft.priority,
|
||||
estimateHours,
|
||||
estimateHours: undefined,
|
||||
aiEstimateHours,
|
||||
assigneeId: undefined,
|
||||
references: refs,
|
||||
aiDraft: true,
|
||||
@@ -155,6 +162,8 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
setTimeout(() => onClose(), 1500);
|
||||
};
|
||||
|
||||
const selectedVisibleCount = (showDevDrafts ? selectedDevIdx.size : 0) + (showTestDrafts ? selectedTcIdx.size : 0);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
|
||||
<div
|
||||
@@ -178,6 +187,12 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4">
|
||||
{filteredDuplicateCount > 0 && (
|
||||
<div className="rounded-lg border border-purple-200 bg-purple-50 px-3 py-2 text-[12px] text-purple-700">
|
||||
已自动过滤 {dedupeSummary?.removedDevTaskCount ?? 0} 个重复开发任务、{dedupeSummary?.removedTestCaseCount ?? 0} 个重复测试用例。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对账报告 */}
|
||||
<section>
|
||||
<h4 className="text-[12px] font-semibold text-[var(--ink-soft)] mb-2">对账报告</h4>
|
||||
@@ -192,8 +207,14 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
</div>
|
||||
<ul className="text-[12px] text-emerald-800 space-y-1 ml-5">
|
||||
{data.report.matched.map((m, i) => (
|
||||
<li key={i}>
|
||||
{m.reqId} ↔ {m.noteIds.join(', ') || '(仅需求驱动)'} → 拆出 {m.taskCount} 个任务
|
||||
<li key={i} className="flex flex-wrap items-center gap-1">
|
||||
<span
|
||||
className="max-w-[280px] truncate font-medium"
|
||||
title={formatReportRequirementLabel(m.reqId, requirements)}
|
||||
>
|
||||
{formatReportRequirementLabel(m.reqId, requirements)}
|
||||
</span>
|
||||
<span>↔ {m.noteIds.join(', ') || '(仅需求驱动)'} → 拆出 {m.taskCount} 个任务</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -210,7 +231,10 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
以下需求在原型上未找到对应批注,已按需求文字拆解,请人工核对:
|
||||
</p>
|
||||
<ul className="text-[12px] text-amber-800 ml-5 list-disc">
|
||||
{data.report.reqOnly.map((id, i) => <li key={i}>{id}</li>)}
|
||||
{data.report.reqOnly.map((id, i) => {
|
||||
const label = formatReportRequirementLabel(id, requirements);
|
||||
return <li key={i} className="max-w-[360px] truncate" title={label}>{label}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
@@ -248,6 +272,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
</section>
|
||||
|
||||
{/* DevTask 草案 */}
|
||||
{showDevDrafts && (
|
||||
<section>
|
||||
<h4 className="text-[12px] font-semibold text-[var(--ink-soft)] mb-2">
|
||||
开发任务草案({data.devTaskDrafts.length})
|
||||
@@ -277,7 +302,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
{d.priority}
|
||||
</span>
|
||||
<span className="text-[10px] text-[var(--ink-muted)]">
|
||||
{clampDevEstimateHours(d.categoryCode, d.estimateHours)}h
|
||||
AI预估 {clampDevAiEstimateHours(d.categoryCode, d.aiEstimateHours)}h
|
||||
</span>
|
||||
</div>
|
||||
{d.description && (
|
||||
@@ -299,8 +324,10 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* TestCase 草案 */}
|
||||
{showTestDrafts && (
|
||||
<section>
|
||||
<h4 className="text-[12px] font-semibold text-[var(--ink-soft)] mb-2">
|
||||
测试用例草案({data.testCaseDrafts.length})
|
||||
@@ -330,7 +357,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
{d.priority}
|
||||
</span>
|
||||
<span className="text-[10px] text-[var(--ink-muted)]">
|
||||
{clampTestCaseEstimateHours(d.categoryCode, d.estimateHours)}h
|
||||
AI预估 {clampTestCaseAiEstimateHours(d.categoryCode, d.aiEstimateHours)}h
|
||||
</span>
|
||||
</div>
|
||||
<pre className="mt-1 text-[11px] text-[var(--ink-soft)] whitespace-pre-wrap font-sans line-clamp-3">
|
||||
@@ -352,12 +379,13 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-t border-[var(--line)]">
|
||||
<div className="text-[12px] text-[var(--ink-muted)]">
|
||||
已选 {selectedDevIdx.size} 个任务 + {selectedTcIdx.size} 个用例
|
||||
已选 {showDevDrafts ? selectedDevIdx.size : 0} 个任务 + {showTestDrafts ? selectedTcIdx.size : 0} 个用例
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -368,7 +396,7 @@ export function DecomposeReportModal({ result, version, plan, requirements, onCl
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAdopt}
|
||||
disabled={submitting || submitted || (selectedDevIdx.size === 0 && selectedTcIdx.size === 0)}
|
||||
disabled={submitting || submitted || selectedVisibleCount === 0}
|
||||
className="h-8 px-4 rounded-lg text-[12px] font-medium bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{submitted ? '✓ 已采纳' : submitting ? '采纳中...' : '采纳选中'}
|
||||
|
||||
Reference in New Issue
Block a user