feat(phase7): add batch classification progress feedback
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Progress,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
@@ -58,6 +59,7 @@ const ProjectAssets: React.FC = () => {
|
||||
const [classifyingAssetId, setClassifyingAssetId] = useState<string>('');
|
||||
const [classificationFilter, setClassificationFilter] = useState<string>('all');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchProgress, setBatchProgress] = useState({ total: 0, submitted: 0, failed: 0, skipped: 0, running: false });
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const librariesQuery = useQuery({
|
||||
@@ -150,20 +152,14 @@ const ProjectAssets: React.FC = () => {
|
||||
});
|
||||
|
||||
const libraryOptions = useMemo(
|
||||
() =>
|
||||
(librariesQuery.data || []).map((item) => ({
|
||||
label: `${item.name} (${item.kind})`,
|
||||
value: item.id,
|
||||
})),
|
||||
() => (librariesQuery.data || []).map((item) => ({ label: `${item.name} (${item.kind})`, value: item.id })),
|
||||
[librariesQuery.data]
|
||||
);
|
||||
|
||||
const filteredAssets = useMemo(() => {
|
||||
const items = assetsQuery.data || [];
|
||||
if (classificationFilter === 'all') return items;
|
||||
if (classificationFilter === 'unclassified') {
|
||||
return items.filter((item) => !item.metadata?.classification);
|
||||
}
|
||||
if (classificationFilter === 'unclassified') return items.filter((item) => !item.metadata?.classification);
|
||||
return items.filter((item) => item.metadata?.classification === classificationFilter);
|
||||
}, [assetsQuery.data, classificationFilter]);
|
||||
|
||||
@@ -172,33 +168,71 @@ const ProjectAssets: React.FC = () => {
|
||||
[filteredAssets, selectedRowKeys]
|
||||
);
|
||||
|
||||
const selectableAssets = useMemo(
|
||||
() =>
|
||||
selectedAssets.filter(
|
||||
(item) => item.metadata?.auto_classification !== 'queued' && classifyingAssetId !== item.id
|
||||
),
|
||||
[selectedAssets, classifyingAssetId]
|
||||
);
|
||||
|
||||
const handleBatchClassification = async () => {
|
||||
if (!selectableAssets.length) {
|
||||
message.warning('没有可批量分类的素材');
|
||||
if (!selectedAssets.length) {
|
||||
message.warning('请先选择素材');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (const asset of selectableAssets) {
|
||||
|
||||
const skippedAssets = selectedAssets.filter(
|
||||
(item) => item.metadata?.auto_classification === 'queued' || classifyingAssetId === item.id
|
||||
);
|
||||
const runnableAssets = selectedAssets.filter(
|
||||
(item) => item.metadata?.auto_classification !== 'queued' && classifyingAssetId !== item.id
|
||||
);
|
||||
|
||||
if (!runnableAssets.length) {
|
||||
message.warning('选中的素材都处于分类中,无法重复提交');
|
||||
return;
|
||||
}
|
||||
|
||||
setBatchProgress({
|
||||
total: selectedAssets.length,
|
||||
submitted: 0,
|
||||
failed: 0,
|
||||
skipped: skippedAssets.length,
|
||||
running: true,
|
||||
});
|
||||
|
||||
let submitted = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const asset of runnableAssets) {
|
||||
try {
|
||||
await submitClassificationJob({
|
||||
workspace_id: asset.workspace_id,
|
||||
project_id: asset.project_id,
|
||||
asset_id: asset.id,
|
||||
});
|
||||
submitted += 1;
|
||||
} catch {
|
||||
failed += 1;
|
||||
}
|
||||
message.success(`已发起 ${selectableAssets.length} 个分类任务`);
|
||||
setSelectedRowKeys([]);
|
||||
assetsQuery.refetch();
|
||||
} catch {
|
||||
message.error('批量发起分类失败');
|
||||
setBatchProgress({
|
||||
total: selectedAssets.length,
|
||||
submitted,
|
||||
failed,
|
||||
skipped: skippedAssets.length,
|
||||
running: true,
|
||||
});
|
||||
}
|
||||
|
||||
setBatchProgress({
|
||||
total: selectedAssets.length,
|
||||
submitted,
|
||||
failed,
|
||||
skipped: skippedAssets.length,
|
||||
running: false,
|
||||
});
|
||||
|
||||
if (failed === 0) {
|
||||
message.success(`批量分类已提交:成功 ${submitted},跳过 ${skippedAssets.length}`);
|
||||
} else {
|
||||
message.warning(`批量分类已完成:成功 ${submitted},失败 ${failed},跳过 ${skippedAssets.length}`);
|
||||
}
|
||||
|
||||
setSelectedRowKeys([]);
|
||||
assetsQuery.refetch();
|
||||
};
|
||||
|
||||
const renderClassification = (asset: AssetItem) => {
|
||||
@@ -222,23 +256,9 @@ const ProjectAssets: React.FC = () => {
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'mime_type',
|
||||
key: 'mime_type',
|
||||
render: (value: string) => <Tag>{value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '分类结果',
|
||||
key: 'classification',
|
||||
render: (_: unknown, record: AssetItem) => renderClassification(record),
|
||||
},
|
||||
{
|
||||
title: '存储键',
|
||||
dataIndex: 'storage_key',
|
||||
key: 'storage_key',
|
||||
ellipsis: true,
|
||||
},
|
||||
{ title: '类型', dataIndex: 'mime_type', key: 'mime_type', render: (value: string) => <Tag>{value}</Tag> },
|
||||
{ title: '分类结果', key: 'classification', render: (_: unknown, record: AssetItem) => renderClassification(record) },
|
||||
{ title: '存储键', dataIndex: 'storage_key', key: 'storage_key', ellipsis: true },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
@@ -250,7 +270,7 @@ const ProjectAssets: React.FC = () => {
|
||||
<Button
|
||||
icon={<TagsOutlined />}
|
||||
loading={isCurrentClassifying && classificationMutation.isPending}
|
||||
disabled={isAutoClassifying || (!!classifyingAssetId && classifyingAssetId !== record.id)}
|
||||
disabled={isAutoClassifying || (!!classifyingAssetId && classifyingAssetId !== record.id) || batchProgress.running}
|
||||
onClick={() => {
|
||||
setClassifyingAssetId(record.id);
|
||||
classificationMutation.mutate({
|
||||
@@ -260,13 +280,7 @@ const ProjectAssets: React.FC = () => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isAutoClassifying
|
||||
? '自动分类中...'
|
||||
: isCurrentClassifying
|
||||
? '分类中...'
|
||||
: hasClassification
|
||||
? '重新分类'
|
||||
: '发起分类'}
|
||||
{isAutoClassifying ? '自动分类中...' : isCurrentClassifying ? '分类中...' : hasClassification ? '重新分类' : '发起分类'}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
@@ -297,6 +311,10 @@ const ProjectAssets: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchPercent = batchProgress.total
|
||||
? Math.round(((batchProgress.submitted + batchProgress.failed + batchProgress.skipped) / batchProgress.total) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
@@ -305,12 +323,7 @@ const ProjectAssets: React.FC = () => {
|
||||
title="项目素材管理"
|
||||
extra={
|
||||
<Space>
|
||||
<Select
|
||||
style={{ width: 180 }}
|
||||
value={classificationFilter}
|
||||
options={classificationOptions}
|
||||
onChange={setClassificationFilter}
|
||||
/>
|
||||
<Select style={{ width: 180 }} value={classificationFilter} options={classificationOptions} onChange={setClassificationFilter} />
|
||||
<Select
|
||||
style={{ width: 280 }}
|
||||
placeholder="选择素材库"
|
||||
@@ -319,19 +332,13 @@ const ProjectAssets: React.FC = () => {
|
||||
onChange={setLibraryId}
|
||||
loading={librariesQuery.isLoading}
|
||||
/>
|
||||
<Button icon={<TagsOutlined />} disabled={!selectedRowKeys.length} onClick={handleBatchClassification}>
|
||||
<Button icon={<TagsOutlined />} disabled={!selectedRowKeys.length || batchProgress.running} onClick={handleBatchClassification}>
|
||||
批量分类 ({selectedRowKeys.length})
|
||||
</Button>
|
||||
<Button icon={<PlusOutlined />} onClick={() => setCreateLibraryOpen(true)}>
|
||||
新建素材库
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
librariesQuery.refetch();
|
||||
assetsQuery.refetch();
|
||||
}}
|
||||
>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { librariesQuery.refetch(); assetsQuery.refetch(); }}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -351,24 +358,24 @@ const ProjectAssets: React.FC = () => {
|
||||
style={{ marginBottom: 16 }}
|
||||
type={classificationJobQuery.data.status === 'failed' ? 'error' : 'success'}
|
||||
message={`分类任务状态:${classificationJobQuery.data.status}`}
|
||||
description={
|
||||
classificationJobQuery.data.error_message ||
|
||||
`分类结果:${classificationJobQuery.data.classification || '处理中'}`
|
||||
}
|
||||
description={classificationJobQuery.data.error_message || `分类结果:${classificationJobQuery.data.classification || '处理中'}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dragger
|
||||
name="file"
|
||||
multiple={false}
|
||||
customRequest={customUpload}
|
||||
showUploadList={false}
|
||||
disabled={uploading}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
{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">
|
||||
<span>总数:{batchProgress.total}</span>
|
||||
<span>成功提交:{batchProgress.submitted}</span>
|
||||
<span>失败:{batchProgress.failed}</span>
|
||||
<span>跳过:{batchProgress.skipped}</span>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Dragger name="file" multiple={false} customRequest={customUpload} showUploadList={false} disabled={uploading} style={{ marginBottom: 24 }}>
|
||||
<p className="ant-upload-drag-icon"><InboxOutlined /></p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到这里上传素材</p>
|
||||
<p className="ant-upload-hint">支持视频、音频、图片文件;上传后自动进入导入任务</p>
|
||||
</Dragger>
|
||||
@@ -376,10 +383,7 @@ const ProjectAssets: React.FC = () => {
|
||||
{filteredAssets.length ? (
|
||||
<Table
|
||||
rowKey="id"
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
}}
|
||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
||||
columns={columns}
|
||||
dataSource={filteredAssets}
|
||||
pagination={false}
|
||||
@@ -391,13 +395,7 @@ const ProjectAssets: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title="新建素材库"
|
||||
open={createLibraryOpen}
|
||||
onCancel={() => setCreateLibraryOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={createLibraryMutation.isPending}
|
||||
>
|
||||
<Modal title="新建素材库" open={createLibraryOpen} onCancel={() => setCreateLibraryOpen(false)} onOk={() => form.submit()} confirmLoading={createLibraryMutation.isPending}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -410,12 +408,8 @@ const ProjectAssets: React.FC = () => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Form.Item label="Workspace ID">
|
||||
<Input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} />
|
||||
</Form.Item>
|
||||
<Form.Item label="素材库名称" name="name" rules={[{ required: true, message: '请输入素材库名称' }]}>
|
||||
<Input placeholder="例如:项目视频库" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Workspace ID"><Input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} /></Form.Item>
|
||||
<Form.Item label="素材库名称" name="name" rules={[{ required: true, message: '请输入素材库名称' }]}><Input placeholder="例如:项目视频库" /></Form.Item>
|
||||
<Form.Item label="素材库类型" name="kind" initialValue="video" rules={[{ required: true }]}>
|
||||
<Select options={[{ label: '视频库', value: 'video' }, { label: '配音库', value: 'voice' }]} />
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user