feat(ai): 优化拆解重跑与结果展示
This commit is contained in:
@@ -7,9 +7,14 @@ import type { VersionWithContext } from '@/lib/derive';
|
||||
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
||||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
||||
import { api } from '@/lib/api';
|
||||
import { filterDuplicateDecomposeDrafts } from '@/lib/ai-decompose-dedupe';
|
||||
import { DecomposeReportModal } from './DecomposeReportModal';
|
||||
import type {
|
||||
AgentDecomposeTarget,
|
||||
AgentDecomposeRequest,
|
||||
AgentDecomposeResponse,
|
||||
AgentDecomposeError,
|
||||
@@ -33,12 +38,18 @@ function formatElapsed(sec: number): string {
|
||||
export function AiDecomposeButton({ plan, version }: Props) {
|
||||
const { updatePlan } = useVersionPlanStore();
|
||||
const { requirements } = useRequirementStore();
|
||||
const devTasks = useDevTaskStore((s) => s.tasks);
|
||||
const testCases = useTestCaseStore((s) => s.testCases);
|
||||
const categories = useTaskCategoryStore((s) => s.categories);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTarget, setActiveTarget] = useState<AgentDecomposeTarget | null>(null);
|
||||
const [result, setResult] = useState<AgentDecomposeResponse | null>(null);
|
||||
const [resultTarget, setResultTarget] = useState<AgentDecomposeTarget>('all');
|
||||
const [dedupeSummary, setDedupeSummary] = useState({ removedDevTaskCount: 0, removedTestCaseCount: 0 });
|
||||
const [tick, setTick] = useState(0);
|
||||
const startedAtRef = useRef<number | null>(null);
|
||||
const loading = activeTarget !== null;
|
||||
|
||||
// plan 上的状态(持久化在 localStorage)
|
||||
const persistStatus = plan.aiDecomposeStatus;
|
||||
@@ -78,14 +89,20 @@ export function AiDecomposeButton({ plan, version }: Props) {
|
||||
description: r.description,
|
||||
}));
|
||||
|
||||
const handleClick = async () => {
|
||||
const targetText = (target: AgentDecomposeTarget) => {
|
||||
if (target === 'dev_tasks') return '开发任务';
|
||||
if (target === 'test_cases') return '测试用例';
|
||||
return '任务和用例';
|
||||
};
|
||||
|
||||
const handleClick = async (target: AgentDecomposeTarget) => {
|
||||
if (loading) return;
|
||||
// 即便 persistStatus 是 in_progress,只要超过阈值就允许重新点
|
||||
if (persistStatus === 'in_progress' && !isStaleInProgress) return;
|
||||
|
||||
startedAtRef.current = Date.now();
|
||||
setTick(Date.now());
|
||||
setLoading(true);
|
||||
setActiveTarget(target);
|
||||
|
||||
updatePlan(plan.id, {
|
||||
aiDecomposeStatus: 'in_progress',
|
||||
@@ -105,6 +122,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
|
||||
members,
|
||||
versionId: version.id,
|
||||
planId: plan.id,
|
||||
target,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -119,7 +137,19 @@ export function AiDecomposeButton({ plan, version }: Props) {
|
||||
aiDecomposeError: resp.error,
|
||||
});
|
||||
} else {
|
||||
setResult(resp);
|
||||
const requirementIdSet = new Set(linkedReqs.map((req) => req.id));
|
||||
const deduped = filterDuplicateDecomposeDrafts(resp.result, {
|
||||
existingDevTasks: devTasks.filter((task) => requirementIdSet.has(task.requirementId)),
|
||||
existingTestCases: testCases.filter((testCase) => testCase.versionId === version.id),
|
||||
categories,
|
||||
requirements: linkedReqs,
|
||||
});
|
||||
setResult({ ...resp, result: deduped.result });
|
||||
setDedupeSummary({
|
||||
removedDevTaskCount: deduped.removedDevTaskCount,
|
||||
removedTestCaseCount: deduped.removedTestCaseCount,
|
||||
});
|
||||
setResultTarget(target);
|
||||
updatePlan(plan.id, {
|
||||
aiDecomposeStatus: 'completed',
|
||||
aiDecomposeError: undefined,
|
||||
@@ -132,7 +162,7 @@ export function AiDecomposeButton({ plan, version }: Props) {
|
||||
aiDecomposeError: msg,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setActiveTarget(null);
|
||||
startedAtRef.current = null;
|
||||
}
|
||||
};
|
||||
@@ -143,39 +173,48 @@ export function AiDecomposeButton({ plan, version }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={isInProgress}
|
||||
className={`inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] font-medium disabled:cursor-not-allowed disabled:opacity-70 ${
|
||||
isError
|
||||
? 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100'
|
||||
: 'border-purple-200 bg-purple-50 text-purple-700 hover:bg-purple-100'
|
||||
}`}
|
||||
title={
|
||||
isError
|
||||
? `上次失败:${persistError || '未知错误'}(点击重试)`
|
||||
: wasCompleted
|
||||
? '此前已拆解过,再次点击会重新拆解'
|
||||
: '使用 AI 把原型 + 关联需求拆解成开发任务和测试用例'
|
||||
}
|
||||
>
|
||||
{isInProgress ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
拆解中 {formatElapsed(elapsedSec)}
|
||||
</>
|
||||
) : isError ? (
|
||||
<>
|
||||
<RotateCw className="h-3 w-3" />
|
||||
重试
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
{wasCompleted ? '重新 AI 拆解' : 'AI 拆解'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{(['dev_tasks', 'test_cases'] as AgentDecomposeTarget[]).map((target) => {
|
||||
const showSpinner = isInProgress && (!activeTarget || activeTarget === target);
|
||||
const label = wasCompleted ? `重新拆解${targetText(target)}` : `AI 拆解${targetText(target)}`;
|
||||
return (
|
||||
<button
|
||||
key={target}
|
||||
onClick={() => handleClick(target)}
|
||||
disabled={isInProgress}
|
||||
className={`inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] font-medium disabled:cursor-not-allowed disabled:opacity-70 ${
|
||||
isError
|
||||
? 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100'
|
||||
: 'border-purple-200 bg-purple-50 text-purple-700 hover:bg-purple-100'
|
||||
}`}
|
||||
title={
|
||||
isError
|
||||
? `上次失败:${persistError || '未知错误'}(点击重试${targetText(target)})`
|
||||
: wasCompleted
|
||||
? `此前已拆解过,再次点击会重新拆解${targetText(target)}`
|
||||
: `使用 AI 把原型 + 关联需求拆解成${targetText(target)}`
|
||||
}
|
||||
>
|
||||
{showSpinner ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
拆解中 {formatElapsed(elapsedSec)}
|
||||
</>
|
||||
) : isError ? (
|
||||
<>
|
||||
<RotateCw className="h-3 w-3" />
|
||||
重试{targetText(target)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
{label}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
|
||||
{/* 错误信息:在按钮旁悬浮显示 */}
|
||||
{isError && persistError && (
|
||||
@@ -198,6 +237,8 @@ export function AiDecomposeButton({ plan, version }: Props) {
|
||||
version={version}
|
||||
plan={plan}
|
||||
requirements={linkedReqs}
|
||||
target={resultTarget}
|
||||
dedupeSummary={dedupeSummary}
|
||||
onClose={() => setResult(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user