import React, { useEffect, useState, useRef } from "react";
import { Button, Popconfirm, message, Tag, Radio, Typography, Spin, Card } from "antd";
import { DeleteOutlined, WarningOutlined, CheckCircleOutlined, CloseCircleOutlined, QuestionCircleOutlined } from "@ant-design/icons";
import InferenceConfigSelector from "../../../components/InferenceConfigSelector";
import { PdpPathInfo, EvaluationCase, InferenceConfig } from "../../../types";
import { useParams } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
import { useRequest } from "ahooks";
import axios from "axios";
const { Text } = Typography;
interface TagListContainerProps {
tagListData: any
}
export const TagListContainer: React.FC<TagListContainerProps> = ({ tagListData }) => {
const { id } = useParams<{ id: string }>();
const { user, isAuthenticated } = useAuth(); // 获取当前用户信息
useEffect(() => {
if (!isAuthenticated) {
console.log(user);
message.error("请先登录后再进行标注操作");
return;
}
}, [isAuthenticated]);
const [isDirtyData, setIsDirtyData] = useState(false);
const [pdpPaths, setPdpPaths] = useState<Record<number, PdpPathInfo>>({});
const [pathInPickle, setHasPdpPaths] = useState(true); // 是否有PDP路径数据
const [pklList, setPklList] = useState<EvaluationCase[]>([]);
const [selectedConfig, setSelectedConfig] = useState<number | null>(null);
const selectedPklRef = useRef<EvaluationCase | null>(null);
const [annotationStats, setAnnotationStats] = useState({
total: 0,
annotated: 0,
good: 0,
bad: 0,
unknown: 0,
});
const [highlightPathIndex, setHighlightPathIndex] = useState<number | null>(
null,
);
// 推理配置相关状态
const [configs, setConfigs] = useState<InferenceConfig[]>([]);
const [configPagination, setConfigPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [loading, setLoading] = useState({
pklList: false,
// paths: false,
visualization: false,
annotation: false,
markDirty: false,
configs: false,
checkPkl: false,
});
// 获取PDP路径
const getPdpPaths = (pklId: number) => {
return axios.get(`/api/annotation/pdp-paths/${pklId}`, {
params: {
evaluation_set_id: pklId,
},
});
};
// 带缓存获取PDP路径
const { run, loading: loadingPaths } = useRequest(getPdpPaths, {
manual: true,
debounceWait: 500,
onSuccess: (response) => {
if (!response.data.success) {
// 没有PDP路径数据,需要通过配置获取
setHasPdpPaths(false);
setPdpPaths({});
setAnnotationStats({
total: 0,
annotated: 0,
good: 0,
bad: 0,
unknown: 0,
});
return;
}
processPdpPaths(response);
},
onError: (error) => {
console.error("Error fetching trajectories:", error);
},
});
// 加载推理配置列表
const fetchConfigs = async (
page = configPagination.current,
pageSize = configPagination.pageSize,
) => {
setLoading((prev) => ({ ...prev, configs: true }));
try {
const response = await axios.get("/api/inference_config", {
params: { page, per_page: pageSize },
});
const parsedConfigs = parseInferenceConfig(
response.data.configs || response.data,
);
setConfigs(parsedConfigs || []);
// 如果API返回了总数,更新分页信息
if (response.data.total !== undefined) {
setConfigPagination((prev) => ({
...prev,
total: response.data.total,
}));
}
} catch (error) {
console.error("Failed to fetch inference configs:", error);
message.error("获取推理配置失败,请稍后再试");
} finally {
setLoading((prev) => ({ ...prev, configs: false }));
}
};
// 初始加载
useEffect(() => {
if (id) {
fetchConfigs();
}
}, [id]);
// 处理配置分页
const handleConfigPageChange = async (page: number, pageSize: number) => {
setConfigPagination((prev) => ({ ...prev, current: page, pageSize }));
await fetchConfigs(page, pageSize);
};
useEffect(() => {
setHighlightPathIndex(null); // 清除之前的高亮
selectedPklRef.current = tagListData;
run(tagListData.id);
}, [tagListData])
// 高亮某条路径
const handleHighlightPath = (pathIndex: number) => {
setHighlightPathIndex(pathIndex === highlightPathIndex ? null : pathIndex);
};
// 处理配置选择
const handleConfigSelect = (configId: number) => {
setSelectedConfig(configId);
};
// 处理数据
const processPdpPaths = (response: any) => {
if (response.data.paths && response.data.paths.length > 0) {
const pathsDict: Record<number, PdpPathInfo> = {};
response.data.paths.forEach((path: PdpPathInfo) => {
pathsDict[path.index] = path;
});
setPdpPaths(pathsDict);
setIsDirtyData(response.data.is_dirty || false);
setHasPdpPaths(true);
} else {
setPdpPaths({});
setHasPdpPaths(false);
setAnnotationStats({
total: 0,
annotated: 0,
good: 0,
bad: 0,
unknown: 0,
});
message.error("加载PDP路径失败");
}
// 计算标注统计信息
const paths = response.data.paths;
const total = paths.length;
let annotated = 0;
let good = 0;
let bad = 0;
let unknown = 0;
paths.forEach((path: PdpPathInfo) => {
if (path.annotation) {
annotated++;
if (path.annotation.annotation === "good") {
good++;
} else if (path.annotation.annotation === "bad") {
bad++;
} else if (path.annotation.annotation === "unknown") {
unknown++;
}
}
});
setAnnotationStats({
total,
annotated,
good,
bad,
unknown,
});
};
/**
* 解析接口配置
*/
const parseInferenceConfig = (data: any[][]): InferenceConfig[] => {
return data.map((item) => ({
id: item[0],
json_name: item[1],
pth_name: item[2],
pth_upload_time: item[3],
}));
}
// 提交标注
const handleAnnotate = async (pathIndex: number, annotation: string) => {
console.log("提交");
if (!tagListData) {
message.error("请先选择PKL文件并输入标注者姓名");
return;
}
// 检查是否需要删除标注(点击了当前已选择的按钮)
const currentAnnotation = pdpPaths[pathIndex]?.annotation?.annotation;
const isDeleteAction = currentAnnotation === annotation;
setLoading((prev) => ({ ...prev, annotation: true }));
try {
const response = await axios.post("/api/annotation/pdp-paths", {
pkl_id: tagListData.id,
path_index: pathIndex,
annotation,
delete_annotation: isDeleteAction, // 新增参数,标识是删除操作
inference_config_id: !pathInPickle ? selectedConfig : undefined, // 如果是从配置获取的路径,传递配置ID
evaluation_set_id: id,
employee_id: user?.employee_id || null, // 添加工号信息
});
if (response.data.success) {
// 更新路径列表中的标注状态
const updatedPaths = { ...pdpPaths };
const oldAnnotation = updatedPaths[pathIndex].annotation?.annotation;
// 如果是删除操作,设置annotation为null,否则更新为新的标注
if (isDeleteAction) {
updatedPaths[pathIndex] = {
...updatedPaths[pathIndex],
annotation: undefined,
};
message.success("标注已删除");
} else {
updatedPaths[pathIndex] = {
...updatedPaths[pathIndex],
annotation: {
annotation,
employee_id: user?.employee_id || undefined,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
};
tagListData.has_annotation = true;
message.success("标注保存成功");
}
setPdpPaths(updatedPaths);
// 更新统计信息
setAnnotationStats((prev) => {
const newStats = { ...prev };
// 如果是删除标注
if (isDeleteAction && oldAnnotation) {
newStats.annotated--;
if (oldAnnotation === "good") newStats.good--;
else if (oldAnnotation === "bad") newStats.bad--;
else if (oldAnnotation === "unknown") newStats.unknown--;
}
// 如果是新增标注
else if (!oldAnnotation && !isDeleteAction) {
newStats.annotated++;
if (annotation === "good") newStats.good++;
else if (annotation === "bad") newStats.bad++;
else if (annotation === "unknown") newStats.unknown++;
}
// 如果是修改标注
else if (oldAnnotation !== annotation && !isDeleteAction) {
if (oldAnnotation === "good") newStats.good--;
else if (oldAnnotation === "bad") newStats.bad--;
else if (oldAnnotation === "unknown") newStats.unknown--;
if (annotation === "good") newStats.good++;
else if (annotation === "bad") newStats.bad++;
else if (annotation === "unknown") newStats.unknown++;
}
if (newStats.annotated === 0) {
tagListData.has_annotation = false;
}
return newStats;
});
} else {
message.error("操作失败");
}
} catch (error) {
console.error("Failed to save/delete annotation:", error);
message.error("操作出错");
} finally {
setLoading((prev) => ({ ...prev, annotation: false }));
}
};
// 处理标记为脏数据
const handleMarkAsDirty = async () => {
console.log("标记了一处地点");
if (!tagListData) {
message.error("请先选择PKL文件");
return;
}
setLoading((prev) => ({ ...prev, markDirty: true }));
try {
const response = await axios.post("/api/annotation/mark-dirty", {
pkl_id: tagListData.id,
is_dirty: !isDirtyData, // 反转当前状态
});
if (response.data.success) {
setIsDirtyData(response.data.is_dirty);
message.success(
response.data.is_dirty ? "已标记为脏数据" : "已取消脏数据标记",
);
// 更新列表中当前项的脏数据状态
const updatedPklList = pklList.map((pkl) => {
if (pkl.id === tagListData.id) {
return { ...pkl, dirty_data: response.data.is_dirty };
}
return pkl;
});
setPklList(updatedPklList);
} else {
message.error("操作失败");
}
} catch (error) {
console.error("Failed to mark as dirty data:", error);
message.error("操作出错");
} finally {
setLoading((prev) => ({ ...prev, markDirty: false }));
}
};
// 删除所有标注
const handleDeleteAnnotations = async () => {
const currentPkl = selectedPklRef.current;
if (!currentPkl) return;
try {
// 删除所有路径的标注
const pathIndices = Object.keys(pdpPaths).map(Number);
const deletePromises = pathIndices.map((index) =>
axios.post("/api/annotation/pdp-paths", {
pkl_id: currentPkl.id,
path_index: index,
annotation: "unknown", // 或者使用空字符串
delete_annotation: true,
inference_config_id: !pathInPickle ? selectedConfig : undefined, // 如果是从配置获取的路径,传递配置ID
evaluation_set_id: id,
employee_id: user?.employee_id || null,
}),
);
await Promise.all(deletePromises);
// 更新本地状态
const updatedPaths = { ...pdpPaths };
Object.keys(updatedPaths).forEach((key) => {
const index = Number(key);
updatedPaths[index] = {
...updatedPaths[index],
annotation: undefined,
};
});
currentPkl.has_annotation = false;
setPdpPaths(updatedPaths);
setAnnotationStats({
total: annotationStats.total,
annotated: 0,
good: 0,
bad: 0,
unknown: 0,
});
message.success("所有标注已删除");
} catch (error) {
console.error("删除标注失败:", error);
message.error("删除标注失败");
}
};
return (
<div className="h-full my-[16px] mx-[12px]">
<div className="flex flex-wrap justify-center gap-2 mb-[16px]">
<Button
danger={!isDirtyData}
type={isDirtyData ? "default" : "primary"}
icon={<WarningOutlined />}
onClick={handleMarkAsDirty}
//loading={loading.markDirty}
>
{isDirtyData ? "取消脏数据标记" : "标记为脏数据"}
</Button>
<Popconfirm
title="确定要删除所有标注结果吗?"
onConfirm={handleDeleteAnnotations}
>
<Button danger type="default" icon={<DeleteOutlined />}>
删除所有标注
</Button>
</Popconfirm>
</div>
{Object.keys(pdpPaths).length > 0 && (
<div className="mt-[8px] py-[8px] border-t border-b border-opacity-0.1">
<div className="flex gap-2 text-[14px]">
<Tag color="success" icon={<CheckCircleOutlined />}>
好: {annotationStats.good}
</Tag>
<Tag color="error" icon={<CloseCircleOutlined />}>
差: {annotationStats.bad}
</Tag>
<Tag color="processing" icon={<QuestionCircleOutlined />}>
未知: {annotationStats.unknown}
</Tag>
</div>
</div>
)}
{loadingPaths ? (
<div className="loading-paths">
<Spin tip="加载路径数据..." />
</div>
) : !pathInPickle && Object.keys(pdpPaths).length === 0 ? (
<div className="config-selector-container">
<Text>
该PKL文件中没有PDP路径数据,请选择一个推理配置查看路径
</Text>
<div className="config-selector">
<InferenceConfigSelector
configs={configs}
selectedConfig={selectedConfig}
onConfigSelect={handleConfigSelect}
loading={loading.configs}
mode="single"
pagination={{
current: configPagination.current,
pageSize: configPagination.pageSize,
total: configPagination.total,
onChange: handleConfigPageChange,
}}
evaluationSetId={id}
/>
</div>
</div>
) : Object.keys(pdpPaths).length === 0 ? (
<div className="no-paths">
<Text>未找到PDP路径数据</Text>
</div>
) : (
<div className="flex-1 overflow-auto mt-[16px]">
{Object.values(pdpPaths)
.sort((a, b) => a.index - b.index)
.map((path) => (
<div className="path-card-container" key={path.index}>
<Card
className={
highlightPathIndex === path.index
? "path-card highlighted"
: "path-card"
}
size="small"
onClick={() => handleHighlightPath(path.index)}
hoverable
>
<div className="path-info">
<div className="path-title">
{highlightPathIndex === path.index ? (
<Tag color="blue">路径 {path.index + 1}</Tag>
) : (
`路径${path.index + 1}`
)}
</div>
<div
className="path-actions"
onClick={(e) => e.stopPropagation()}
>
<Radio.Group
value={path.annotation?.annotation}
onChange={(e) => {
e.stopPropagation();
handleAnnotate(path.index, e.target.value);
}}
buttonStyle="solid"
size="small"
>
<Radio.Button
value="good"
className={
path.annotation?.annotation === "good"
? "good-selected"
: ""
}
onClick={(e) => {
e.stopPropagation();
if (path.annotation?.annotation === "good") {
handleAnnotate(path.index, "good");
}
}}
>
<CheckCircleOutlined /> 好
</Radio.Button>
<Radio.Button
value="bad"
className={
path.annotation?.annotation === "bad"
? "bad-selected"
: ""
}
onClick={(e) => {
e.stopPropagation();
if (path.annotation?.annotation === "bad") {
handleAnnotate(path.index, "bad");
}
}}
>
<CloseCircleOutlined /> 差
</Radio.Button>
<Radio.Button
value="unknown"
className={
path.annotation?.annotation === "unknown"
? "unknown-selected"
: ""
}
onClick={(e) => {
e.stopPropagation();
if (path.annotation?.annotation === "unknown") {
handleAnnotate(path.index, "unknown");
}
}}
>
<QuestionCircleOutlined /> 未知
</Radio.Button>
</Radio.Group>
</div>
</div>
</Card>
</div>
))}
</div>
)}
</div>
)
}
将组件进行拆分
最新发布