feat(phase7): track batch classification jobs
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user