You've already forked DataMate
feature: add data-evaluation
* feature: add evaluation task management function * feature: add evaluation task detail page * fix: delete duplicate definition for table t_model_config * refactor: rename package synthesis to ratio * refactor: add eval file table and refactor related code * fix: calling large models in parallel during evaluation
This commit is contained in:
@@ -1,574 +1,395 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Badge,
|
||||
Input,
|
||||
Select,
|
||||
Checkbox,
|
||||
Form,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
ArrowLeftOutlined,
|
||||
EditOutlined,
|
||||
SaveOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
evaluationTemplates,
|
||||
presetEvaluationDimensions,
|
||||
sliceOperators,
|
||||
} from "@/mock/evaluation";
|
||||
import { useNavigate } from "react-router";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Form, Input, Select, message, Modal, Row, Col, Table, Space } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api.ts";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const.tsx";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis.ts";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess.tsx";
|
||||
import { createEvaluationTaskUsingPost } from "@/pages/DataEvaluation/evaluation.api.ts";
|
||||
import { queryPromptTemplatesUsingGet } from "@/pages/DataEvaluation/evaluation.api.ts";
|
||||
import PreviewPromptModal from "@/pages/DataEvaluation/Create/PreviewPrompt.tsx";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
interface Dataset {
|
||||
id: string;
|
||||
name: string;
|
||||
fileCount: number;
|
||||
size: string;
|
||||
}
|
||||
|
||||
const EvaluationTaskCreate = () => {
|
||||
const navigate = useNavigate();
|
||||
const [datasets, setDatasets] = useState([]);
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<string>("dialogue_text");
|
||||
const [allDimensions, setAllDimensions] = useState<EvaluationDimension[]>([
|
||||
...presetEvaluationDimensions,
|
||||
]);
|
||||
const [editingDimension, setEditingDimension] = useState<string | null>(null);
|
||||
const [newDimension, setNewDimension] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
datasetId: "",
|
||||
evaluationType: "model" as "model" | "manual",
|
||||
dimensions: [] as string[],
|
||||
customDimensions: [] as EvaluationDimension[],
|
||||
sliceConfig: {
|
||||
threshold: 0.8,
|
||||
sampleCount: 100,
|
||||
method: "语义分割",
|
||||
},
|
||||
modelConfig: {
|
||||
url: "",
|
||||
apiKey: "",
|
||||
prompt: "",
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000,
|
||||
},
|
||||
interface Dimension {
|
||||
key: string;
|
||||
dimension: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface PromptTemplate {
|
||||
evalType: string;
|
||||
prompt: string;
|
||||
defaultDimensions: Dimension[];
|
||||
}
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const TASK_TYPES = [
|
||||
{ label: 'QA评估', value: 'QA' },
|
||||
];
|
||||
|
||||
const EVAL_METHODS = [
|
||||
{ label: '模型自动评估', value: 'AUTO' },
|
||||
];
|
||||
|
||||
const DEFAULT_EVAL_METHOD = 'AUTO';
|
||||
const DEFAULT_TASK_TYPE = 'QA';
|
||||
|
||||
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({ visible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const [dimensions, setDimensions] = useState<Dimension[]>([]);
|
||||
const [newDimension, setNewDimension] = useState<Omit<Dimension, 'key'>>({
|
||||
dimension: '',
|
||||
description: ''
|
||||
});
|
||||
const [taskType, setTaskType] = useState<string>("QA");
|
||||
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [evaluationPrompt, setEvaluationPrompt] = useState('');
|
||||
|
||||
const handleTemplateChange = (templateKey: string) => {
|
||||
setSelectedTemplate(templateKey);
|
||||
const template =
|
||||
evaluationTemplates[templateKey as keyof typeof evaluationTemplates];
|
||||
if (template) {
|
||||
const customDimensions = allDimensions.filter((d) => d.isCustom);
|
||||
setAllDimensions([...template.dimensions, ...customDimensions]);
|
||||
const handleAddDimension = () => {
|
||||
if (!newDimension.dimension.trim()) {
|
||||
message.warning('请输入维度名称');
|
||||
return;
|
||||
}
|
||||
setDimensions([...dimensions, { ...newDimension, key: `dim-${Date.now()}` }]);
|
||||
setNewDimension({ dimension: '', description: '' });
|
||||
};
|
||||
|
||||
const handleDeleteDimension = (key: string) => {
|
||||
if (dimensions.length <= 1) {
|
||||
message.warning('至少需要保留一个评估维度');
|
||||
return;
|
||||
}
|
||||
setDimensions(dimensions.filter(item => item.key !== key));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchDatasets().then();
|
||||
fetchModels().then();
|
||||
fetchPromptTemplates().then();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
try {
|
||||
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching datasets:', error);
|
||||
message.error('获取数据集列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCustomDimension = () => {
|
||||
if (newDimension.name.trim() && newDimension.description.trim()) {
|
||||
const customDimension: EvaluationDimension = {
|
||||
id: `custom_${Date.now()}`,
|
||||
name: newDimension.name.trim(),
|
||||
description: newDimension.description.trim(),
|
||||
category: "custom",
|
||||
isCustom: true,
|
||||
isEnabled: true,
|
||||
};
|
||||
setAllDimensions([...allDimensions, customDimension]);
|
||||
setNewDimension({ name: "", description: "" });
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
message.error('获取模型列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDimensionToggle = (id: string, checked: boolean) => {
|
||||
setAllDimensions(
|
||||
allDimensions.map((d) => (d.id === id ? { ...d, isEnabled: checked } : d))
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditDimension = (
|
||||
id: string,
|
||||
field: "name" | "description",
|
||||
value: string
|
||||
) => {
|
||||
setAllDimensions(
|
||||
allDimensions.map((d) => (d.id === id ? { ...d, [field]: value } : d))
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteCustomDimension = (id: string) => {
|
||||
setAllDimensions(allDimensions.filter((d) => d.id !== id));
|
||||
};
|
||||
|
||||
const handleDeletePresetDimension = (id: string) => {
|
||||
setAllDimensions(
|
||||
allDimensions.map((d) => (d.id === id ? { ...d, isEnabled: false } : d))
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreateTask = () => {
|
||||
const selectedDataset = datasets.find((d) => d.id === createForm.datasetId);
|
||||
if (!selectedDataset) return;
|
||||
|
||||
const enabledDimensions = allDimensions.filter((d) => d.isEnabled);
|
||||
const presetDimensionIds = enabledDimensions
|
||||
.filter((d) => !d.isCustom)
|
||||
.map((d) => d.id);
|
||||
const customDimensions = enabledDimensions.filter((d) => d.isCustom);
|
||||
|
||||
let finalPrompt = createForm.modelConfig.prompt;
|
||||
if (createForm.evaluationType === "model" && !finalPrompt.trim()) {
|
||||
finalPrompt = generateDefaultPrompt(selectedDataset.name);
|
||||
}
|
||||
|
||||
const newTask: EvaluationTask = {
|
||||
id: Date.now().toString(),
|
||||
name: createForm.name,
|
||||
datasetId: createForm.datasetId,
|
||||
datasetName: selectedDataset.name,
|
||||
evaluationType: createForm.evaluationType,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
createdAt: new Date().toLocaleString(),
|
||||
description: `${
|
||||
createForm.evaluationType === "model" ? "模型自动" : "人工"
|
||||
}评估${selectedDataset.name}`,
|
||||
dimensions: presetDimensionIds,
|
||||
customDimensions: customDimensions,
|
||||
modelConfig:
|
||||
createForm.evaluationType === "model"
|
||||
? {
|
||||
...createForm.modelConfig,
|
||||
prompt: finalPrompt,
|
||||
}
|
||||
: undefined,
|
||||
metrics: {
|
||||
accuracy: 0,
|
||||
completeness: 0,
|
||||
consistency: 0,
|
||||
relevance: 0,
|
||||
},
|
||||
issues: [],
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
setCreateForm({
|
||||
name: "",
|
||||
datasetId: "",
|
||||
evaluationType: "model",
|
||||
dimensions: [],
|
||||
customDimensions: [],
|
||||
modelConfig: {
|
||||
url: "",
|
||||
apiKey: "",
|
||||
prompt: "",
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000,
|
||||
},
|
||||
const formatDimensionsForPrompt = (dimensions: Dimension[]) => {
|
||||
let result = "\n";
|
||||
dimensions.forEach((dim, index) => {
|
||||
result += `### ${index + 1}. ${dim.dimension}\n**评估标准:**\n${dim.description}\n\n`;
|
||||
});
|
||||
navigate("/data/evaluation");
|
||||
return result;
|
||||
};
|
||||
|
||||
const formatResultExample = (dimensions: Dimension[]) => {
|
||||
return dimensions.map(dim => `\n "${dim.dimension}": "Y",`).join('');
|
||||
};
|
||||
|
||||
const fetchPromptTemplates = async () => {
|
||||
try {
|
||||
const response = await queryPromptTemplatesUsingGet();
|
||||
const templates: PromptTemplate[] = response.data?.templates
|
||||
setPromptTemplates(templates)
|
||||
if (taskType) {
|
||||
const template = templates.find(t => t.evalType === taskType);
|
||||
if (template) {
|
||||
setDimensions(template.defaultDimensions.map((dim: any, index: number) => ({
|
||||
key: `dim-${index}`,
|
||||
dimension: dim.dimension,
|
||||
description: dim.description
|
||||
})));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompt templates:', error);
|
||||
message.error('获取评估维度失败');
|
||||
}
|
||||
};
|
||||
|
||||
const generateEvaluationPrompt = () => {
|
||||
if (dimensions.length === 0) {
|
||||
message.warning('请先添加评估维度');
|
||||
return;
|
||||
}
|
||||
const template = promptTemplates.find(t => t.evalType === taskType);
|
||||
setEvaluationPrompt(template?.prompt.replace("{dimensions}", formatDimensionsForPrompt(dimensions))
|
||||
.replace('{result_example}', formatResultExample(dimensions)));
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: `${model.modelName} (${model.provider})`,
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (dimensions.length === 0) {
|
||||
message.warning('请至少添加一个评估维度');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { datasetId, modelId, ...rest } = values;
|
||||
const selectedDataset = datasets.find(d => d.id === datasetId);
|
||||
const selectedModel = models.find(d => d.id === modelId);
|
||||
|
||||
const payload = {
|
||||
...rest,
|
||||
sourceType: 'DATASET',
|
||||
sourceId: datasetId,
|
||||
sourceName: selectedDataset?.name,
|
||||
evalConfig: {
|
||||
modelId: selectedModel?.id,
|
||||
dimensions: dimensions.map(d => ({
|
||||
dimension: d.dimension,
|
||||
description: d.description
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
await createEvaluationTaskUsingPost(payload);
|
||||
message.success('评估任务创建成功');
|
||||
onSuccess();
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
} catch (error: any) {
|
||||
console.error('Error creating task:', error);
|
||||
message.error(error.response?.data?.message || '创建评估任务失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'dimension',
|
||||
key: 'dimension',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '60%',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="middle">
|
||||
<a
|
||||
onClick={() => handleDeleteDimension(record.key)}
|
||||
style={{ color: dimensions.length <= 1 ? '#ccc' : '#ff4d4f' }}
|
||||
className={dimensions.length <= 1 ? 'disabled-link' : ''}
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center mb-2">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate("/data/evaluation")}
|
||||
></Button>
|
||||
<div className="text-xl font-bold">创建评估任务</div>
|
||||
</div>
|
||||
<Modal
|
||||
title="创建评估任务"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{
|
||||
evalMethod: DEFAULT_EVAL_METHOD,
|
||||
taskType: DEFAULT_TASK_TYPE,
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入任务名称' }]}
|
||||
>
|
||||
<Input placeholder="输入任务名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务类型"
|
||||
name="taskType"
|
||||
rules={[{ required: true, message: '请选择任务类型' }]}
|
||||
>
|
||||
<Select options={TASK_TYPES} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form layout="vertical">
|
||||
{/* 基本信息 */}
|
||||
<Card title="基本信息" style={{ marginBottom: 24 }}>
|
||||
<Form.Item label="任务名称" required>
|
||||
<Input
|
||||
value={createForm.name}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, name: e.target.value })
|
||||
}
|
||||
placeholder="输入任务名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="选择数据集" required>
|
||||
<Select
|
||||
value={createForm.datasetId || undefined}
|
||||
onChange={(value) =>
|
||||
setCreateForm({ ...createForm, datasetId: value })
|
||||
}
|
||||
placeholder="选择要评估的数据集"
|
||||
>
|
||||
{datasets.map((dataset) => (
|
||||
<Option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}({dataset.fileCount} 文件 • {dataset.size})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="评估方式" required>
|
||||
<Select
|
||||
value={createForm.evaluationType}
|
||||
onChange={(value: "model" | "manual") =>
|
||||
setCreateForm({ ...createForm, evaluationType: value })
|
||||
}
|
||||
>
|
||||
<Option value="model">模型自动评估</Option>
|
||||
<Option value="manual">人工评估</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* 算子配置 */}
|
||||
<Card title="切片算子配置" style={{ marginBottom: 24 }}>
|
||||
<Form.Item label="切片算子">
|
||||
<Select
|
||||
value={createForm.sliceConfig.method}
|
||||
onChange={(value) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
sliceConfig: { ...createForm.sliceConfig, method: value },
|
||||
})
|
||||
}
|
||||
placeholder="选择切片算子"
|
||||
>
|
||||
{sliceOperators.map((operator) => (
|
||||
<Option key={operator.id} value={operator.name}>
|
||||
{operator.name}{" "}
|
||||
<Badge style={{ marginLeft: 8 }} count={operator.type} />
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="分隔符">
|
||||
<Input
|
||||
placeholder="输入分隔符,如 \\n\\n"
|
||||
value={createForm.sliceConfig.delimiter}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
sliceConfig: {
|
||||
...createForm.sliceConfig,
|
||||
delimiter: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="分块大小">
|
||||
<Input
|
||||
type="number"
|
||||
value={createForm.sliceConfig.chunkSize}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
sliceConfig: {
|
||||
...createForm.sliceConfig,
|
||||
chunkSize: Number(e.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="重叠长度">
|
||||
<Input
|
||||
type="number"
|
||||
value={createForm.sliceConfig.overlapLength}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
sliceConfig: {
|
||||
...createForm.sliceConfig,
|
||||
overlapLength: Number(e.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="抽样比例">
|
||||
<Input
|
||||
type="number"
|
||||
value={createForm.sliceConfig.threshold}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
sliceConfig: {
|
||||
...createForm.sliceConfig,
|
||||
threshold: Number(e.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* 评估维度配置 */}
|
||||
<Card
|
||||
title={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span>评估维度配置</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Select
|
||||
value={selectedTemplate}
|
||||
onChange={handleTemplateChange}
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
{Object.entries(evaluationTemplates).map(
|
||||
([key, template]) => (
|
||||
<Option key={key} value={key}>
|
||||
{template.name}
|
||||
</Option>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
<Badge
|
||||
count={allDimensions.filter((d) => d.isEnabled).length}
|
||||
style={{ background: "#f0f0f0", color: "#333" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: 24 }}
|
||||
<Form.Item
|
||||
label="任务描述"
|
||||
name="description"
|
||||
>
|
||||
{/* 维度表格 */}
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: 6,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "#fafafa",
|
||||
padding: "8px 12px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
}}
|
||||
<Input.TextArea placeholder="输入任务描述(可选)" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="选择数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: '请选择数据集' }]}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div style={{ width: 60 }}>启用</div>
|
||||
<div style={{ width: 160 }}>维度名称</div>
|
||||
<div style={{ flex: 1 }}>描述</div>
|
||||
<div style={{ width: 120 }}>操作</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ maxHeight: 320, overflowY: "auto" }}>
|
||||
{allDimensions.map((dimension) => (
|
||||
<div
|
||||
key={dimension.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px 12px",
|
||||
borderBottom: "1px solid #f5f5f5",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 60 }}>
|
||||
<Checkbox
|
||||
checked={dimension.isEnabled}
|
||||
onChange={(e) =>
|
||||
handleDimensionToggle(dimension.id, e.target.checked!)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: 160 }}>
|
||||
{editingDimension === dimension.id && dimension.isCustom ? (
|
||||
<Input
|
||||
value={dimension.name}
|
||||
onChange={(e) =>
|
||||
handleEditDimension(
|
||||
dimension.id,
|
||||
"name",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
{dimension.name}
|
||||
{dimension.isCustom && (
|
||||
<Badge
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
background: "#f9f0ff",
|
||||
color: "#722ed1",
|
||||
}}
|
||||
count="自定义"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
{editingDimension === dimension.id && dimension.isCustom ? (
|
||||
<Input
|
||||
value={dimension.description}
|
||||
onChange={(e) =>
|
||||
handleEditDimension(
|
||||
dimension.id,
|
||||
"description",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<span style={{ color: "#888" }}>
|
||||
{dimension.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ width: 120 }}>
|
||||
{editingDimension === dimension.id && dimension.isCustom ? (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SaveOutlined />}
|
||||
size="small"
|
||||
onClick={() => setEditingDimension(null)}
|
||||
/>
|
||||
) : (
|
||||
dimension.isCustom && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
size="small"
|
||||
onClick={() => setEditingDimension(dimension.id)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
danger
|
||||
onClick={() =>
|
||||
dimension.isCustom
|
||||
? handleDeleteCustomDimension(dimension.id)
|
||||
: handleDeletePresetDimension(dimension.id)
|
||||
}
|
||||
disabled={
|
||||
allDimensions.filter((d) => d.isEnabled).length <= 1 &&
|
||||
dimension.isEnabled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
placeholder="请选择要评估的数据集"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{datasets.map((dataset) => (
|
||||
<Select.Option key={dataset.id} value={dataset.id} label={dataset.name}>
|
||||
<div className="flex justify-between w-full">
|
||||
<span>{dataset.name}</span>
|
||||
<span className="text-gray-500">{dataset.size}</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="评估方式"
|
||||
name="evalMethod"
|
||||
initialValue={DEFAULT_EVAL_METHOD}
|
||||
>
|
||||
<Select options={EVAL_METHODS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.evalMethod !== currentValues.evalMethod
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => getFieldValue('evalMethod') === 'AUTO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="评估模型"
|
||||
name="modelId"
|
||||
rules={[{ required: true, message: '请选择评估模型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择模型"
|
||||
options={chatModelOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="评估维度">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dimensions}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey="key"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="输入维度名称"
|
||||
value={newDimension.dimension}
|
||||
onChange={(e) => setNewDimension({...newDimension, dimension: e.target.value})}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="输入维度描述"
|
||||
value={newDimension.description}
|
||||
onChange={(e) => setNewDimension({...newDimension, description: e.target.value})}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddDimension}
|
||||
disabled={!newDimension.dimension.trim()}
|
||||
>
|
||||
添加维度
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* 添加自定义维度 */}
|
||||
<div style={{ background: "#fafafa", borderRadius: 6, padding: 16 }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8 }}>
|
||||
添加自定义维度
|
||||
</div>
|
||||
<Input
|
||||
value={newDimension.name}
|
||||
onChange={(e) =>
|
||||
setNewDimension({ ...newDimension, name: e.target.value })
|
||||
}
|
||||
placeholder="维度名称"
|
||||
style={{ width: 180, marginRight: 8 }}
|
||||
size="small"
|
||||
/>
|
||||
<Input
|
||||
value={newDimension.description}
|
||||
onChange={(e) =>
|
||||
setNewDimension({
|
||||
...newDimension,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="维度描述"
|
||||
style={{ width: 260, marginRight: 8 }}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '16px' }}>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddCustomDimension}
|
||||
disabled={
|
||||
!newDimension.name.trim() || !newDimension.description.trim()
|
||||
}
|
||||
size="small"
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={generateEvaluationPrompt}
|
||||
>
|
||||
添加维度
|
||||
查看评估提示词
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Form.Item>
|
||||
|
||||
{/* 模型配置(仅在选择模型评估时显示) */}
|
||||
{createForm.evaluationType === "model" && (
|
||||
<Card title="模型配置" style={{ marginBottom: 24 }}>
|
||||
<Form.Item label="模型 URL" required>
|
||||
<Input
|
||||
value={createForm.modelConfig.url}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
modelConfig: {
|
||||
...createForm.modelConfig,
|
||||
url: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="https://api.openai.com/v1/chat/completions"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="API Key" required>
|
||||
<Input.Password
|
||||
value={createForm.modelConfig.apiKey}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
modelConfig: {
|
||||
...createForm.modelConfig,
|
||||
apiKey: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="sk-***"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Form.Item>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 12 }}>
|
||||
<Button onClick={() => navigate("/data/evaluation")}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleCreateTask}
|
||||
disabled={
|
||||
!createForm.name ||
|
||||
!createForm.datasetId ||
|
||||
allDimensions.filter((d) => d.isEnabled).length === 0 ||
|
||||
(createForm.evaluationType === "model" &&
|
||||
(!createForm.modelConfig.url ||
|
||||
!createForm.modelConfig.apiKey))
|
||||
}
|
||||
>
|
||||
创建评估任务
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={onCancel} style={{ marginRight: 8 }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<PreviewPromptModal
|
||||
previewVisible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
evaluationPrompt={evaluationPrompt}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationTaskCreate;
|
||||
export default CreateTaskModal;
|
||||
|
||||
42
frontend/src/pages/DataEvaluation/Create/PreviewPrompt.tsx
Normal file
42
frontend/src/pages/DataEvaluation/Create/PreviewPrompt.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Button, message, Modal } from 'antd';
|
||||
|
||||
interface PreviewPromptModalProps {
|
||||
previewVisible: boolean;
|
||||
onCancel: () => void;
|
||||
evaluationPrompt: string;
|
||||
}
|
||||
|
||||
const PreviewPromptModal: React.FC<PreviewPromptModalProps> = ({ previewVisible, onCancel, evaluationPrompt }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="评估提示词预览"
|
||||
open={previewVisible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="copy" onClick={() => {
|
||||
navigator.clipboard.writeText(evaluationPrompt).then();
|
||||
message.success('已复制到剪贴板');
|
||||
}}>
|
||||
复制
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={onCancel}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '4px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{evaluationPrompt}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewPromptModal;
|
||||
152
frontend/src/pages/DataEvaluation/Detail/TaskDetail.tsx
Normal file
152
frontend/src/pages/DataEvaluation/Detail/TaskDetail.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Link, useParams } from "react-router";
|
||||
import { Tabs, Spin, message, Breadcrumb } from 'antd';
|
||||
import { LayoutList, Clock } from "lucide-react";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getEvaluationTaskByIdUsingGet, queryEvaluationItemsUsingGet } from '../evaluation.api';
|
||||
import { EvaluationTask, EvaluationStatus } from '../evaluation.model';
|
||||
import DetailHeader from "@/components/DetailHeader.tsx";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
import EvaluationItems from "@/pages/DataEvaluation/Detail/components/EvaluationItems.tsx";
|
||||
import Overview from "@/pages/DataEvaluation/Detail/components/Overview.tsx";
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
{
|
||||
key: "evaluationItems",
|
||||
label: "评估详情",
|
||||
}
|
||||
];
|
||||
|
||||
interface EvaluationItem {
|
||||
id: string;
|
||||
content: string;
|
||||
status: EvaluationStatus;
|
||||
score?: number;
|
||||
dimensions: {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
}[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const EvaluationDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [task, setTask] = useState<EvaluationTask | null>(null);
|
||||
const [items, setItems] = useState<EvaluationItem[]>([]);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
try {
|
||||
const response = await getEvaluationTaskByIdUsingGet(id);
|
||||
setTask(response.data);
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch task details');
|
||||
console.error('Error fetching task detail:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEvaluationItems = async (page = 1, pageSize = 10) => {
|
||||
try {
|
||||
const response = await queryEvaluationItemsUsingGet({
|
||||
taskId: id,
|
||||
page: page,
|
||||
size: pageSize,
|
||||
});
|
||||
setItems(response.data.content || []);
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: page,
|
||||
total: response.data.totalElements || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch evaluation items');
|
||||
console.error('Error fetching evaluation items:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
fetchTaskDetail(),
|
||||
fetchEvaluationItems(1, pagination.pageSize),
|
||||
]).finally(() => setLoading(false));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (loading && !task) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return <div>Task not found</div>;
|
||||
}
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/evaluation">数据评估</Link>,
|
||||
},
|
||||
{
|
||||
title: "数据评估详情",
|
||||
},
|
||||
];
|
||||
|
||||
const headerData = {
|
||||
...task,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
status: TaskStatusMap[task?.status],
|
||||
createdAt: task?.createdAt,
|
||||
lastUpdated: task?.updatedAt,
|
||||
};
|
||||
|
||||
// 基本信息描述项
|
||||
const statistics = [
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
key: "time",
|
||||
value: task?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = []
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full overflow-auto">
|
||||
{activeTab === "overview" && <Overview task={task} />}
|
||||
{activeTab === "evaluationItems" && <EvaluationItems task={task} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationDetailPage;
|
||||
@@ -0,0 +1,271 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Typography, Button, Space, Spin, Empty, message, Tooltip } from 'antd';
|
||||
import { FolderOpen, FileText, ArrowLeft } from 'lucide-react';
|
||||
import { queryEvaluationFilesUsingGet, queryEvaluationItemsUsingGet } from '../../evaluation.api';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const COLUMN_WIDTH = 520;
|
||||
const MONO_FONT = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
const codeBlockStyle = {
|
||||
fontFamily: MONO_FONT,
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: '#334155',
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 6,
|
||||
padding: 8,
|
||||
} as const;
|
||||
|
||||
type EvalFile = {
|
||||
taskId: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
totalCount: number;
|
||||
evaluatedCount: number;
|
||||
pendingCount: number;
|
||||
};
|
||||
|
||||
type EvalItem = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
itemId: string;
|
||||
fileId: string;
|
||||
evalContent: any;
|
||||
evalScore?: number | null;
|
||||
evalResult: any;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export default function EvaluationItems({ task }: { task: any }) {
|
||||
const [loadingFiles, setLoadingFiles] = useState<boolean>(false);
|
||||
const [files, setFiles] = useState<EvalFile[]>([]);
|
||||
const [filePagination, setFilePagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState<{ fileId: string; fileName: string } | null>(null);
|
||||
const [loadingItems, setLoadingItems] = useState<boolean>(false);
|
||||
const [items, setItems] = useState<EvalItem[]>([]);
|
||||
const [itemPagination, setItemPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
// Fetch files list
|
||||
useEffect(() => {
|
||||
if (!task?.id || selectedFile) return;
|
||||
const fetchFiles = async () => {
|
||||
setLoadingFiles(true);
|
||||
try {
|
||||
const res = await queryEvaluationFilesUsingGet({ taskId: task.id, page: filePagination.current, size: filePagination.pageSize });
|
||||
const data = res?.data;
|
||||
const list: EvalFile[] = data?.content || [];
|
||||
setFiles(list);
|
||||
setFilePagination((p) => ({ ...p, total: data?.totalElements || 0 }));
|
||||
} catch (e) {
|
||||
message.error('加载评估文件失败');
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoadingFiles(false);
|
||||
}
|
||||
};
|
||||
fetchFiles();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [task?.id, filePagination.current, filePagination.pageSize, selectedFile]);
|
||||
|
||||
// Fetch items of selected file
|
||||
useEffect(() => {
|
||||
if (!task?.id || !selectedFile) return;
|
||||
const fetchItems = async () => {
|
||||
setLoadingItems(true);
|
||||
try {
|
||||
const res = await queryEvaluationItemsUsingGet({
|
||||
taskId: task.id,
|
||||
page: itemPagination.current,
|
||||
size: itemPagination.pageSize,
|
||||
file_id: selectedFile.fileId,
|
||||
});
|
||||
const data = res?.data;
|
||||
const list: EvalItem[] = data?.content || [];
|
||||
setItems(list);
|
||||
setItemPagination((p) => ({ ...p, total: data?.totalElements || 0 }));
|
||||
} catch (e) {
|
||||
message.error('加载评估条目失败');
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoadingItems(false);
|
||||
}
|
||||
};
|
||||
fetchItems();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [task?.id, selectedFile?.fileId, itemPagination.current, itemPagination.pageSize]);
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
render: (_: any, record: EvalFile) => (
|
||||
<Space onClick={(e) => { e.stopPropagation(); setSelectedFile({ fileId: record.fileId, fileName: record.fileName }); }} style={{ cursor: 'pointer' }}>
|
||||
<FolderOpen size={16} />
|
||||
<Button type="link" style={{ padding: 0 }}>{record.fileName}</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '总条目',
|
||||
dataIndex: 'totalCount',
|
||||
key: 'totalCount',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '已评估',
|
||||
dataIndex: 'evaluatedCount',
|
||||
key: 'evaluatedCount',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '待评估',
|
||||
dataIndex: 'pendingCount',
|
||||
key: 'pendingCount',
|
||||
width: 120,
|
||||
},
|
||||
];
|
||||
|
||||
const renderEvalObject = (rec: EvalItem) => {
|
||||
const c = rec.evalContent;
|
||||
let jsonString = '';
|
||||
try {
|
||||
if (typeof c === 'string') {
|
||||
// 尝试将字符串解析为 JSON,失败则按原字符串显示
|
||||
try {
|
||||
jsonString = JSON.stringify(JSON.parse(c), null, 2);
|
||||
} catch {
|
||||
jsonString = JSON.stringify({ value: c }, null, 2);
|
||||
}
|
||||
} else {
|
||||
jsonString = JSON.stringify(c, null, 2);
|
||||
}
|
||||
} catch {
|
||||
jsonString = 'null';
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
title={<pre style={{ ...codeBlockStyle, margin: 0, maxWidth: COLUMN_WIDTH, whiteSpace: 'pre-wrap' }}>{jsonString}</pre>}
|
||||
overlayInnerStyle={{ maxHeight: 600, overflow: 'auto', width: COLUMN_WIDTH }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: MONO_FONT, fontSize: 12, lineHeight: '20px', color: '#334155' }}
|
||||
ellipsis={{ rows: 6 }}
|
||||
>
|
||||
<pre style={{ ...codeBlockStyle, whiteSpace: 'pre-wrap', margin: 0 }}>{jsonString}</pre>
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEvalResult = (rec: EvalItem) => {
|
||||
const r = rec.evalResult;
|
||||
let jsonString = '';
|
||||
try {
|
||||
if (typeof r === 'string') {
|
||||
try {
|
||||
jsonString = JSON.stringify(JSON.parse(r), null, 2);
|
||||
} catch {
|
||||
jsonString = JSON.stringify({ value: r, score: rec.evalScore ?? undefined }, null, 2);
|
||||
}
|
||||
} else {
|
||||
const withScore = rec.evalScore !== undefined && rec.evalScore !== null ? { ...r, evalScore: rec.evalScore } : r;
|
||||
jsonString = JSON.stringify(withScore, null, 2);
|
||||
}
|
||||
} catch {
|
||||
jsonString = 'null';
|
||||
}
|
||||
// 判空展示未评估
|
||||
const isEmpty = !r || (typeof r === 'string' && r.trim() === '') || (typeof r === 'object' && r !== null && Object.keys(r).length === 0);
|
||||
if (isEmpty) {
|
||||
return <Text type="secondary">未评估</Text>;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
title={<pre style={{ ...codeBlockStyle, margin: 0, maxWidth: 800, whiteSpace: 'pre-wrap' }}>{jsonString}</pre>}
|
||||
overlayInnerStyle={{ maxHeight: 600, overflow: 'auto' }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: MONO_FONT, fontSize: 12, lineHeight: '20px', color: '#334155' }}
|
||||
ellipsis={{ rows: 6 }}
|
||||
>
|
||||
<pre style={{ ...codeBlockStyle, whiteSpace: 'pre-wrap', margin: 0 }}>{jsonString}</pre>
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const itemColumns = [
|
||||
{
|
||||
title: '评估对象',
|
||||
dataIndex: 'evalContent',
|
||||
key: 'evalContent',
|
||||
render: (_: any, record: EvalItem) => renderEvalObject(record),
|
||||
width: COLUMN_WIDTH,
|
||||
},
|
||||
{
|
||||
title: '评估结果',
|
||||
dataIndex: 'evalResult',
|
||||
key: 'evalResult',
|
||||
render: (_: any, record: EvalItem) => renderEvalResult(record),
|
||||
width: COLUMN_WIDTH,
|
||||
},
|
||||
];
|
||||
|
||||
if (!task?.id) return <Empty description="任务不存在" />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{!selectedFile ? (
|
||||
<Table
|
||||
rowKey={(r: EvalFile) => r.fileId}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
loading={loadingFiles}
|
||||
size="middle"
|
||||
onRow={(record) => ({ onClick: () => setSelectedFile({ fileId: record.fileId, fileName: record.fileName }) })}
|
||||
pagination={{
|
||||
current: filePagination.current,
|
||||
pageSize: filePagination.pageSize,
|
||||
total: filePagination.total,
|
||||
onChange: (current, pageSize) => setFilePagination({ current, pageSize, total: filePagination.total }),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="sticky top-0 z-10 bg-white py-2" style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Space wrap>
|
||||
<Button icon={<ArrowLeft size={16} />} onClick={() => { setSelectedFile(null); setItems([]); }}>
|
||||
返回文件列表
|
||||
</Button>
|
||||
<Space>
|
||||
<FileText size={16} />
|
||||
<Text strong>{selectedFile.fileName}</Text>
|
||||
<Text type="secondary">文件ID:{selectedFile.fileId}</Text>
|
||||
<Text type="secondary">共 {itemPagination.total} 条</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
rowKey={(r: EvalItem) => r.id}
|
||||
columns={itemColumns}
|
||||
dataSource={items}
|
||||
loading={loadingItems}
|
||||
size="middle"
|
||||
pagination={{
|
||||
current: itemPagination.current,
|
||||
pageSize: itemPagination.pageSize,
|
||||
total: itemPagination.total,
|
||||
onChange: (current, pageSize) => setItemPagination({ current, pageSize, total: itemPagination.total }),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
frontend/src/pages/DataEvaluation/Detail/components/Overview.tsx
Normal file
122
frontend/src/pages/DataEvaluation/Detail/components/Overview.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useState } from 'react';
|
||||
import { Descriptions, Empty, DescriptionsProps, Table, Button, message } from 'antd';
|
||||
import { CheckCircle, XCircle, Clock as ClockIcon } from 'lucide-react';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import { EvaluationStatus } from '../../evaluation.model';
|
||||
import PreviewPromptModal from "@/pages/DataEvaluation/Create/PreviewPrompt.tsx";
|
||||
|
||||
const statusMap = {
|
||||
[EvaluationStatus.PENDING]: { color: 'blue', text: '待处理', icon: <ClockIcon className="mr-1" size={14} /> },
|
||||
[EvaluationStatus.RUNNING]: { color: 'processing', text: '进行中', icon: <ClockIcon className="mr-1" size={14} /> },
|
||||
[EvaluationStatus.COMPLETED]: { color: 'success', text: '已完成', icon: <CheckCircle className="mr-1" size={14} /> },
|
||||
[EvaluationStatus.FAILED]: { color: 'error', text: '失败', icon: <XCircle className="mr-1" size={14} /> },
|
||||
[EvaluationStatus.PAUSED]: { color: 'warning', text: '已暂停', icon: <ClockIcon className="mr-1" size={14} /> },
|
||||
};
|
||||
|
||||
const Overview = ({ task }) => {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
if (!task) {
|
||||
return <Empty description="未找到评估任务信息" />;
|
||||
}
|
||||
|
||||
const generateEvaluationPrompt = () => {
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
const statusInfo = statusMap[task.status] || { color: 'default', text: '未知状态' };
|
||||
|
||||
// 基本信息
|
||||
const items: DescriptionsProps["items"] = [
|
||||
{
|
||||
key: "id",
|
||||
label: "ID",
|
||||
children: task.id,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "名称",
|
||||
children: task.name,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
children: statusInfo.text || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdBy",
|
||||
label: "创建者",
|
||||
children: task.createdBy || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: task.createdAt,
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: task.updatedAt,
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: task.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'dimension',
|
||||
key: 'dimension',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '60%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Descriptions
|
||||
title="基本信息"
|
||||
layout="vertical"
|
||||
size="small"
|
||||
items={items}
|
||||
column={5}
|
||||
/>
|
||||
<h2 className="text-base font-semibold mt-8">评估维度</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={task?.evalConfig?.dimensions}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={generateEvaluationPrompt}
|
||||
>
|
||||
查看评估提示词
|
||||
</Button>
|
||||
</div>
|
||||
<PreviewPromptModal
|
||||
previewVisible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
evaluationPrompt={task?.evalPrompt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@@ -1,482 +1,267 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Card, Badge, Progress, Table } from "antd";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Progress,
|
||||
Popconfirm,
|
||||
App,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
ClockCircleOutlined,
|
||||
DatabaseOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
ReloadOutlined,
|
||||
UserOutlined,
|
||||
RobotOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { mockTasks } from "@/mock/evaluation";
|
||||
import CardView from "@/components/CardView";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
import { deleteEvaluationTaskUsingGet, getPagedEvaluationTaskUsingGet } from "@/pages/DataEvaluation/evaluation.api";
|
||||
import CardView from "@/components/CardView";
|
||||
import CreateTaskModal from "@/pages/DataEvaluation/Create/CreateTask.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import { EvaluationTask } from "@/pages/DataEvaluation/evaluation.model.ts";
|
||||
import { mapEvaluationTask } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const statusMap = {
|
||||
PENDING: { text: '等待中', color: 'warning'},
|
||||
RUNNING: { text: '运行中', color: 'processing'},
|
||||
COMPLETED: { text: '已完成', color: 'success'},
|
||||
STOPPED: { text: '已停止', color: 'default'},
|
||||
FAILED: { text: '失败', color: 'error'},
|
||||
};
|
||||
|
||||
export default function DataEvaluationPage() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [tasks, setTasks] = useState<EvaluationTask[]>(mockTasks);
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
fetchData,
|
||||
} = useFetchData<EvaluationTask>(
|
||||
getPagedEvaluationTaskUsingGet,
|
||||
mapEvaluationTask,
|
||||
30000,
|
||||
true,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
// 搜索和过滤状态
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedFilters, setSelectedFilters] = useState<
|
||||
Record<string, string[]>
|
||||
>({});
|
||||
const [sortBy, setSortBy] = useState("createdAt");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [viewMode, setViewMode] = useState<"card" | "table">("card");
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("evaluation-tasks", JSON.stringify(tasks));
|
||||
const handleDeleteTask = async (task: EvaluationTask) => {
|
||||
try {
|
||||
// 调用删除接口
|
||||
await deleteEvaluationTaskUsingGet(task.id);
|
||||
message.success("任务删除成功");
|
||||
// 重新加载数据
|
||||
fetchData().then();
|
||||
} catch (error) {
|
||||
message.error("任务删除失败,请稍后重试");
|
||||
}
|
||||
}, [tasks]);
|
||||
};
|
||||
|
||||
// 搜索和过滤配置
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "evaluationType",
|
||||
label: "评估方式",
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
options: Object.entries(statusMap).map(([value, { text }]) => ({
|
||||
value,
|
||||
label: text,
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'taskType',
|
||||
label: '任务类型',
|
||||
options: [
|
||||
{ label: "模型评估", value: "model" },
|
||||
{ label: "人工评估", value: "manual" },
|
||||
{ value: 'QA', label: 'QA评估' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
key: 'evalMethod',
|
||||
label: '评估方式',
|
||||
options: [
|
||||
{ label: "待处理", value: "pending" },
|
||||
{ label: "运行中", value: "running" },
|
||||
{ label: "已完成", value: "completed" },
|
||||
{ label: "失败", value: "failed" },
|
||||
{ value: 'AUTO', label: '自动评估' },
|
||||
{ value: 'MANUAL', label: '人工评估' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "dataset",
|
||||
label: "数据集",
|
||||
options: datasets.map((d) => ({ label: d.name, value: d.id })),
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/evaluation/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务类型',
|
||||
dataIndex: 'taskType',
|
||||
key: 'taskType',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'QA' ? 'blue' : 'default'}>
|
||||
{text === 'QA' ? 'QA评估' : text}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '评估方式',
|
||||
dataIndex: 'evalMethod',
|
||||
key: 'evalMethod',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'AUTO' ? 'geekblue' : 'orange'}>
|
||||
{text === 'AUTO' ? '自动评估' : '人工评估'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: any) => {
|
||||
return (<Tag color={status.color}> {status.label} </Tag>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'evalProcess',
|
||||
key: 'evalProcess',
|
||||
render: (progress: number, record: EvaluationTask) => (
|
||||
<Progress
|
||||
percent={Math.round(progress * 100)}
|
||||
size="small"
|
||||
status={record.status === 'FAILED' ? 'exception' : 'active'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, task: EvaluationTask) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => {
|
||||
if (op.confirm) {
|
||||
<Popconfirm
|
||||
title={op.confirm.title}
|
||||
description={op.confirm.description}
|
||||
onConfirm={() => op.onClick(task)}
|
||||
>
|
||||
<Button type="text" icon={op.icon} />
|
||||
</Popconfirm>;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={op.key}
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(task)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const sortOptions = [
|
||||
{ label: "创建时间", value: "createdAt" },
|
||||
{ label: "任务名称", value: "name" },
|
||||
{ label: "完成时间", value: "completedAt" },
|
||||
{ label: "评分", value: "score" },
|
||||
const operations = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该任务?",
|
||||
description: "删除后该任务将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteTask,
|
||||
}
|
||||
];
|
||||
|
||||
// 过滤和排序逻辑
|
||||
const filteredTasks = tasks.filter((task) => {
|
||||
// 搜索过滤
|
||||
if (
|
||||
searchTerm &&
|
||||
!task.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||
!task.datasetName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 评估方式过滤
|
||||
if (
|
||||
selectedFilters.evaluationType?.length &&
|
||||
!selectedFilters.evaluationType.includes(task.evaluationType)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (
|
||||
selectedFilters.status?.length &&
|
||||
!selectedFilters.status.includes(task.status)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 数据集过滤
|
||||
if (
|
||||
selectedFilters.dataset?.length &&
|
||||
!selectedFilters.dataset.includes(task.datasetId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 排序
|
||||
const sortedTasks = [...filteredTasks].sort((a, b) => {
|
||||
let aValue: any = a[sortBy as keyof EvaluationTask];
|
||||
let bValue: any = b[sortBy as keyof EvaluationTask];
|
||||
|
||||
if (sortBy === "score") {
|
||||
aValue = a.score || 0;
|
||||
bValue = b.score || 0;
|
||||
}
|
||||
|
||||
if (typeof aValue === "string") {
|
||||
aValue = aValue.toLowerCase();
|
||||
bValue = bValue.toLowerCase();
|
||||
}
|
||||
|
||||
if (sortOrder === "asc") {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "running":
|
||||
return "blue";
|
||||
case "failed":
|
||||
return "red";
|
||||
case "pending":
|
||||
return "gold";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <CheckCircleOutlined />;
|
||||
case "running":
|
||||
return <ReloadOutlined spin />;
|
||||
case "failed":
|
||||
return <CloseCircleOutlined />;
|
||||
case "pending":
|
||||
return <ClockCircleOutlined />;
|
||||
default:
|
||||
return <ExclamationCircleOutlined />;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始人工评估
|
||||
const handleStartManualEvaluation = (task: EvaluationTask) => {
|
||||
navigate(`/data/evaluation/manual-evaluate/${task.id}`);
|
||||
};
|
||||
|
||||
// 查看评估报告
|
||||
const handleViewReport = (task: EvaluationTask) => {
|
||||
navigate(`/data/evaluation/task-report/${task.id}`);
|
||||
};
|
||||
|
||||
// 删除任务
|
||||
const handleDeleteTask = (taskId: string) => {
|
||||
setTasks(tasks.filter((task) => task.id !== taskId));
|
||||
};
|
||||
|
||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
||||
// 主列表界面
|
||||
return (
|
||||
<div>
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据评估</h1>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title level={4} style={{ margin: 0 }}>数据评估</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate("/data/evaluation/create-task")}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
创建评估任务
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 搜索和过滤控件 */}
|
||||
<SearchControls
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="搜索任务名称或数据集..."
|
||||
filters={filterOptions}
|
||||
selectedFilters={selectedFilters}
|
||||
onFiltersChange={setSelectedFilters}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
|
||||
{/* 任务列表 */}
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={sortedTasks.map((task) => ({
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
type: task.evaluationType,
|
||||
icon:
|
||||
task.evaluationType === "model" ? (
|
||||
<RobotOutlined style={{ fontSize: 24, color: "#722ed1" }} />
|
||||
) : (
|
||||
<UserOutlined style={{ fontSize: 24, color: "#52c41a" }} />
|
||||
),
|
||||
iconColor: "",
|
||||
status: {
|
||||
label:
|
||||
task.status === "completed"
|
||||
? "已完成"
|
||||
: task.status === "running"
|
||||
? "运行中"
|
||||
: task.status === "failed"
|
||||
? "失败"
|
||||
: "待处理",
|
||||
icon: getStatusIcon(task.status),
|
||||
color: getStatusColor(task.status),
|
||||
},
|
||||
description: task.description,
|
||||
tags: [task.datasetName],
|
||||
statistics: [
|
||||
{
|
||||
label: "进度",
|
||||
value: task.progress !== undefined ? `${task.progress}%` : "-",
|
||||
},
|
||||
{ label: "评分", value: task.score ? `${task.score}分` : "-" },
|
||||
],
|
||||
lastModified: task.createdAt,
|
||||
}))}
|
||||
operations={[
|
||||
{
|
||||
key: "view",
|
||||
label: "查看报告",
|
||||
icon: <EyeOutlined />,
|
||||
onClick: (item) => {
|
||||
const task = tasks.find((t) => t.id === item.id);
|
||||
if (task) handleViewReport(task);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "evaluate",
|
||||
label: "开始评估",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item) => {
|
||||
const task = tasks.find((t) => t.id === item.id);
|
||||
if (task) handleStartManualEvaluation(task);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: (item) => handleDeleteTask(item.id as string),
|
||||
},
|
||||
]}
|
||||
onView={(item) => {
|
||||
const task = tasks.find((t) => t.id === item.id);
|
||||
if (task) handleViewReport(task);
|
||||
}}
|
||||
<>
|
||||
{/* 搜索、筛选和视图控制 */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(keyword) =>
|
||||
setSearchParams({ ...searchParams, keyword })
|
||||
}
|
||||
searchPlaceholder="搜索任务名称..."
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() =>
|
||||
setSearchParams({ ...searchParams, filter: {} })
|
||||
}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={sortedTasks}
|
||||
pagination={false}
|
||||
scroll={{ x: "max-content" }}
|
||||
columns={[
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{text}</div>
|
||||
<div style={{ fontSize: 13, color: "#888" }}>
|
||||
{record.description}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
render: (text) => (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||
>
|
||||
<DatabaseOutlined />
|
||||
<span style={{ fontSize: 13 }}>{text}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "评估方式",
|
||||
dataIndex: "evaluationType",
|
||||
key: "evaluationType",
|
||||
render: (type) => (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||
>
|
||||
{type === "model" ? (
|
||||
<RobotOutlined style={{ color: "#722ed1" }} />
|
||||
) : (
|
||||
<UserOutlined style={{ color: "#52c41a" }} />
|
||||
)}
|
||||
<span style={{ fontSize: 13 }}>
|
||||
{type === "model" ? "模型评估" : "人工评估"}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status) => (
|
||||
<Badge
|
||||
color={getStatusColor(status)}
|
||||
style={{ background: "none", padding: 0 }}
|
||||
count={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{getStatusIcon(status)}
|
||||
<span>
|
||||
{status === "completed" && "已完成"}
|
||||
{status === "running" && "运行中"}
|
||||
{status === "failed" && "失败"}
|
||||
{status === "pending" && "待处理"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
showZero={false}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
render: (progress) =>
|
||||
progress !== undefined ? (
|
||||
<div style={{ width: 100 }}>
|
||||
<Progress
|
||||
percent={progress}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "#888",
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{progress}%
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, color: "#bbb" }}>-</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "评分",
|
||||
dataIndex: "score",
|
||||
key: "score",
|
||||
render: (score) =>
|
||||
score ? (
|
||||
<span style={{ fontWeight: 500, color: "#389e0d" }}>
|
||||
{score}分
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, color: "#bbb" }}>-</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
render: (text) => <span style={{ fontSize: 13 }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, task) => (
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{task.status === "completed" && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewReport(task)}
|
||||
>
|
||||
报告
|
||||
</Button>
|
||||
)}
|
||||
{task.evaluationType === "manual" &&
|
||||
task.status === "pending" && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleStartManualEvaluation(task)}
|
||||
>
|
||||
评估
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteTask(task.id)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div
|
||||
style={{ textAlign: "center", padding: 48, color: "#bbb" }}
|
||||
>
|
||||
<DatabaseOutlined style={{ fontSize: 48, marginBottom: 8 }} />
|
||||
<div style={{ marginTop: 8 }}>暂无评估任务</div>
|
||||
<div style={{ fontSize: 13, color: "#ccc" }}>
|
||||
点击"创建评估任务"开始评估数据集质量
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{/* 任务列表 */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(task) => {
|
||||
navigate(`/data/evaluation/detail/${task.id}`);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{sortedTasks.length === 0 && (
|
||||
<div style={{ textAlign: "center", padding: "48px 0" }}>
|
||||
<DatabaseOutlined
|
||||
style={{ fontSize: 64, color: "#bbb", marginBottom: 16 }}
|
||||
/>
|
||||
<div style={{ fontSize: 18, fontWeight: 500, marginBottom: 8 }}>
|
||||
暂无评估任务
|
||||
</div>
|
||||
<div style={{ color: "#888", marginBottom: 24 }}>
|
||||
创建您的第一个数据评估任务
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate("/data/evaluation/create-task")}
|
||||
>
|
||||
创建评估任务
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
<CreateTaskModal
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setIsModalVisible(false);
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
interface EvaluationDimension {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: "quality" | "accuracy" | "completeness" | "consistency" | "bias" | "custom"
|
||||
isCustom?: boolean
|
||||
isEnabled?: boolean
|
||||
}
|
||||
|
||||
interface EvaluationTask {
|
||||
id: string
|
||||
name: string
|
||||
datasetId: string
|
||||
datasetName: string
|
||||
evaluationType: "model" | "manual"
|
||||
status: "running" | "completed" | "failed" | "pending"
|
||||
score?: number
|
||||
progress?: number
|
||||
createdAt: string
|
||||
completedAt?: string
|
||||
description: string
|
||||
dimensions: string[]
|
||||
customDimensions: EvaluationDimension[]
|
||||
sliceConfig?: {
|
||||
threshold: number
|
||||
sampleCount: number
|
||||
method: string
|
||||
}
|
||||
modelConfig?: {
|
||||
url: string
|
||||
apiKey: string
|
||||
prompt: string
|
||||
temperature: number
|
||||
maxTokens: number
|
||||
}
|
||||
metrics: {
|
||||
accuracy: number
|
||||
completeness: number
|
||||
consistency: number
|
||||
relevance: number
|
||||
}
|
||||
issues: {
|
||||
type: string
|
||||
count: number
|
||||
severity: "high" | "medium" | "low"
|
||||
}[]
|
||||
}
|
||||
|
||||
interface EvaluationSlice {
|
||||
id: string
|
||||
content: string
|
||||
sourceFile: string
|
||||
sliceIndex: number
|
||||
sliceType: string
|
||||
metadata: {
|
||||
startPosition?: number
|
||||
endPosition?: number
|
||||
pageNumber?: number
|
||||
section?: string
|
||||
processingMethod: string
|
||||
}
|
||||
scores?: { [dimensionId: string]: number }
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface QAPair {
|
||||
id: string
|
||||
question: string
|
||||
answer: string
|
||||
sliceId: string
|
||||
score: number
|
||||
feedback?: string
|
||||
}
|
||||
@@ -1,5 +1,46 @@
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
export function createEvaluationTaskUsingPost(data: any) {
|
||||
return post("/api/evaluation/tasks", data);
|
||||
}
|
||||
|
||||
export function getPagedEvaluationTaskUsingGet(params?: any) {
|
||||
return get("/api/evaluation/tasks", params);
|
||||
}
|
||||
|
||||
export function deleteEvaluationTaskUsingGet(id: string) {
|
||||
const url = `/api/evaluation/tasks?ids=${id}`;
|
||||
return del(url);
|
||||
}
|
||||
|
||||
export function queryPromptTemplatesUsingGet() {
|
||||
return get("/api/evaluation/prompt-templates");
|
||||
}
|
||||
|
||||
export function getEvaluationTaskByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/evaluation/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function queryEvaluationFilesUsingGet(params: {
|
||||
taskId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}) {
|
||||
const { taskId, ...rest } = params;
|
||||
return get(`/api/evaluation/tasks/${taskId}/files`, rest);
|
||||
}
|
||||
|
||||
export function queryEvaluationItemsUsingGet(params: {
|
||||
taskId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: string;
|
||||
file_id?: string;
|
||||
}) {
|
||||
const { taskId, ...rest } = params;
|
||||
return get(`/api/evaluation/tasks/${taskId}/items`, rest);
|
||||
}
|
||||
|
||||
// 数据质量评估相关接口
|
||||
export function evaluateDataQualityUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/quality", data);
|
||||
@@ -113,14 +154,6 @@ export function queryEvaluationTasksUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/tasks", params);
|
||||
}
|
||||
|
||||
export function createEvaluationTaskUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/tasks", data);
|
||||
}
|
||||
|
||||
export function getEvaluationTaskByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/v1/evaluation/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function cancelEvaluationTaskUsingPost(taskId: string | number) {
|
||||
return post(`/api/v1/evaluation/tasks/${taskId}/cancel`);
|
||||
}
|
||||
@@ -240,4 +273,4 @@ export function enableEvaluationScheduleUsingPost(scheduleId: string | number) {
|
||||
|
||||
export function disableEvaluationScheduleUsingPost(scheduleId: string | number) {
|
||||
return post(`/api/v1/evaluation/schedules/${scheduleId}/disable`);
|
||||
}
|
||||
}
|
||||
67
frontend/src/pages/DataEvaluation/evaluation.const.tsx
Normal file
67
frontend/src/pages/DataEvaluation/evaluation.const.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { formatDate } from "@/utils/unit";
|
||||
import { BarChart3 } from "lucide-react";
|
||||
import { EvaluationStatus, EvaluationTask } from "@/pages/DataEvaluation/evaluation.model.ts";
|
||||
|
||||
export const evalTaskStatusMap: Record<
|
||||
string,
|
||||
{
|
||||
value: EvaluationStatus;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
> = {
|
||||
[EvaluationStatus.PENDING]: {
|
||||
value: EvaluationStatus.PENDING,
|
||||
label: "等待中",
|
||||
color: "gray",
|
||||
},
|
||||
[EvaluationStatus.RUNNING]: {
|
||||
value: EvaluationStatus.RUNNING,
|
||||
label: "运行中",
|
||||
color: "blue",
|
||||
},
|
||||
[EvaluationStatus.COMPLETED]: {
|
||||
value: EvaluationStatus.COMPLETED,
|
||||
label: "已完成",
|
||||
color: "green",
|
||||
},
|
||||
[EvaluationStatus.FAILED]: {
|
||||
value: EvaluationStatus.FAILED,
|
||||
label: "失败",
|
||||
color: "red",
|
||||
},
|
||||
[EvaluationStatus.PAUSED]: {
|
||||
value: EvaluationStatus.PAUSED,
|
||||
label: "已暂停",
|
||||
color: "orange",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapEvaluationTask(task: Partial<EvaluationTask>): EvaluationTask {
|
||||
return {
|
||||
...task,
|
||||
status: evalTaskStatusMap[task.status || EvaluationStatus.PENDING],
|
||||
createdAt: formatDate(task.createdAt),
|
||||
updatedAt: formatDate(task.updatedAt),
|
||||
description: task.description,
|
||||
icon: <BarChart3 />,
|
||||
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
|
||||
statistics: [
|
||||
{
|
||||
label: "任务类型",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.taskType ?? 0).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: "评估方式",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.evalMethod ?? 0).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: "数据源",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.sourceName ?? 0).toLocaleString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
57
frontend/src/pages/DataEvaluation/evaluation.model.ts
Normal file
57
frontend/src/pages/DataEvaluation/evaluation.model.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export enum EvaluationStatus {
|
||||
PENDING = "PENDING",
|
||||
RUNNING = "RUNNING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
PAUSED = "PAUSED",
|
||||
}
|
||||
|
||||
export interface EvaluationTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
taskType: string;
|
||||
sourceType: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'STOPPED' | 'FAILED';
|
||||
evalProcess: number;
|
||||
evalMethod: 'AUTO' | 'MANUAL';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface EvaluationDimension {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: "quality" | "accuracy" | "completeness" | "consistency" | "bias" | "custom"
|
||||
isCustom?: boolean
|
||||
isEnabled?: boolean
|
||||
}
|
||||
|
||||
interface EvaluationSlice {
|
||||
id: string
|
||||
content: string
|
||||
sourceFile: string
|
||||
sliceIndex: number
|
||||
sliceType: string
|
||||
metadata: {
|
||||
startPosition?: number
|
||||
endPosition?: number
|
||||
pageNumber?: number
|
||||
section?: string
|
||||
processingMethod: string
|
||||
}
|
||||
scores?: { [dimensionId: string]: number }
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface QAPair {
|
||||
id: string
|
||||
question: string
|
||||
answer: string
|
||||
sliceId: string
|
||||
score: number
|
||||
feedback?: string
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export interface Dataset {
|
||||
updatedAt: string;
|
||||
tags: string[];
|
||||
targetLocation?: string;
|
||||
distribution?: Record<string, number>;
|
||||
distribution?: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
export interface TagItem {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Button, Form, message } from "antd";
|
||||
import { ArrowLeft, ChevronRight } from "lucide-react";
|
||||
import { createRatioTaskUsingPost } from "@/pages/RatioTask/ratio.api.ts";
|
||||
|
||||
@@ -2,14 +2,12 @@ import React, { useMemo, useState, useEffect, FC } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Progress,
|
||||
Button,
|
||||
Select,
|
||||
Table,
|
||||
InputNumber,
|
||||
Space,
|
||||
} from "antd";
|
||||
import { BarChart3, Filter } from "lucide-react";
|
||||
import { BarChart3 } from "lucide-react";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
||||
|
||||
const TIME_RANGE_OPTIONS = [
|
||||
@@ -20,6 +18,11 @@ const TIME_RANGE_OPTIONS = [
|
||||
{ label: '最近30天', value: 30 },
|
||||
];
|
||||
|
||||
interface LabelFilter {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RatioConfigItem {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -27,7 +30,7 @@ interface RatioConfigItem {
|
||||
quantity: number;
|
||||
percentage: number;
|
||||
source: string; // dataset id
|
||||
labelFilter?: string;
|
||||
labelFilter?: LabelFilter;
|
||||
dateRange?: number;
|
||||
}
|
||||
|
||||
@@ -36,7 +39,8 @@ interface RatioConfigProps {
|
||||
selectedDatasets: string[];
|
||||
datasets: Dataset[];
|
||||
totalTargetCount: number;
|
||||
distributions: Record<string, Record<string, number>>;
|
||||
// distributions now: { datasetId: { labelName: { labelValue: count } } }
|
||||
distributions: Record<string, Record<string, Record<string, number>>>;
|
||||
onChange?: (configs: RatioConfigItem[]) => void;
|
||||
}
|
||||
|
||||
@@ -63,6 +67,10 @@ const RatioConfig: FC<RatioConfigProps> = ({
|
||||
return Object.keys(dist);
|
||||
};
|
||||
|
||||
const getLabelValues = (datasetId: string, label: string): string[] => {
|
||||
return Object.keys(distributions[String(datasetId)]?.[label] || {});
|
||||
};
|
||||
|
||||
const addConfig = (datasetId: string) => {
|
||||
const dataset = datasets.find((d) => String(d.id) === datasetId);
|
||||
const newConfig: RatioConfigItem = {
|
||||
@@ -208,46 +216,85 @@ const RatioConfig: FC<RatioConfigProps> = ({
|
||||
);
|
||||
|
||||
const labels = getDatasetLabels(datasetId);
|
||||
const usedLabels = datasetConfigs
|
||||
.map((c) => c.labelFilter)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
// helper: used values per label for this dataset (exclude a given row when needed)
|
||||
const getUsedValuesForLabel = (label: string, excludeId?: string) => {
|
||||
return new Set(
|
||||
datasetConfigs
|
||||
.filter((c) => c.id !== excludeId && c.labelFilter?.label === label)
|
||||
.map((c) => c.labelFilter?.value)
|
||||
.filter(Boolean) as string[]
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "配比项",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
render: (_: any, record: RatioConfigItem) => (
|
||||
<Space>
|
||||
<Filter size={14} className="text-gray-400" />
|
||||
<span className="text-sm">{record.name}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "标签筛选",
|
||||
title: "标签",
|
||||
dataIndex: "labelFilter",
|
||||
key: "labelFilter",
|
||||
render: (_: any, record: RatioConfigItem) => {
|
||||
const availableLabels = labels
|
||||
.map((l) => ({ label: l, value: l }))
|
||||
.filter(
|
||||
(opt) =>
|
||||
opt.value === record.labelFilter ||
|
||||
!usedLabels.includes(opt.value)
|
||||
);
|
||||
.map((l) => ({
|
||||
label: l,
|
||||
value: l,
|
||||
disabled: getLabelValues(datasetId, l).every((v) => getUsedValuesForLabel(l, record.id).has(v)),
|
||||
}))
|
||||
return (
|
||||
<Select
|
||||
style={{ width: "160px" }}
|
||||
placeholder="选择标签"
|
||||
value={record.labelFilter}
|
||||
value={record.labelFilter?.label}
|
||||
options={availableLabels}
|
||||
allowClear
|
||||
onChange={(value) =>
|
||||
onChange={(value) => {
|
||||
if (!value) {
|
||||
updateConfig(record.id, { labelFilter: undefined });
|
||||
} else {
|
||||
// reset value when label changes
|
||||
updateConfig(record.id, {
|
||||
labelFilter: { label: value, value: "" },
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "标签值",
|
||||
dataIndex: "labelValue",
|
||||
key: "labelValue",
|
||||
render: (_: any, record: RatioConfigItem) => {
|
||||
const selectedLabel = record.labelFilter?.label;
|
||||
const options = selectedLabel
|
||||
? getLabelValues(datasetId, selectedLabel).map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
disabled: datasetConfigs.some(
|
||||
(c) =>
|
||||
c.id !== record.id &&
|
||||
c.labelFilter?.label === selectedLabel &&
|
||||
c.labelFilter?.value === v
|
||||
),
|
||||
}))
|
||||
: [];
|
||||
return (
|
||||
<Select
|
||||
style={{ width: "180px" }}
|
||||
placeholder="选择标签值"
|
||||
value={record.labelFilter?.value || undefined}
|
||||
options={options}
|
||||
allowClear
|
||||
disabled={!selectedLabel}
|
||||
onChange={(value) => {
|
||||
if (!selectedLabel) return;
|
||||
updateConfig(record.id, {
|
||||
labelFilter: value || undefined,
|
||||
})
|
||||
}
|
||||
labelFilter: {
|
||||
label: selectedLabel,
|
||||
value: value || "",
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -286,23 +333,6 @@ const RatioConfig: FC<RatioConfigProps> = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "占比",
|
||||
dataIndex: "percentage",
|
||||
key: "percentage",
|
||||
render: (_: any, record: RatioConfigItem) => (
|
||||
<div style={{ minWidth: 140 }}>
|
||||
<div className="text-xs mb-1">
|
||||
{record.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress
|
||||
percent={record.percentage ?? 0}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
dataIndex: "actions",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// typescript
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Badge, Button, Card, Checkbox, Input, Pagination } from "antd";
|
||||
import { Search as SearchIcon } from "lucide-react";
|
||||
@@ -5,32 +6,48 @@ import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
||||
import {
|
||||
queryDatasetsUsingGet,
|
||||
queryDatasetByIdUsingGet,
|
||||
queryDatasetStatisticsByIdUsingGet,
|
||||
} from "@/pages/DataManagement/dataset.api.ts";
|
||||
|
||||
interface SelectDatasetProps {
|
||||
selectedDatasets: string[];
|
||||
onSelectedDatasetsChange: (next: string[]) => void;
|
||||
// distributions now: { datasetId: { labelName: { labelValue: count } } }
|
||||
onDistributionsChange?: (
|
||||
next: Record<string, Record<string, number>>
|
||||
next: Record<string, Record<string, Record<string, number>>>
|
||||
) => void;
|
||||
onDatasetsChange?: (list: Dataset[]) => void;
|
||||
}
|
||||
|
||||
const SelectDataset: React.FC<SelectDatasetProps> = ({
|
||||
selectedDatasets,
|
||||
onSelectedDatasetsChange,
|
||||
onDistributionsChange,
|
||||
onDatasetsChange,
|
||||
}) => {
|
||||
selectedDatasets,
|
||||
onSelectedDatasetsChange,
|
||||
onDistributionsChange,
|
||||
onDatasetsChange,
|
||||
}) => {
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [pagination, setPagination] = useState({ page: 1, size: 10, total: 0 });
|
||||
const [distributions, setDistributions] = useState<
|
||||
Record<string, Record<string, number>>
|
||||
Record<string, Record<string, Record<string, number>>>
|
||||
>({});
|
||||
|
||||
// Helper: flatten nested distribution for preview and filter logic
|
||||
const flattenDistribution = (
|
||||
dist?: Record<string, Record<string, number>>
|
||||
): Array<{ label: string; value: string; count: number }> => {
|
||||
if (!dist) return [];
|
||||
const items: Array<{ label: string; value: string; count: number }> = [];
|
||||
Object.entries(dist).forEach(([label, values]) => {
|
||||
if (values && typeof values === "object") {
|
||||
Object.entries(values).forEach(([val, cnt]) => {
|
||||
items.push({ label, value: val, count: cnt });
|
||||
});
|
||||
}
|
||||
});
|
||||
return items;
|
||||
};
|
||||
|
||||
// Fetch dataset list
|
||||
useEffect(() => {
|
||||
const fetchDatasets = async () => {
|
||||
@@ -52,10 +69,10 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchDatasets();
|
||||
fetchDatasets().then(() => {});
|
||||
}, [pagination.page, pagination.size, searchQuery]);
|
||||
|
||||
// Fetch label distributions when in label mode
|
||||
// Fetch label distributions when datasets change
|
||||
useEffect(() => {
|
||||
const fetchDistributions = async () => {
|
||||
if (!datasets?.length) return;
|
||||
@@ -64,74 +81,25 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
||||
.filter((id) => !distributions[id]);
|
||||
if (!idsToFetch.length) return;
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
idsToFetch.map(async (id) => {
|
||||
try {
|
||||
const statRes = await queryDatasetStatisticsByIdUsingGet(id);
|
||||
return { id, stats: statRes?.data };
|
||||
} catch {
|
||||
return { id, stats: null };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const next: Record<string, Record<string, number>> = {
|
||||
...distributions,
|
||||
};
|
||||
for (const { id, stats } of results) {
|
||||
let dist: Record<string, number> | undefined = undefined;
|
||||
if (stats) {
|
||||
const candidates: any[] = [
|
||||
(stats as any).labelDistribution,
|
||||
(stats as any).tagDistribution,
|
||||
(stats as any).label_stats,
|
||||
(stats as any).labels,
|
||||
(stats as any).distribution,
|
||||
];
|
||||
let picked = candidates.find(
|
||||
(c) => c && (typeof c === "object" || Array.isArray(c))
|
||||
);
|
||||
if (Array.isArray(picked)) {
|
||||
const obj: Record<string, number> = {};
|
||||
picked.forEach((it: any) => {
|
||||
const key = it?.label ?? it?.name ?? it?.tag ?? it?.key;
|
||||
const val = it?.count ?? it?.value ?? it?.num ?? it?.total;
|
||||
if (key != null && typeof val === "number")
|
||||
obj[String(key)] = val;
|
||||
});
|
||||
dist = obj;
|
||||
} else if (picked && typeof picked === "object") {
|
||||
dist = picked as Record<string, number>;
|
||||
}
|
||||
}
|
||||
if (!dist) {
|
||||
try {
|
||||
const detRes = await queryDatasetByIdUsingGet(id);
|
||||
const det = detRes?.data;
|
||||
if (det) {
|
||||
let picked =
|
||||
(det as any).distribution ||
|
||||
(det as any).labelDistribution ||
|
||||
(det as any).tagDistribution ||
|
||||
(det as any).label_stats ||
|
||||
(det as any).labels ||
|
||||
undefined;
|
||||
if (Array.isArray(picked)) {
|
||||
const obj: Record<string, number> = {};
|
||||
picked.forEach((it: any) => {
|
||||
const key = it?.label ?? it?.name ?? it?.tag ?? it?.key;
|
||||
const val = it?.count ?? it?.value ?? it?.num ?? it?.total;
|
||||
if (key != null && typeof val === "number")
|
||||
obj[String(key)] = val;
|
||||
});
|
||||
dist = obj;
|
||||
} else if (picked && typeof picked === "object") {
|
||||
dist = picked as Record<string, number>;
|
||||
}
|
||||
const next: Record<
|
||||
string,
|
||||
Record<string, Record<string, number>>
|
||||
> = { ...distributions };
|
||||
for (const id of idsToFetch) {
|
||||
let dist: Record<string, Record<string, number>> | undefined =
|
||||
undefined;
|
||||
try {
|
||||
const detRes = await queryDatasetByIdUsingGet(id);
|
||||
const det = detRes?.data;
|
||||
if (det) {
|
||||
const picked = det?.distribution;
|
||||
if (picked && typeof picked === "object") {
|
||||
// Assume picked is now { labelName: { labelValue: count } }
|
||||
dist = picked as Record<string, Record<string, number>>;
|
||||
}
|
||||
} catch {
|
||||
dist = undefined;
|
||||
}
|
||||
} catch {
|
||||
dist = undefined;
|
||||
}
|
||||
next[String(id)] = dist || {};
|
||||
}
|
||||
@@ -141,7 +109,7 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
fetchDistributions();
|
||||
fetchDistributions().then(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasets]);
|
||||
|
||||
@@ -195,6 +163,8 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
||||
datasets.map((dataset) => {
|
||||
const idStr = String(dataset.id);
|
||||
const checked = selectedDatasets.includes(idStr);
|
||||
const distFor = distributions[idStr];
|
||||
const flat = flattenDistribution(distFor);
|
||||
return (
|
||||
<Card
|
||||
key={dataset.id}
|
||||
@@ -224,17 +194,15 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
||||
<span>{dataset.size}</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{distributions[idStr] ? (
|
||||
Object.entries(distributions[idStr]).length > 0 ? (
|
||||
{distFor ? (
|
||||
flat.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
{Object.entries(distributions[idStr])
|
||||
.slice(0, 8)
|
||||
.map(([tag, count]) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
color="gray"
|
||||
>{`${tag}: ${count}`}</Badge>
|
||||
))}
|
||||
{flat.slice(0, 8).map((it) => (
|
||||
<Badge
|
||||
key={`${it.label}_${it.value}`}
|
||||
color="gray"
|
||||
>{`${it.label}/${it.value}: ${it.count}`}</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-400">
|
||||
|
||||
@@ -45,9 +45,7 @@ export function mapRatioTask(task: Partial<RatioTaskItem>): RatioTaskItem {
|
||||
status: ratioTaskStatusMap[task.status || RatioStatus.PENDING],
|
||||
createdAt: formatDate(task.created_at),
|
||||
updatedAt: formatDate(task.updated_at),
|
||||
description:
|
||||
task.description ||
|
||||
(task.ratio_method === "DATASET" ? "按数据集配比" : "按标签配比"),
|
||||
description: task.description,
|
||||
icon: <BarChart3 />,
|
||||
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
|
||||
statistics: [
|
||||
@@ -73,16 +71,5 @@ export function mapRatioTask(task: Partial<RatioTaskItem>): RatioTaskItem {
|
||||
value: task.created_at || "-",
|
||||
},
|
||||
],
|
||||
type: task.ratio_method === "DATASET" ? "数据集配比" : "标签配比",
|
||||
// progress: 100,
|
||||
// sourceDatasets: ["sentiment_dataset", "news_classification"],
|
||||
// targetRatio: { 正面: 33, 负面: 33, 中性: 34 },
|
||||
// currentRatio: { 正面: 33, 负面: 33, 中性: 34 },
|
||||
// totalRecords: 15000,
|
||||
// processedRecords: 15000,
|
||||
// estimatedTime: "已完成",
|
||||
// quality: 95,
|
||||
// strategy: "随机下采样",
|
||||
// outputPath: "/data/balanced/sentiment_balanced_20250120",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface ModelI {
|
||||
modelName: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
type: string;
|
||||
isEnabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user