feat(phase7): track batch classification jobs

This commit is contained in:
Xiaoxia AI
2026-06-17 19:48:19 +08:00
parent ac70a01f50
commit f1d5842203
+110 -12
View File
@@ -52,6 +52,26 @@ const batchModeOptions = [
{ label: '包含已分类重跑', value: 'include_classified' },
];
type BatchProgressState = {
total: number;
submitted: number;
failed: number;
skipped: number;
completed: number;
running: boolean;
tracking: boolean;
};
const defaultBatchProgress: BatchProgressState = {
total: 0,
submitted: 0,
failed: 0,
skipped: 0,
completed: 0,
running: false,
tracking: false,
};
const ProjectAssets: React.FC = () => {
const { id } = useParams<{ id: string }>();
const projectId = id || '';
@@ -65,7 +85,10 @@ const ProjectAssets: React.FC = () => {
const [classificationFilter, setClassificationFilter] = useState<string>('all');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchMode, setBatchMode] = useState<'unclassified_only' | 'include_classified'>('unclassified_only');
const [batchProgress, setBatchProgress] = useState({ total: 0, submitted: 0, failed: 0, skipped: 0, running: false });
const [batchProgress, setBatchProgress] = useState<BatchProgressState>(defaultBatchProgress);
const [batchJobIds, setBatchJobIds] = useState<string[]>([]);
const [batchCompletedIds, setBatchCompletedIds] = useState<string[]>([]);
const [batchFailedIds, setBatchFailedIds] = useState<string[]>([]);
const [form] = Form.useForm();
const librariesQuery = useQuery({
@@ -133,6 +156,50 @@ const ProjectAssets: React.FC = () => {
}
}, [classificationJobQuery.data?.status, classificationJobQuery.data?.classification, assetsQuery]);
useEffect(() => {
if (!batchJobIds.length || !batchProgress.tracking) {
return;
}
let cancelled = false;
const pollBatchJobs = async () => {
try {
const results = await Promise.all(batchJobIds.map((jobId) => getClassificationJob(jobId)));
if (cancelled) {
return;
}
const completedIds = results.filter((job) => job.status === 'completed').map((job) => job.id);
const failedIds = results.filter((job) => job.status === 'failed').map((job) => job.id);
const pendingCount = results.filter((job) => job.status === 'pending' || job.status === 'processing').length;
setBatchCompletedIds(completedIds);
setBatchFailedIds(failedIds);
setBatchProgress((current) => ({
...current,
completed: completedIds.length,
failed: current.failed < failedIds.length ? failedIds.length : current.failed,
tracking: pendingCount > 0,
running: pendingCount > 0,
}));
if (pendingCount === 0) {
assetsQuery.refetch();
}
} catch {
}
};
pollBatchJobs();
const timer = setInterval(pollBatchJobs, 2000);
return () => {
cancelled = true;
clearInterval(timer);
};
}, [batchJobIds, batchProgress.tracking, assetsQuery]);
const createLibraryMutation = useMutation({
mutationFn: createAssetLibrary,
onSuccess: (library) => {
@@ -201,31 +268,61 @@ const ProjectAssets: React.FC = () => {
return;
}
setBatchProgress({ total: selectedAssets.length, submitted: 0, failed: 0, skipped: skippedAssets.length, running: true });
setBatchProgress({
total: selectedAssets.length,
submitted: 0,
failed: 0,
skipped: skippedAssets.length,
completed: 0,
running: true,
tracking: false,
});
setBatchJobIds([]);
setBatchCompletedIds([]);
setBatchFailedIds([]);
let submitted = 0;
let failed = 0;
const createdJobIds: string[] = [];
for (const asset of runnableAssets) {
try {
await submitClassificationJob({
const job = await submitClassificationJob({
workspace_id: asset.workspace_id,
project_id: asset.project_id,
asset_id: asset.id,
});
submitted += 1;
createdJobIds.push(job.id);
} catch {
failed += 1;
}
setBatchProgress({ total: selectedAssets.length, submitted, failed, skipped: skippedAssets.length, running: true });
setBatchProgress({
total: selectedAssets.length,
submitted,
failed,
skipped: skippedAssets.length,
completed: 0,
running: true,
tracking: false,
});
}
setBatchProgress({ total: selectedAssets.length, submitted, failed, skipped: skippedAssets.length, running: false });
setBatchJobIds(createdJobIds);
setBatchProgress({
total: selectedAssets.length,
submitted,
failed,
skipped: skippedAssets.length,
completed: 0,
running: createdJobIds.length > 0,
tracking: createdJobIds.length > 0,
});
if (failed === 0) {
message.success(`批量分类已提交:成功 ${submitted},跳过 ${skippedAssets.length}`);
} else {
message.warning(`批量分类已完成:成功 ${submitted},失败 ${failed},跳过 ${skippedAssets.length}`);
message.warning(`批量分类已提交:成功 ${submitted},失败 ${failed},跳过 ${skippedAssets.length}`);
}
setSelectedRowKeys([]);
@@ -309,7 +406,7 @@ const ProjectAssets: React.FC = () => {
};
const batchPercent = batchProgress.total
? Math.round(((batchProgress.submitted + batchProgress.failed + batchProgress.skipped) / batchProgress.total) * 100)
? Math.round(((batchProgress.completed + batchProgress.failed + batchProgress.skipped) / batchProgress.total) * 100)
: 0;
return (
@@ -321,7 +418,7 @@ const ProjectAssets: React.FC = () => {
extra={
<Space>
<Select style={{ width: 180 }} value={classificationFilter} options={classificationOptions} onChange={setClassificationFilter} />
<Select style={{ width: 180 }} value={batchMode} options={batchModeOptions} onChange={setBatchMode} />
<Select style={{ width: 180 }} value={batchMode} options={batchModeOptions} onChange={(value) => setBatchMode(value)} />
<Select
style={{ width: 280 }}
placeholder="选择素材库"
@@ -361,11 +458,12 @@ const ProjectAssets: React.FC = () => {
)}
{batchProgress.total > 0 && (
<Card size="small" style={{ marginBottom: 16 }} title="批量分类进度">
<Progress percent={batchPercent} status={batchProgress.running ? 'active' : batchProgress.failed > 0 ? 'exception' : 'success'} />
<Space size="large">
<Card size="small" style={{ marginBottom: 16 }} title="批量分类追踪">
<Progress percent={batchPercent} status={batchProgress.running ? 'active' : batchFailedIds.length > 0 ? 'exception' : 'success'} />
<Space size="large" wrap>
<span>总数:{batchProgress.total}</span>
<span>成功提交:{batchProgress.submitted}</span>
<span>已提交:{batchProgress.submitted}</span>
<span>已完成:{batchProgress.completed}</span>
<span>失败:{batchProgress.failed}</span>
<span>跳过:{batchProgress.skipped}</span>
</Space>