2615 lines
88 KiB
TypeScript
2615 lines
88 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
ActionIcon,
|
||
Box,
|
||
Button,
|
||
Checkbox,
|
||
Divider,
|
||
Flex,
|
||
Group,
|
||
HoverCard,
|
||
NumberInput,
|
||
Popover,
|
||
Radio,
|
||
ScrollArea,
|
||
Select,
|
||
Slider,
|
||
Stack,
|
||
Switch,
|
||
Text,
|
||
TextInput,
|
||
Tooltip,
|
||
UnstyledButton,
|
||
} from "@mantine/core"
|
||
import { useForm } from "@mantine/form"
|
||
import { notifications } from "@mantine/notifications"
|
||
import { IconCaretDownFilled, IconChevronDown } from "@tabler/icons-react"
|
||
import dayjs from "dayjs"
|
||
import duration from "dayjs/plugin/duration"
|
||
import {
|
||
BadgeInfoIcon,
|
||
Baseline,
|
||
ChevronDownIcon,
|
||
ChevronLeftIcon,
|
||
CirclePlus,
|
||
ClipboardList,
|
||
Cloud,
|
||
CloudCog,
|
||
CloudDownload,
|
||
Component,
|
||
Copy,
|
||
CopyMinus,
|
||
CopyPlus,
|
||
CopySlash,
|
||
Eclipse,
|
||
Film,
|
||
FolderDownIcon,
|
||
Group as GroupIcon,
|
||
Locate,
|
||
LocateFixed,
|
||
LocateOff,
|
||
MagnetIcon,
|
||
MonitorSmartphone,
|
||
MousePointer,
|
||
Paintbrush,
|
||
Pen,
|
||
SquarePen,
|
||
Tag,
|
||
Timer,
|
||
} from "lucide-react"
|
||
import { useRouter } from "next/navigation"
|
||
import {
|
||
forwardRef,
|
||
useCallback,
|
||
useEffect,
|
||
useImperativeHandle,
|
||
useMemo,
|
||
useState,
|
||
} from "react"
|
||
import { saveLabelResult } from "../api/label"
|
||
import {
|
||
ImageObjects,
|
||
LabelResult,
|
||
RequestLabelResult,
|
||
WorkLoad,
|
||
} from "../api/label/typing"
|
||
import { Project } from "../api/project/typing"
|
||
import { getNextTaskId, taskCommit } from "../api/task"
|
||
import { Task } from "../api/task/typing"
|
||
import {
|
||
storage,
|
||
useKeyEventStore,
|
||
useLabelStore,
|
||
useObjectStore,
|
||
useVideoFrameStore,
|
||
} from "../store"
|
||
import { useBackUrlStore, usePermissionStore } from "../store/auth"
|
||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||
import { useDescToolsStore } from "../useDescToolsStore"
|
||
import { useKeyboardStore } from "../useKeyBoardStore"
|
||
import { usePaperStore } from "../usePaperStore"
|
||
import { useRightToolsStore } from "../useRightToolsStore"
|
||
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
import { getSubAttrStatus } from "../util"
|
||
import { safeClone } from "../utils/clone"
|
||
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
||
import { adjustAllPoints } from "../utils/paperjs"
|
||
import BackConfirmModal from "./BackConfirmModal"
|
||
import BackupModal from "./BackupModal"
|
||
import ConfirmModal from "./ConfirmModal"
|
||
import { splitWord } from "./EditorContainer"
|
||
import RecoverModal from "./RecoverModal"
|
||
import { renderOperationIcon } from "./RightObjectTools"
|
||
import TaskCacheModal from "./TaskCacheModal"
|
||
import TaskStatusTag from "./TaskStatusTag"
|
||
dayjs.extend(duration)
|
||
|
||
interface TopToolsComponentProps {
|
||
taskDetail?: Task.DataProps
|
||
projectDetail?: Project.DetailResponse
|
||
oldWorkLoad: WorkLoad | null
|
||
updateOldWorkLoad: (data: WorkLoad) => void
|
||
isLabelTask: boolean
|
||
renderPolygons: (
|
||
operationId: string,
|
||
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
||
) => void | null
|
||
}
|
||
|
||
const initialWorkLoad: WorkLoad = {
|
||
object_size: {},
|
||
prelabel_modify_size: {},
|
||
review_size: 0,
|
||
comment_size: 0,
|
||
work_time: 0,
|
||
dynamic_size: {},
|
||
static_size: {},
|
||
review_dynamic_size: {},
|
||
review_static_size: {},
|
||
review_question_size: {},
|
||
key_frame_size: {},
|
||
review_key_frame_size: {},
|
||
question_size: {},
|
||
}
|
||
|
||
const TaskTimerDisplay = ({
|
||
currentTimeKey,
|
||
}: {
|
||
currentTimeKey: "label" | "review1" | "review2"
|
||
}) => {
|
||
const currentTime = useLabelTimeStore(
|
||
(state) => state.labelTime[currentTimeKey]
|
||
)
|
||
return <>{dayjs.duration(currentTime, "seconds").format("HH:mm:ss")}</>
|
||
}
|
||
|
||
const TopTools = (
|
||
props: TopToolsComponentProps,
|
||
ref: React.Ref<unknown> | undefined
|
||
) => {
|
||
const {
|
||
taskDetail,
|
||
projectDetail,
|
||
oldWorkLoad,
|
||
updateOldWorkLoad,
|
||
isLabelTask,
|
||
renderPolygons,
|
||
} = props
|
||
const { backUrl, basePath } = useBackUrlStore()
|
||
const { mode, loadingData } = usePaperStore()
|
||
const router = useRouter()
|
||
const user_id = usePermissionStore.getState().user_id
|
||
|
||
// const { getAll } = usePaperStore();
|
||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||
const [backupOpen, setBackupOpen] = useState(false)
|
||
const [duplicateConfirmOpen, setDuplicateConfirmOpen] = useState(false)
|
||
const [duplicateName, setDuplicateName] = useState("")
|
||
const [recoverOpen, setRecoverOpen] = useState(false)
|
||
const [taskOpen, setTaskOpen] = useState(false)
|
||
const [operationPickerOpened, setOperationPickerOpened] = useState(false)
|
||
const [operationKeyword, setOperationKeyword] = useState("")
|
||
const labelData = useLabelStore((state) => state.label)
|
||
const setLabel = useLabelStore((state) => state.setLabel)
|
||
const pushStateStack = useLabelStore((state) => state.pushStateStack)
|
||
const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
|
||
const { activeImage } = useBottomToolsStore()
|
||
const {
|
||
isView,
|
||
setIsView,
|
||
saveCurrentScale,
|
||
setSaveCurrentScale,
|
||
showMultiFrame,
|
||
setShowMultiFrame,
|
||
showObjectList,
|
||
setShowObjectList,
|
||
showDescList,
|
||
setShowDescList,
|
||
showGroupList,
|
||
setShowGroupList,
|
||
showTaskList,
|
||
setShowTaskList,
|
||
editMode,
|
||
setEditMode,
|
||
pressA,
|
||
setPressA,
|
||
activeOperation,
|
||
setActiveOperation,
|
||
drawOption,
|
||
setDrawOption,
|
||
showTags,
|
||
setShowTags,
|
||
showTagsConfigs,
|
||
setShowTagsConfigs,
|
||
currentTimeKey,
|
||
setCurrentTimeKey,
|
||
subAttrPresetShow,
|
||
setSubAttrPresetShow,
|
||
subAttributes,
|
||
setsubAttributes,
|
||
objectOperations,
|
||
imageFilter,
|
||
setImageFilter,
|
||
crosshairStatus,
|
||
setCrosshairStatus,
|
||
assistToolEnabled,
|
||
setAssistToolEnabled,
|
||
assistToolSize,
|
||
setAssistToolSize,
|
||
needBackup,
|
||
setNeedBackup,
|
||
checkSize,
|
||
setCheckSize,
|
||
magnetFlag,
|
||
setMagnetFlag,
|
||
} = useTopToolsStore()
|
||
|
||
const { pathGroupMap } = useRightToolsStore()
|
||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||
const setIsAutoSave = useIntervalStore((state) => state.setIsAutoSave)
|
||
const autoSaveGap = useIntervalStore((state) => state.autoSaveGap)
|
||
const setAutoSaveGap = useIntervalStore((state) => state.setAutoSaveGap)
|
||
|
||
// 当前是否可以操作功能按键
|
||
const currentEditAuth = useMemo(() => {
|
||
if (!taskDetail) return false
|
||
if (!taskDetail.current_uid) return false
|
||
if (user_id !== taskDetail.current_uid) return false
|
||
const NotAllowEdit = [1, 3, 5, 7, 8]
|
||
return !NotAllowEdit.includes(taskDetail.label_status)
|
||
}, [taskDetail, user_id])
|
||
|
||
const getOperationSchema = useCallback(
|
||
(operationId: string) => {
|
||
return (
|
||
projectDetail?.label_schema_list?.find(
|
||
(item) => item.category_id === +operationId
|
||
) || null
|
||
)
|
||
},
|
||
[projectDetail?.label_schema_list]
|
||
)
|
||
|
||
const labelCategories = useMemo(() => {
|
||
return (
|
||
objectOperations.map((item) => ({
|
||
id: item.category_id,
|
||
name: item.label_class,
|
||
supercategory: item.super_category,
|
||
})) || []
|
||
)
|
||
}, [objectOperations])
|
||
|
||
const filteredObjectOperations = useMemo(() => {
|
||
const keyword = operationKeyword.trim().toLowerCase()
|
||
if (!keyword) return objectOperations
|
||
return objectOperations.filter((item) => {
|
||
return (
|
||
item.label_class.toLowerCase().includes(keyword) ||
|
||
item.category_id.toString().includes(keyword) ||
|
||
item.super_category.toLowerCase().includes(keyword)
|
||
)
|
||
})
|
||
}, [objectOperations, operationKeyword])
|
||
|
||
useEffect(() => {
|
||
if (
|
||
taskDetail?.label_status &&
|
||
TaskStatusEnum.get(taskDetail?.label_status)
|
||
) {
|
||
switch (
|
||
taskDetail?.label_status &&
|
||
TaskStatusEnum.get(taskDetail?.label_status)
|
||
) {
|
||
case "标注中":
|
||
setCurrentTimeKey("label")
|
||
return
|
||
case "审核中":
|
||
setCurrentTimeKey("review1")
|
||
return
|
||
case "复审中":
|
||
setCurrentTimeKey("review2")
|
||
return
|
||
default:
|
||
setCurrentTimeKey("label")
|
||
return
|
||
}
|
||
}
|
||
}, [setCurrentTimeKey, taskDetail?.label_status])
|
||
|
||
const calculateMinMaxDifference = (arr: [number, number][]) => {
|
||
let min1 = arr[0][0]
|
||
let min2 = arr[0][1]
|
||
let max1 = arr[0][0]
|
||
let max2 = arr[0][1]
|
||
|
||
for (let i = 1; i < arr.length; i++) {
|
||
min1 = Math.min(min1, arr[i][0])
|
||
min2 = Math.min(min2, arr[i][1])
|
||
max1 = Math.max(max1, arr[i][0])
|
||
max2 = Math.max(max2, arr[i][1])
|
||
}
|
||
|
||
return [min1, min2, max1 - min1, max2 - min2]
|
||
}
|
||
|
||
const transferPath = useCallback(
|
||
(
|
||
img_id: string,
|
||
category_id: string,
|
||
path: any[],
|
||
compoundPath: any[],
|
||
detail: any
|
||
) => {
|
||
let operationSchema = getOperationSchema(category_id)
|
||
// 保存后类型为 [[string, number, number]] 保存前为 [SegmentPoint]
|
||
let flag = !Array.isArray(path?.[0]?.[0])
|
||
let compoundFlag = !Array.isArray(compoundPath?.[0]?.[0])
|
||
|
||
console.log("transferPath", path, flag, compoundFlag)
|
||
|
||
// 根据当前图片缩放比例还原各坐标点位置
|
||
const scale = usePaperStore.getState().rasterScale[img_id] ?? 1
|
||
|
||
switch (operationSchema && labelTypeMap.get(operationSchema.label_type)) {
|
||
case "多边形":
|
||
return {
|
||
segmentation: flag
|
||
? path.map((child) => {
|
||
return child.map((item: { x: number; y: number }) => [
|
||
item.x / scale,
|
||
item.y / scale,
|
||
])
|
||
})
|
||
: path.map((child) => {
|
||
return child.map((item: [number, number]) =>
|
||
item && item.length
|
||
? [item[0] / scale, item[1] / scale]
|
||
: []
|
||
)
|
||
}),
|
||
hollow_segmentation: compoundFlag
|
||
? compoundPath.map((child) => {
|
||
return child.map((item: { x: number; y: number }) => [
|
||
item.x / scale,
|
||
item.y / scale,
|
||
])
|
||
})
|
||
: compoundPath.map((child) => {
|
||
return child.map((item: [number, number]) =>
|
||
item && item.length
|
||
? [item[0] / scale, item[1] / scale]
|
||
: []
|
||
)
|
||
}),
|
||
}
|
||
case "多线段":
|
||
return {
|
||
line_segment: flag
|
||
? path.map((child) => {
|
||
return child.map((item: paper.Segment) => [
|
||
item.point.x / scale,
|
||
item.point.y / scale,
|
||
])
|
||
})
|
||
: path.map((child) => {
|
||
return child.map((item: [number, number]) => [
|
||
item[0] / scale,
|
||
item[1] / scale,
|
||
])
|
||
}),
|
||
}
|
||
case "2D框":
|
||
// 2D框 不存在多区域情况
|
||
return {
|
||
rect: flag
|
||
? path.map((child) => {
|
||
return calculateMinMaxDifference(
|
||
child.map((item: { x: number; y: number }) => [
|
||
item.x / scale,
|
||
item.y / scale,
|
||
])
|
||
)
|
||
})?.[0] || []
|
||
: path.map((child) => {
|
||
return calculateMinMaxDifference(
|
||
child.map((item: [number, number]) => [
|
||
item[0] / scale,
|
||
item[1] / scale,
|
||
])
|
||
)
|
||
})?.[0] || [],
|
||
}
|
||
case "关键点":
|
||
return {
|
||
key_points: flag
|
||
? path.map((child) => {
|
||
return child.map(
|
||
(
|
||
item: { point: { x: number; y: number } },
|
||
index: number
|
||
) => {
|
||
return {
|
||
point: [item.point.x / scale, item.point.y / scale],
|
||
attributes: detail.circles?.[index]?.attributes,
|
||
}
|
||
}
|
||
)
|
||
})?.[0] || []
|
||
: path.map((child) => {
|
||
return child.map((item: [number, number], index: number) => {
|
||
return {
|
||
point: [item[0] / scale, item[1] / scale],
|
||
attributes: detail.circles?.[index]?.attributes,
|
||
}
|
||
})
|
||
})?.[0] || [],
|
||
}
|
||
default:
|
||
return {
|
||
segmentation: [],
|
||
}
|
||
}
|
||
},
|
||
[getOperationSchema]
|
||
)
|
||
|
||
const handleSave = useCallback(async () => {
|
||
if (loadingData) {
|
||
return
|
||
}
|
||
|
||
if (!oldWorkLoad) {
|
||
notifications.hide("save")
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "统计数据生成失败,请联系管理员",
|
||
})
|
||
return
|
||
}
|
||
|
||
const group = usePaperStore.getState().group
|
||
|
||
if (!group) {
|
||
notifications.hide("save")
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 标注相关数据统计
|
||
const labelObjectSize = {
|
||
object_size: 0,
|
||
valid_size: 0,
|
||
first_comment_size: 0,
|
||
second_comment_size: 0,
|
||
new_size: 0,
|
||
prelabel_size: 0,
|
||
prelabel_modify_size: 0,
|
||
dynamic_size: 0,
|
||
static_size: 0,
|
||
review_size: 0,
|
||
review_dynamic_size: 0,
|
||
review_static_size: 0,
|
||
key_frame_size: 0,
|
||
question_size: 0,
|
||
total_qa_group_size: 0,
|
||
single_qa_group_size: 0,
|
||
multiple_qa_group_size: 0,
|
||
new_qa_group_size: 0,
|
||
new_single_qa_group_size: 0,
|
||
new_multiple_qa_group_size: 0,
|
||
review_question_size: 0,
|
||
comment_question_size: 0,
|
||
}
|
||
|
||
const { label_status, data_name } = taskDetail!
|
||
const frameResults = data_name.map((item, imageId) => {
|
||
const name = item.name?.[0]
|
||
let finalData: LabelResult = {
|
||
data_name: name,
|
||
}
|
||
const imageLabelData = useLabelStore.getState().label.get(name)
|
||
const pathGroupData = pathGroupMap.get(name)
|
||
|
||
let image_objects: ImageObjects = {
|
||
annotations: [],
|
||
categories: labelCategories,
|
||
images: {
|
||
id: imageId + 1,
|
||
file_name: name,
|
||
width: usePaperStore.getState().rasterSize[name]?.[0] || 0,
|
||
height: usePaperStore.getState().rasterSize[name]?.[1] || 0,
|
||
},
|
||
image_groups:
|
||
(pathGroupData && Object.fromEntries(pathGroupData)) || {},
|
||
}
|
||
if (imageLabelData)
|
||
[...imageLabelData.entries()].forEach(([key, labelObjs]) => {
|
||
let annotations = labelObjs.map(
|
||
([id, points, detail, compoundPoints]) => {
|
||
// 处理统计数据
|
||
labelObjectSize.object_size++
|
||
let has_first_review = false
|
||
let has_second_review = false
|
||
let reviewed = false
|
||
const { comment, prelabel, uid } = detail
|
||
comment.forEach((c: any) => {
|
||
if (c.comment_type > 0) reviewed = true
|
||
if (
|
||
c.turn === 1 &&
|
||
(c.check_box_comments.length > 0 ||
|
||
(c.text_comments[0] && c.text_comments[0] !== ""))
|
||
)
|
||
has_first_review = true
|
||
if (
|
||
c.turn === 2 &&
|
||
(c.check_box_comments.length > 0 ||
|
||
(c.text_comments[0] && c.text_comments[0] !== ""))
|
||
)
|
||
has_second_review = true
|
||
})
|
||
if (has_first_review) labelObjectSize.first_comment_size++
|
||
if (has_second_review) labelObjectSize.second_comment_size++
|
||
if (!prelabel || uid > 0) labelObjectSize.valid_size++
|
||
if (!prelabel) labelObjectSize.new_size++
|
||
if (prelabel) labelObjectSize.prelabel_size++
|
||
if (prelabel && uid > 0) labelObjectSize.prelabel_modify_size++
|
||
if (reviewed) labelObjectSize.review_size++
|
||
|
||
// 返回标注对象的annotations
|
||
return {
|
||
...detail,
|
||
...transferPath(name, key, points, compoundPoints, detail),
|
||
id,
|
||
category_id: Number(key),
|
||
image_id: imageId + 1,
|
||
}
|
||
}
|
||
)
|
||
image_objects.annotations = [
|
||
...image_objects.annotations,
|
||
...annotations,
|
||
]
|
||
})
|
||
// 处理关键帧相关信息
|
||
const allKeyFrameData = useBottomToolsStore.getState().keyFrameData
|
||
const currentKeyFrameData = allKeyFrameData.get(name) || {
|
||
key_frame: false,
|
||
label1_ts: 0,
|
||
label1_uid: 0,
|
||
review1_ts: 0,
|
||
review1_uid: 0,
|
||
review2_ts: 0,
|
||
review2_uid: 0,
|
||
}
|
||
if (currentKeyFrameData.key_frame) labelObjectSize.key_frame_size++
|
||
image_objects = { ...image_objects, ...currentKeyFrameData }
|
||
// 基础LLM 文本描述和元操作序列
|
||
if (projectDetail?.label_type === 5) {
|
||
const metaData = useDescToolsStore.getState().metaData.get(name) || []
|
||
const descData =
|
||
useDescToolsStore.getState().descData.get(name) || new Map()
|
||
let other_desc: any = {}
|
||
descData
|
||
.entries()
|
||
.forEach(([label_class, { value, is_correct, comment }]) => {
|
||
let obj = {
|
||
value:
|
||
typeof value === "string" ? value.split(splitWord)[0] : value,
|
||
is_correct:
|
||
typeof is_correct === "boolean" ? is_correct : undefined,
|
||
comment:
|
||
comment &&
|
||
comment.split(splitWord)[1] &&
|
||
comment.split(splitWord)[1].trim()
|
||
? comment.split(splitWord)[0]
|
||
: undefined,
|
||
}
|
||
other_desc[label_class] = obj
|
||
})
|
||
image_objects.desc = {
|
||
other_desc: JSON.stringify(other_desc),
|
||
meta_operation: metaData,
|
||
}
|
||
}
|
||
// 处理QA数据
|
||
if (projectDetail?.label_type === 6) {
|
||
const currentQaData = safeClone(
|
||
useDescToolsStore.getState().qaData.get(name) || {}
|
||
)
|
||
let other_desc: any = {}
|
||
// 更新统计数据
|
||
Object.entries(currentQaData).forEach(
|
||
([label_class, questionArr]) => {
|
||
if (currentKeyFrameData.key_frame)
|
||
labelObjectSize.question_size += questionArr.length
|
||
let singleCount = 0
|
||
let multiCount = 0
|
||
let newSingleCount = 0
|
||
let newMultiCount = 0
|
||
const roundMap = new Map<
|
||
number,
|
||
{ hasNewQuestion: boolean; count: number }
|
||
>()
|
||
let arr: any[] = []
|
||
questionArr.forEach((q) => {
|
||
const [name, v] = Object.entries(q)[0]
|
||
if (!roundMap.has(v.id)) {
|
||
roundMap.set(v.id, { hasNewQuestion: !v.is_pre, count: 1 })
|
||
} else {
|
||
const exist = roundMap.get(v.id)
|
||
if (!exist?.hasNewQuestion && !v.is_pre)
|
||
exist!.hasNewQuestion = true
|
||
exist!.count++
|
||
}
|
||
arr.push({
|
||
[name]: {
|
||
...v,
|
||
value: v.value ? v.value.split(splitWord)[0] : "",
|
||
comment:
|
||
v.comment &&
|
||
v.comment.split(splitWord)[1] &&
|
||
v.comment.split(splitWord)[1].trim()
|
||
? v.comment.split(splitWord)[0]
|
||
: undefined,
|
||
},
|
||
})
|
||
})
|
||
other_desc[label_class] = arr
|
||
arr.forEach((obj: any) => {
|
||
const v: any = Object.values(obj)[0]
|
||
if (v.tag && currentKeyFrameData.key_frame) {
|
||
labelObjectSize.review_question_size++
|
||
}
|
||
})
|
||
roundMap.values().forEach((round) => {
|
||
if (round.count > 1) {
|
||
multiCount++
|
||
if (round.hasNewQuestion) newMultiCount++
|
||
} else {
|
||
singleCount++
|
||
if (round.hasNewQuestion) newSingleCount++
|
||
}
|
||
})
|
||
if (currentKeyFrameData.key_frame) {
|
||
labelObjectSize.total_qa_group_size += singleCount + multiCount
|
||
labelObjectSize.single_qa_group_size += singleCount
|
||
labelObjectSize.multiple_qa_group_size += multiCount
|
||
labelObjectSize.new_qa_group_size +=
|
||
newSingleCount + newMultiCount
|
||
labelObjectSize.new_single_qa_group_size += newSingleCount
|
||
labelObjectSize.new_multiple_qa_group_size += newMultiCount
|
||
}
|
||
}
|
||
)
|
||
image_objects.desc = {
|
||
other_desc: JSON.stringify(other_desc),
|
||
meta_operation: [],
|
||
}
|
||
}
|
||
finalData.image_objects = image_objects
|
||
return finalData
|
||
})
|
||
|
||
let saveResults: LabelResult[] = frameResults
|
||
if (projectDetail?.label_type === 7) {
|
||
const frameNamesFromTask = data_name
|
||
.map((item) => item.name?.[0] || "")
|
||
.filter((name) => !!name)
|
||
const frameNameSet = new Set(frameNamesFromTask)
|
||
const frameStore = useVideoFrameStore.getState().videoFrames
|
||
const taskVideoPrefix = `${taskDetail!.project_id}:${taskDetail!.id}:`
|
||
const videoFrameEntries = Array.from(frameStore.entries()).filter(
|
||
([key]) => key.startsWith(taskVideoPrefix)
|
||
)
|
||
if (!videoFrameEntries.length) {
|
||
throw new Error("视频帧映射缺失,无法保存,请刷新任务后重试")
|
||
}
|
||
|
||
const videoFrameMap = new Map<string, string[]>()
|
||
videoFrameEntries.forEach(([key, frameNames]) => {
|
||
const videoName = key.slice(taskVideoPrefix.length)
|
||
if (!videoName) return
|
||
videoFrameMap.set(
|
||
videoName,
|
||
(frameNames || []).filter((frameName) =>
|
||
frameNameSet.has(frameName)
|
||
)
|
||
)
|
||
})
|
||
if (!videoFrameMap.size) {
|
||
throw new Error("视频帧映射无效,无法保存,请刷新任务后重试")
|
||
}
|
||
|
||
const videoNames = Array.from(videoFrameMap.keys())
|
||
const frameToVideoMap = new Map<string, string>()
|
||
videoFrameMap.forEach((frameNames, videoName) => {
|
||
frameNames.forEach((frameName) => {
|
||
frameToVideoMap.set(frameName, videoName)
|
||
})
|
||
})
|
||
|
||
const unresolvedFrames = frameNamesFromTask.filter(
|
||
(frameName) => !frameToVideoMap.has(frameName)
|
||
)
|
||
if (unresolvedFrames.length) {
|
||
throw new Error(
|
||
`视频帧映射缺失,无法保存,请刷新任务后重试: ${unresolvedFrames.slice(0, 5).join(", ")}`
|
||
)
|
||
}
|
||
|
||
const frameResultMap = new Map<string, ImageObjects>()
|
||
frameResults.forEach((item) => {
|
||
if (item.image_objects) {
|
||
frameResultMap.set(item.data_name, item.image_objects)
|
||
}
|
||
})
|
||
|
||
const groupedVideoObjects = new Map<
|
||
string,
|
||
{ [key: string]: ImageObjects }
|
||
>()
|
||
frameResultMap.forEach((imageObjects, frameName) => {
|
||
const videoName = frameToVideoMap.get(frameName)
|
||
if (!videoName) return
|
||
if (!groupedVideoObjects.has(videoName)) {
|
||
groupedVideoObjects.set(videoName, {})
|
||
}
|
||
groupedVideoObjects.get(videoName)![frameName] = imageObjects
|
||
})
|
||
|
||
saveResults = videoNames
|
||
.map((videoName) => {
|
||
const videoObjects = groupedVideoObjects.get(videoName)
|
||
if (!videoObjects || !Object.keys(videoObjects).length) return null
|
||
return {
|
||
data_name: videoName,
|
||
video_objects: videoObjects,
|
||
} as LabelResult
|
||
})
|
||
.filter((item): item is LabelResult => !!item)
|
||
|
||
if (!saveResults.length) {
|
||
throw new Error("视频任务保存结果为空")
|
||
}
|
||
}
|
||
|
||
const currentLabelTime = useLabelTimeStore.getState().labelTime
|
||
const workTime = {
|
||
label_work_time: currentLabelTime.label,
|
||
first_review_work_time: currentLabelTime.review1,
|
||
second_review_work_time: currentLabelTime.review2,
|
||
}
|
||
// 生成新workload
|
||
let time = 0
|
||
if (label_status === 2) {
|
||
time = workTime.label_work_time
|
||
} else if (label_status === 4) {
|
||
time = workTime.first_review_work_time
|
||
} else if (label_status === 6) {
|
||
time = workTime.second_review_work_time
|
||
}
|
||
const workLoad: WorkLoad = JSON.parse(JSON.stringify(initialWorkLoad))
|
||
workLoad.work_time = time ? time - oldWorkLoad.work_time : 0
|
||
frameResults.forEach((item) => {
|
||
const { image_objects } = item
|
||
if (image_objects) {
|
||
const { annotations, key_frame, desc } = image_objects
|
||
annotations.forEach((annotation) => {
|
||
let reviewed = false
|
||
const { comment, prelabel, uid } = annotation
|
||
if (prelabel)
|
||
workLoad.prelabel_modify_size[uid]
|
||
? workLoad.prelabel_modify_size[uid]++
|
||
: (workLoad.prelabel_modify_size[uid] = 1)
|
||
else
|
||
workLoad.object_size[uid]
|
||
? workLoad.object_size[uid]++
|
||
: (workLoad.object_size[uid] = 1)
|
||
if (comment) {
|
||
comment.forEach((c) => {
|
||
if (c.comment_type > 0 && c.uid === user_id) reviewed = true
|
||
if (c.comment_type === 2 && c.uid === user_id)
|
||
workLoad.comment_size++
|
||
})
|
||
}
|
||
if (reviewed) workLoad.review_size++
|
||
})
|
||
if (key_frame) {
|
||
let id = usePermissionStore.getState().user_id
|
||
if (label_status === 2) {
|
||
workLoad.key_frame_size[id]
|
||
? workLoad.key_frame_size[id]++
|
||
: (workLoad.key_frame_size[id] = 1)
|
||
} else if (label_status === 4) {
|
||
workLoad.review_key_frame_size[id]
|
||
? workLoad.review_key_frame_size[id]++
|
||
: (workLoad.review_key_frame_size[id] = 1)
|
||
} else if (label_status === 6) {
|
||
workLoad.review_key_frame_size[id]
|
||
? workLoad.review_key_frame_size[id]++
|
||
: (workLoad.review_key_frame_size[id] = 1)
|
||
}
|
||
}
|
||
if (projectDetail?.label_type === 6) {
|
||
if (desc) {
|
||
const { other_desc } = desc
|
||
const descData = other_desc ? JSON.parse(other_desc) : {}
|
||
Object.values(descData).forEach((questionArr: any) => {
|
||
questionArr.forEach((q: any) => {
|
||
const v: any = Object.values(q)[0]
|
||
if (v.uid) {
|
||
workLoad.question_size[v.uid]
|
||
? workLoad.question_size[v.uid]++
|
||
: (workLoad.question_size[v.uid] = 1)
|
||
}
|
||
if (v.tag) {
|
||
const uid = usePermissionStore.getState().user_id
|
||
workLoad.review_question_size[uid]
|
||
? workLoad.review_question_size[uid]++
|
||
: (workLoad.review_question_size[uid] = 1)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
})
|
||
const newWorkLoad: WorkLoad = JSON.parse(JSON.stringify(initialWorkLoad))
|
||
Object.entries(workLoad).forEach(([key, value]) => {
|
||
if (key === "work_time") {
|
||
newWorkLoad.work_time = value
|
||
return
|
||
}
|
||
if (typeof value === "number") {
|
||
;(newWorkLoad as any)[key] = value - (oldWorkLoad as any)[key]
|
||
} else {
|
||
Object.keys(value).forEach((k) => {
|
||
if ((oldWorkLoad as any)[key][k]) {
|
||
;(newWorkLoad as any)[key][k] =
|
||
value[k] - (oldWorkLoad as any)[key][k]
|
||
} else {
|
||
;(newWorkLoad as any)[key][k] = value[k]
|
||
}
|
||
})
|
||
}
|
||
})
|
||
const defaultObj = {
|
||
label_object_size: labelObjectSize,
|
||
work_time: workTime,
|
||
work_load: newWorkLoad,
|
||
}
|
||
let params: RequestLabelResult = {
|
||
uid: user_id,
|
||
task_id: taskDetail?.id || 0,
|
||
results: saveResults,
|
||
...defaultObj,
|
||
}
|
||
console.log(params, workLoad)
|
||
await saveLabelResult(params)
|
||
notifications.show({
|
||
id: "save",
|
||
color: "green",
|
||
message: "保存成功",
|
||
})
|
||
updateOldWorkLoad(workLoad)
|
||
} catch (error: any) {
|
||
console.log(error)
|
||
notifications.hide("save")
|
||
notifications.show({
|
||
color: "red",
|
||
message: error?.message || "保存失败",
|
||
})
|
||
}
|
||
}, [
|
||
labelCategories,
|
||
oldWorkLoad,
|
||
pathGroupMap,
|
||
projectDetail,
|
||
taskDetail,
|
||
transferPath,
|
||
updateOldWorkLoad,
|
||
user_id,
|
||
loadingData,
|
||
])
|
||
|
||
const handleBackup = useCallback(
|
||
async (name: string) => {
|
||
let newTaskLabelData = new Map()
|
||
if (!taskDetail) {
|
||
return
|
||
}
|
||
let textData = new Map()
|
||
let metaData = new Map()
|
||
taskDetail.data_name.forEach((item) => {
|
||
const name = item.name?.[0] ?? ""
|
||
if (labelData.get(name)) newTaskLabelData.set(name, labelData.get(name))
|
||
if (projectDetail?.label_type === 5) {
|
||
const desc = useDescToolsStore.getState().descData.get(name)
|
||
if (desc) {
|
||
textData.set(name, desc)
|
||
}
|
||
const meta = useDescToolsStore.getState().metaData.get(name)
|
||
if (meta) metaData.set(name, meta)
|
||
}
|
||
if (projectDetail?.label_type === 6) {
|
||
const qaDesc = useDescToolsStore.getState().qaData.get(name)
|
||
if (qaDesc) textData.set(name, qaDesc)
|
||
}
|
||
})
|
||
// 调整备份数据结果的点位比例
|
||
const adjustedData = adjustAllPoints(
|
||
newTaskLabelData,
|
||
usePaperStore.getState().reciprocalRasterScale
|
||
)
|
||
let taskLabelData = await storage.getItem(taskDetail.id.toString())
|
||
|
||
if (taskLabelData) {
|
||
try {
|
||
const keys = Object.keys(taskLabelData.state)
|
||
keys.forEach((item) => {
|
||
if (item === name && name !== "auto_backup") throw new Error("")
|
||
})
|
||
} catch (error) {
|
||
console.log(error)
|
||
setDuplicateName(name)
|
||
setDuplicateConfirmOpen(true)
|
||
return
|
||
}
|
||
let newState = {
|
||
...taskLabelData?.state,
|
||
[name]: adjustedData,
|
||
[`${name}-text`]: { text: textData, meta: metaData },
|
||
}
|
||
storage.setItem(taskDetail.id.toString(), {
|
||
state: newState,
|
||
})
|
||
} else {
|
||
storage.setItem(taskDetail.id.toString(), {
|
||
state: {
|
||
[name]: adjustedData,
|
||
[`${name}-text`]: { text: textData, meta: metaData },
|
||
},
|
||
})
|
||
}
|
||
if (name !== "auto_backup")
|
||
notifications.show({
|
||
/** Position of the notification, if not set, the position is determined based on `position` prop on Notifications component */
|
||
position: "top-center",
|
||
/** Notification message, required for all notifications */
|
||
message: "备份成功",
|
||
})
|
||
setBackupOpen(false)
|
||
},
|
||
[labelData, projectDetail?.label_type, taskDetail]
|
||
)
|
||
|
||
const handleRecover = useCallback(
|
||
async (name: string) => {
|
||
try {
|
||
if (!renderPolygons) return
|
||
let taskLabelData = await storage.getItem(taskDetail!.id.toString())
|
||
if (taskLabelData) {
|
||
const backupData = taskLabelData.state?.[name]
|
||
if (!name || !backupData) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "备份不存在或尚未选择备份",
|
||
})
|
||
return
|
||
}
|
||
const finalData = adjustAllPoints(
|
||
backupData,
|
||
usePaperStore.getState().rasterScale
|
||
)
|
||
for (const entry of finalData.entries()) {
|
||
// 赋值用过的id
|
||
for (const category of entry[1].values()) {
|
||
category.forEach((item) => {
|
||
useRightToolsStore.getState().pushPathId(item[0])
|
||
})
|
||
}
|
||
// 绘制数据时先清空当前页所有图形后再进行绘制
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({})
|
||
.filter((item: any) => item.data.id)
|
||
.forEach((item: any) => {
|
||
item.remove()
|
||
})
|
||
// 只渲染当前页数据
|
||
if (entry[0] === activeImage) {
|
||
const map = entry[1]
|
||
for (const key of map.keys()) {
|
||
renderPolygons(key, map)
|
||
}
|
||
}
|
||
}
|
||
setLabel(finalData)
|
||
pushStateStack(finalData)
|
||
// 大语言
|
||
if ([5, 6].includes(projectDetail?.label_type || 0)) {
|
||
const data = taskLabelData.state[`${name}-text`]
|
||
if (projectDetail?.label_type === 5) {
|
||
const { text, meta } = data
|
||
useDescToolsStore.getState().setDescData(text)
|
||
useDescToolsStore.getState().setMetaData(meta)
|
||
} else if (projectDetail?.label_type === 6) {
|
||
const { text } = data
|
||
const updateQaData = safeClone(
|
||
useDescToolsStore.getState().qaData
|
||
)
|
||
text.entries().forEach(([key, value]: any) => {
|
||
updateQaData.set(key, value)
|
||
})
|
||
useDescToolsStore.getState().setQaData(updateQaData)
|
||
}
|
||
useDescToolsStore.getState().updateFlag(true)
|
||
}
|
||
}
|
||
notifications.show({
|
||
color: "green",
|
||
message: "应用成功",
|
||
})
|
||
setRecoverOpen(false)
|
||
} catch (error) {
|
||
console.log(error)
|
||
notifications.show({
|
||
color: "red",
|
||
message: "恢复备份失败",
|
||
})
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
projectDetail?.label_type,
|
||
pushStateStack,
|
||
renderPolygons,
|
||
setLabel,
|
||
taskDetail,
|
||
]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (needBackup) {
|
||
handleBackup("auto_backup")
|
||
setNeedBackup(false)
|
||
}
|
||
}, [handleBackup, needBackup, setNeedBackup])
|
||
|
||
// 任务跳转
|
||
const getNextTaskData = async (needSave = false) => {
|
||
if (loadingData) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "视频帧仍在同步,请稍后再切换任务",
|
||
})
|
||
return
|
||
}
|
||
if (needSave) {
|
||
notifications.show({
|
||
id: "save",
|
||
loading: true,
|
||
message: "标注结果自动保存中...",
|
||
autoClose: false,
|
||
})
|
||
try {
|
||
await handleSave()
|
||
} catch (error) {
|
||
console.log(error)
|
||
return
|
||
}
|
||
}
|
||
const params = {
|
||
project_id: taskDetail?.project_id || 0,
|
||
task_status: taskDetail?.label_status || 0,
|
||
rejected: taskDetail?.rejected || false,
|
||
last_task_id: taskDetail?.id || 0,
|
||
}
|
||
const res = await getNextTaskId(params)
|
||
if (res.task_id) {
|
||
router.push(`/label?project_id=${res.project_id}&task_id=${res.task_id}`)
|
||
// 重置图片比例及选中标注对象
|
||
usePaperStore.getState().resetRasterScale()
|
||
useObjectStore.getState().resetPathAndOperationStatus()
|
||
} else {
|
||
// 无id返回时跳转回任务列表页
|
||
const url = backUrl ? basePath + backUrl : "/"
|
||
// router.push(url)
|
||
window.location.href = url
|
||
// 重置图片比例及选中标注对象
|
||
usePaperStore.getState().resetRasterScale()
|
||
useObjectStore.getState().resetPathAndOperationStatus()
|
||
}
|
||
}
|
||
|
||
const commitTaskByActionKey = async (key?: number) => {
|
||
if (loadingData) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "视频帧仍在同步,请稍后再提交",
|
||
})
|
||
return
|
||
}
|
||
if (!key) {
|
||
// 提交时对数据进行校验
|
||
let imgBool = taskDetail!.data_name.some((item) => {
|
||
let operationBool = objectOperations.some((operation) => {
|
||
let bool = labelData
|
||
.get(item.name?.[0])
|
||
?.get(operation.category_id.toString())
|
||
?.some((child) => {
|
||
return getSubAttrStatus(
|
||
getOperationSchema(operation.category_id.toString()),
|
||
child[2]?.sub_attributes
|
||
)
|
||
})
|
||
return bool
|
||
})
|
||
return operationBool
|
||
})
|
||
if (imgBool) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "操作失败,存在未设置子属性的对象",
|
||
})
|
||
return
|
||
}
|
||
// 校验
|
||
try {
|
||
const taskNames = taskDetail!.data_name.map(({ name }) => name[0])
|
||
if (projectDetail && [5, 6].includes(projectDetail.label_type)) {
|
||
taskNames.forEach((name) => {
|
||
// const imageLabelData = labelData.get(name);
|
||
const currentKeyFrameData = useBottomToolsStore
|
||
.getState()
|
||
.keyFrameData.get(name) || {
|
||
key_frame: false,
|
||
}
|
||
// console.log(imageLabelData?.values());
|
||
// let hasLabelObj = false;
|
||
// imageLabelData?.values().forEach((value) => {
|
||
// if (value.length) hasLabelObj = true;
|
||
// });
|
||
if (currentKeyFrameData.key_frame) {
|
||
// if (!hasLabelObj) throw new Error("1");
|
||
if (projectDetail.label_type === 5) {
|
||
// 元操作序列 校验
|
||
const metaData =
|
||
useDescToolsStore.getState().metaData.get(name) || []
|
||
if (!metaData.length) throw new Error("3")
|
||
const descData =
|
||
useDescToolsStore.getState().descData.get(name) || new Map()
|
||
const operations = useDescToolsStore
|
||
.getState()
|
||
.descOperations.map((item) => ({
|
||
operationName: item.label_class,
|
||
attrs: item.sub_attributes_describe,
|
||
}))
|
||
operations.forEach(({ operationName, attrs }) => {
|
||
if (!descData.get(operationName)) throw new Error("2")
|
||
const { value, is_correct } = descData.get(operationName)
|
||
if (!value) throw new Error("2")
|
||
if (!attrs.length) {
|
||
// 无子属性类型
|
||
const [htmlText, text] = value.split(splitWord)
|
||
if (!text || !text.trim()) throw new Error("2")
|
||
if (
|
||
htmlText.includes("<s>") ||
|
||
htmlText.includes("<u>") ||
|
||
htmlText.includes(`"color:`)
|
||
) {
|
||
throw new Error("5")
|
||
}
|
||
// 审核时判断问题是否正确
|
||
if (
|
||
[4, 6].includes(taskDetail?.label_status || 0) &&
|
||
!is_correct
|
||
) {
|
||
throw new Error("9")
|
||
}
|
||
} else {
|
||
// 有子属性类型
|
||
attrs.forEach((attr) => {
|
||
if (attr.isrequired) {
|
||
if (!value[attr.chinese_name]) throw new Error("4")
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
if (projectDetail.label_type === 6) {
|
||
const checkData =
|
||
useDescToolsStore.getState().qaData.get(name) || {}
|
||
|
||
if (!Object.values(checkData).length) throw new Error("2")
|
||
Object.values(checkData).forEach((questionArr) => {
|
||
questionArr.forEach((q) => {
|
||
const v = Object.values(q)[0]
|
||
if (!v.value) throw new Error("2")
|
||
const [htmlText, text] = v.value.split(splitWord)
|
||
if (!text || !text.trim()) throw new Error("2")
|
||
if (
|
||
htmlText.includes("<s>") ||
|
||
htmlText.includes("<u>") ||
|
||
htmlText.includes(`"color:`)
|
||
) {
|
||
throw new Error("5")
|
||
}
|
||
// 标注时判断是否问题已被修改
|
||
if (
|
||
taskDetail?.label_status === 2 &&
|
||
v.tag === 2 &&
|
||
v.flag !== 1
|
||
) {
|
||
throw new Error("10")
|
||
}
|
||
// 审核时判断问题是否正确
|
||
if (
|
||
[4, 6].includes(taskDetail?.label_status || 0) &&
|
||
v.tag !== 1
|
||
) {
|
||
throw new Error("9")
|
||
}
|
||
})
|
||
})
|
||
const counts = useDescToolsStore
|
||
.getState()
|
||
.countAllTurnsAndQaNumber(name)
|
||
if (counts.new_single_turns + counts.new_multiple_turns < 40)
|
||
throw new Error("6")
|
||
if (counts.new_multiple_turns < 25) throw new Error("7")
|
||
}
|
||
} else {
|
||
// if (hasLabelObj) throw new Error("8");
|
||
}
|
||
})
|
||
}
|
||
} catch (error: any) {
|
||
if (error.message === "1") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分关键帧内无关键目标,请修改后再提交",
|
||
})
|
||
} else if (error.message === "2") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分关键帧中的文本描述不全,请修改后再提交",
|
||
})
|
||
} else if (error.message === "3") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分关键帧中的元操作序列为空,请修改后再提交",
|
||
})
|
||
} else if (error.message === "4") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message:
|
||
"当前任务部分关键帧中可选择的描述中,有必填项为空的情况,请修改后再提交",
|
||
})
|
||
} else if (error.message === "5") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分关键帧内批注内容未修改,请修改后再提交",
|
||
})
|
||
} else if (error.message === "6") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分关键帧内的新增问答组数小于40,请修改后再提交",
|
||
})
|
||
} else if (error.message === "7") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message:
|
||
"当前任务部分关键帧内的新增多轮问答组数小于25,请修改后再提交",
|
||
})
|
||
} else if (error.message === "8") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分非关键帧中有关键目标,请修改后再提交",
|
||
})
|
||
} else if (error.message === "9") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前任务部分关键帧中有问题未审核正确,请修改后再提交",
|
||
})
|
||
} else if (error.message === "10") {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message:
|
||
"当前任务部分关键帧中有批注的问题未确认修改,请修改后再提交",
|
||
})
|
||
}
|
||
return
|
||
}
|
||
}
|
||
|
||
try {
|
||
await handleSave()
|
||
} catch (error) {
|
||
console.log(error)
|
||
return
|
||
}
|
||
const currentLabelTime = useLabelTimeStore.getState().labelTime
|
||
const handleAction = (current_status: number) => {
|
||
switch (current_status) {
|
||
case 2:
|
||
return {
|
||
commit_action: key || 1,
|
||
work_time: currentLabelTime.label,
|
||
}
|
||
case 4:
|
||
return {
|
||
commit_action: key || 2,
|
||
work_time: currentLabelTime.review1,
|
||
}
|
||
case 6:
|
||
return {
|
||
commit_action: key || 3,
|
||
work_time: currentLabelTime.review2,
|
||
}
|
||
default:
|
||
return {
|
||
commit_action: 0,
|
||
work_time: 0,
|
||
}
|
||
}
|
||
}
|
||
|
||
let size: null | number = null
|
||
if (key === 4 && projectDetail?.label_type === 6) {
|
||
// 问答式大语言模型项目 驳回操作
|
||
size = 0
|
||
taskDetail?.data_name.forEach((nameObj) => {
|
||
const name = nameObj.name[0]
|
||
const currentQaData =
|
||
useDescToolsStore.getState().qaData.get(name) ?? {}
|
||
const currentKeyFrameData = useBottomToolsStore
|
||
.getState()
|
||
.keyFrameData.get(name) || {
|
||
key_frame: false,
|
||
}
|
||
if (currentKeyFrameData) {
|
||
Object.entries(currentQaData).forEach(([_key, questionArr]) => {
|
||
questionArr.forEach((question) => {
|
||
const v = Object.values(question)[0]
|
||
if (
|
||
v.comment &&
|
||
v.comment.split(splitWord)[1] &&
|
||
v.comment.split(splitWord)[1].trim()
|
||
) {
|
||
size!++
|
||
}
|
||
})
|
||
})
|
||
}
|
||
})
|
||
}
|
||
const params =
|
||
typeof size === "number"
|
||
? {
|
||
task_id: taskDetail?.id || 0,
|
||
uid: user_id,
|
||
...handleAction(taskDetail?.label_status || 0),
|
||
comment_question_size: size,
|
||
}
|
||
: {
|
||
task_id: taskDetail?.id || 0,
|
||
uid: user_id,
|
||
...handleAction(taskDetail?.label_status || 0),
|
||
}
|
||
await taskCommit(params)
|
||
notifications.show({
|
||
color: "green",
|
||
message: "操作成功",
|
||
})
|
||
// 提交成功后跳转下一任务
|
||
getNextTaskData()
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (objectOperations.length) {
|
||
setActiveOperation(objectOperations[0].category_id.toString())
|
||
setPressA(false)
|
||
// open object list
|
||
// setShowObjectList(true);
|
||
}
|
||
}, [objectOperations, setActiveOperation, setPressA, setShowObjectList])
|
||
|
||
useEffect(() => {
|
||
let timer = setInterval(() => {
|
||
if (isView) return
|
||
setLabelTime((prev) => ({
|
||
...prev,
|
||
[currentTimeKey]: prev[currentTimeKey] + 1,
|
||
}))
|
||
}, 1000)
|
||
return () => {
|
||
clearInterval(timer)
|
||
}
|
||
}, [currentTimeKey, isView, setLabelTime])
|
||
|
||
const form = useForm({
|
||
initialValues: {
|
||
sub_attributes: {} as any,
|
||
},
|
||
validate: (values) => {
|
||
const errors: Record<string, string> = {}
|
||
const schema = getOperationSchema(activeOperation)
|
||
if (!schema) return errors
|
||
schema.sub_attributes_describe.forEach((item) => {
|
||
if (item.isrequired) {
|
||
const val = values.sub_attributes?.[item.chinese_name]
|
||
if (
|
||
!val ||
|
||
(Array.isArray(val) && val.length === 0) ||
|
||
(typeof val === "string" && !val.trim())
|
||
) {
|
||
let errorKey = `sub_attributes.${item.chinese_name}`
|
||
errors[errorKey] = `请输入${item.chinese_name}`
|
||
}
|
||
}
|
||
})
|
||
return errors
|
||
},
|
||
})
|
||
|
||
const getPresetFormValues = useCallback(
|
||
(
|
||
operationId: string,
|
||
values: Record<string, string | string[] | undefined> = {}
|
||
) => {
|
||
const schema = getOperationSchema(operationId)
|
||
if (!schema?.sub_attributes_describe?.length) return {}
|
||
|
||
const nextValues: Record<string, string | string[]> = {}
|
||
|
||
schema.sub_attributes_describe.forEach((item) => {
|
||
const currentValue = values[item.chinese_name]
|
||
if (
|
||
currentValue === undefined ||
|
||
currentValue === null ||
|
||
currentValue === ""
|
||
)
|
||
return
|
||
|
||
if (item.select_mode === 2) {
|
||
nextValues[item.chinese_name] = Array.isArray(currentValue)
|
||
? currentValue
|
||
: currentValue
|
||
.split(",")
|
||
.map((value) => value.trim())
|
||
.filter(Boolean)
|
||
return
|
||
}
|
||
|
||
nextValues[item.chinese_name] = Array.isArray(currentValue)
|
||
? currentValue[0] || ""
|
||
: currentValue
|
||
})
|
||
|
||
return nextValues
|
||
},
|
||
[getOperationSchema]
|
||
)
|
||
|
||
const getPresetStoreValues = useCallback(
|
||
(operationId: string, values: Record<string, any> = {}) => {
|
||
const schema = getOperationSchema(operationId)
|
||
if (!schema?.sub_attributes_describe?.length) return {}
|
||
|
||
const nextValues: Record<string, string> = {}
|
||
|
||
schema.sub_attributes_describe.forEach((item) => {
|
||
const currentValue = values[item.chinese_name]
|
||
if (
|
||
currentValue === undefined ||
|
||
currentValue === null ||
|
||
currentValue === ""
|
||
)
|
||
return
|
||
|
||
if (item.select_mode === 2) {
|
||
nextValues[item.chinese_name] = Array.isArray(currentValue)
|
||
? currentValue.join(",")
|
||
: currentValue
|
||
return
|
||
}
|
||
|
||
nextValues[item.chinese_name] = Array.isArray(currentValue)
|
||
? currentValue[0] || ""
|
||
: currentValue
|
||
})
|
||
|
||
return nextValues
|
||
},
|
||
[getOperationSchema]
|
||
)
|
||
|
||
useEffect(() => {
|
||
let operationSchema = getOperationSchema(activeOperation)
|
||
if (operationSchema?.label_class && subAttrPresetShow) {
|
||
form.setFieldValue(
|
||
"sub_attributes",
|
||
getPresetFormValues(
|
||
activeOperation,
|
||
subAttributes[operationSchema?.label_class] || {}
|
||
)
|
||
)
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
activeOperation,
|
||
getOperationSchema,
|
||
getPresetFormValues,
|
||
subAttributes,
|
||
subAttrPresetShow,
|
||
])
|
||
|
||
useEffect(() => {
|
||
usePaperStore.getState().polygonTool?.emit("keydown", { key: "escape" })
|
||
usePaperStore.getState().rectangleTool?.emit("keydown", { key: "escape" })
|
||
usePaperStore.getState().brushTool?.emit("keydown", { key: "escape" })
|
||
usePaperStore.getState().pointTool?.emit("keydown", { key: "escape" })
|
||
}, [activeOperation, mode])
|
||
|
||
const operationSchema = getOperationSchema(activeOperation)
|
||
const drawAction = useKeyboardStore((state) => state.drawAction)
|
||
|
||
const subAttrForm = (
|
||
<Box w={256}>
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
pb={4}
|
||
mb={8}
|
||
style={{
|
||
borderBottom:
|
||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
fontWeight: 700,
|
||
}}>
|
||
<Box>
|
||
<Text span c="blue">
|
||
{`${operationSchema?.label_class || ""}`}
|
||
</Text>
|
||
<Text span size="sm" ml={4}>
|
||
子属性
|
||
</Text>
|
||
</Box>
|
||
<Flex>
|
||
<Button
|
||
variant="transparent"
|
||
size="xs"
|
||
onClick={async () => {
|
||
const { hasErrors } = form.validate()
|
||
if (hasErrors) return
|
||
if (operationSchema?.label_class) {
|
||
const nextPresetValues = getPresetStoreValues(
|
||
activeOperation,
|
||
safeClone(form.values.sub_attributes || {})
|
||
)
|
||
setsubAttributes(operationSchema.label_class, nextPresetValues)
|
||
}
|
||
}}>
|
||
确定预选
|
||
</Button>
|
||
<Button
|
||
variant="transparent"
|
||
size="xs"
|
||
onClick={() => {
|
||
form.reset()
|
||
if (operationSchema?.label_class)
|
||
setsubAttributes(operationSchema.label_class, {})
|
||
}}>
|
||
重置
|
||
</Button>
|
||
</Flex>
|
||
</Flex>
|
||
<ScrollArea.Autosize mah={300}>
|
||
<Stack gap="xs">
|
||
{operationSchema?.sub_attributes_describe.map((item) => {
|
||
if (item.select_mode === 0) {
|
||
return (
|
||
<TextInput
|
||
key={item.chinese_name}
|
||
label={item.chinese_name}
|
||
placeholder={`请输入${item.chinese_name}`}
|
||
size="xs"
|
||
{...form.getInputProps(`sub_attributes.${item.chinese_name}`)}
|
||
/>
|
||
)
|
||
} else if (item.select_mode === 1) {
|
||
return (
|
||
<Radio.Group
|
||
key={item.chinese_name}
|
||
label={item.chinese_name}
|
||
size="xs"
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}>
|
||
<Group mt="xs" gap="xs">
|
||
{item.optional_item.map((option) => (
|
||
<Radio key={option} value={option} label={option} />
|
||
))}
|
||
</Group>
|
||
</Radio.Group>
|
||
)
|
||
} else if (item.select_mode === 2) {
|
||
return (
|
||
<Checkbox.Group
|
||
key={item.chinese_name}
|
||
label={item.chinese_name}
|
||
size="xs"
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}>
|
||
<Group mt="xs" gap="xs">
|
||
{item.optional_item.map((option) => (
|
||
<Checkbox key={option} value={option} label={option} />
|
||
))}
|
||
</Group>
|
||
</Checkbox.Group>
|
||
)
|
||
} else {
|
||
return null
|
||
}
|
||
})}
|
||
</Stack>
|
||
</ScrollArea.Autosize>
|
||
</Box>
|
||
)
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
handleSave,
|
||
handleBackup,
|
||
commitTaskByActionKey,
|
||
}))
|
||
|
||
const renderEditIcon = useMemo(() => {
|
||
const iconStyle = { width: 20, height: 20 }
|
||
if (mode === "support") return <Paintbrush style={iconStyle} />
|
||
if (editMode) {
|
||
if (pressA && drawAction === "subtract") {
|
||
return <CopyMinus style={iconStyle} />
|
||
}
|
||
if (pressA && drawAction === "add") {
|
||
return <CopyPlus style={iconStyle} />
|
||
}
|
||
return pressA ? (
|
||
<CirclePlus style={iconStyle} />
|
||
) : (
|
||
<Pen style={iconStyle} />
|
||
)
|
||
} else {
|
||
return <MousePointer style={iconStyle} />
|
||
}
|
||
}, [mode, editMode, pressA, drawAction])
|
||
|
||
const showTagConfigContainer = useMemo(() => {
|
||
const configs = ["ID", "类别", "外框", "点ID", "子属性", "属性值"]
|
||
return (
|
||
<Flex direction="column" align="center">
|
||
{configs.map((config) => (
|
||
<Box
|
||
key={config}
|
||
w="100%"
|
||
style={{
|
||
textAlign: "center",
|
||
cursor: "pointer",
|
||
borderRadius: "4px",
|
||
padding: "4px 8px",
|
||
}}
|
||
c={showTagsConfigs.includes(config) ? "blue" : undefined}
|
||
onClick={() => {
|
||
let arr = safeClone(showTagsConfigs)
|
||
const index = arr.findIndex((item) => item === config)
|
||
if (index !== -1) {
|
||
arr.splice(index, 1)
|
||
} else {
|
||
arr.push(config)
|
||
}
|
||
setShowTagsConfigs(arr)
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.backgroundColor =
|
||
"var(--mantine-color-gray-1)"
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.backgroundColor = "transparent"
|
||
}}>
|
||
{config}
|
||
</Box>
|
||
))}
|
||
</Flex>
|
||
)
|
||
}, [setShowTagsConfigs, showTagsConfigs])
|
||
|
||
const showSaveConfigContainer = () => {
|
||
return (
|
||
<Box>
|
||
<Flex align="center" gap="xs" mb="xs">
|
||
自动保存
|
||
<Switch
|
||
onLabel="是"
|
||
offLabel="否"
|
||
checked={isAutoSave}
|
||
onChange={(event) => setIsAutoSave(event.currentTarget.checked)}
|
||
/>
|
||
</Flex>
|
||
<Flex align="center" gap="xs">
|
||
自动保存时间间隔
|
||
<Select
|
||
data={[
|
||
{ label: "5", value: "5" },
|
||
{ label: "10", value: "10" },
|
||
{ label: "15", value: "15" },
|
||
{ label: "30", value: "30" },
|
||
{ label: "45", value: "45" },
|
||
]}
|
||
size="xs"
|
||
w={80}
|
||
value={autoSaveGap.toString()}
|
||
onChange={(value) => setAutoSaveGap(Number(value))}
|
||
allowDeselect={false}
|
||
/>
|
||
分钟
|
||
</Flex>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Box w="100%" h="100%">
|
||
<Flex
|
||
h="50%"
|
||
justify="center"
|
||
style={{
|
||
borderBottom:
|
||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Box
|
||
onClick={() => {
|
||
console.log(useLabelStore.getState().label)
|
||
console.log(useRightToolsStore.getState().pathGroupMap)
|
||
console.log(usePaperStore.getState().group)
|
||
}}
|
||
display="none">
|
||
test
|
||
</Box>
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
gap="xs"
|
||
px="md"
|
||
flex={1}
|
||
style={{ justifyContent: "flex-start" }}>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c="dark"
|
||
onClick={() => {
|
||
if (isView) {
|
||
const url = backUrl ? basePath + backUrl : "/"
|
||
// router.push(url)
|
||
window.location.href = url
|
||
// handleBackup("auto_backup");
|
||
// 重置图片比例及选中标注对象
|
||
usePaperStore.getState().resetRasterScale()
|
||
useObjectStore.getState().resetPathAndOperationStatus()
|
||
} else {
|
||
setConfirmOpen(true)
|
||
}
|
||
}}>
|
||
<ChevronLeftIcon style={{ width: 24, height: 24 }} />
|
||
</ActionIcon>
|
||
{/* <ActionIcon
|
||
variant="transparent"
|
||
c="var(--mantine-color-text)"
|
||
onClick={() => {
|
||
let url = `${window.location.origin}/component/label/picture/standalone`
|
||
window.open(url, "_blank")
|
||
}}>
|
||
<IconCornerRightUp size={22} />
|
||
</ActionIcon> */}
|
||
<HoverCard width={280} shadow="md">
|
||
<HoverCard.Target>
|
||
<Flex align="center" style={{ maxWidth: 390 }}>
|
||
<Box style={{ minWidth: "fit-content" }}>
|
||
{taskDetail?.label_status && (
|
||
<TaskStatusTag
|
||
status={taskDetail.label_status}
|
||
rejected={taskDetail.rejected}
|
||
center
|
||
/>
|
||
)}
|
||
</Box>
|
||
<Box px={4}>|</Box>
|
||
<Flex justify="space-between" align="center" flex={1}>
|
||
<Box style={{ maxWidth: 256, fontSize: 12 }}>
|
||
<Text truncate="end" size="xs">
|
||
{`${projectDetail?.name} - ${taskDetail?.id || 0}`}
|
||
</Text>
|
||
</Box>
|
||
<ChevronDownIcon style={{ width: 12, height: 12 }} />
|
||
</Flex>
|
||
</Flex>
|
||
</HoverCard.Target>
|
||
<HoverCard.Dropdown w="fit-content">
|
||
<Flex justify="space-between" gap="md">
|
||
<Stack gap={4}>
|
||
<Text fw={600} size="sm">
|
||
{`${
|
||
taskDetail?.label_status &&
|
||
TaskStatusEnum.get(taskDetail?.label_status)
|
||
}|${taskDetail?.project_name}-${taskDetail?.id}`}
|
||
</Text>
|
||
<Text size="sm">图片名称:{activeImage}</Text>
|
||
<Text size="sm">标注员:{taskDetail?.label_username}</Text>
|
||
<Text size="sm">
|
||
审核员(1):{taskDetail?.review1_username}
|
||
</Text>
|
||
<Text size="sm">
|
||
审核员(2):{taskDetail?.review2_username}
|
||
</Text>
|
||
<Text size="sm">已确认:</Text>
|
||
<Text size="sm">未确认:</Text>
|
||
<Text size="sm">正常创建:</Text>
|
||
</Stack>
|
||
</Flex>
|
||
</HoverCard.Dropdown>
|
||
</HoverCard>
|
||
<Box style={{ maxWidth: 300 }}>
|
||
<Text truncate="end" size="sm" title={activeImage}>
|
||
{activeImage}
|
||
</Text>
|
||
</Box>
|
||
</Flex>
|
||
<Flex justify="space-between" align="center" gap="md" px="md">
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={isView ? "var(--mantine-color-text)" : "gray"}
|
||
onClick={() => {
|
||
if (currentEditAuth) setIsView(!isView)
|
||
}}>
|
||
<MonitorSmartphone style={{ width: 20, height: 20 }} />
|
||
</ActionIcon>
|
||
{isLabelTask && (
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={!isView ? "var(--mantine-color-text)" : "gray"}
|
||
onClick={() => {
|
||
setIsView(!isView)
|
||
}}>
|
||
<SquarePen style={{ width: 20, height: 20 }} />
|
||
</ActionIcon>
|
||
)}
|
||
<Eclipse
|
||
style={{ width: 20, height: 20, display: "none" }}
|
||
color="gray"
|
||
/>
|
||
<CopySlash style={{ width: 20, height: 20, display: "none" }} />
|
||
</Flex>
|
||
<Flex
|
||
justify="flex-end"
|
||
align="center"
|
||
gap="md"
|
||
px="md"
|
||
flex={1}
|
||
style={{ fontSize: 14 }}>
|
||
<Box flex={1}></Box>
|
||
<Tooltip label="任务缓存">
|
||
<ActionIcon
|
||
variant="transparent"
|
||
color="var(--mantine-color-text)"
|
||
onClick={() => setTaskOpen(true)}>
|
||
<FolderDownIcon style={{ width: 20, height: 20 }} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
{isLabelTask && (
|
||
<Flex justify="space-between" align="center" gap="md" px="md">
|
||
<Timer style={{ width: 20, height: 20 }} />
|
||
<TaskTimerDisplay currentTimeKey={currentTimeKey} />
|
||
</Flex>
|
||
)}
|
||
<Flex justify="flex-end" align="center" gap="md" px="0" flex={1}>
|
||
<UnstyledButton
|
||
variant="transparent"
|
||
c={"blue"}
|
||
fz={"sm"}
|
||
disabled={isView}
|
||
style={{ cursor: isView ? "default" : "pointer" }}
|
||
onClick={() => {
|
||
if (!isView) commitTaskByActionKey()
|
||
}}>
|
||
提交
|
||
</UnstyledButton>
|
||
<UnstyledButton
|
||
variant="transparent"
|
||
c={"blue"}
|
||
fz={"sm"}
|
||
style={{ cursor: isView ? "default" : "pointer" }}
|
||
disabled={isView}
|
||
onClick={() => {
|
||
if (!isView) getNextTaskData(true)
|
||
}}>
|
||
跳过
|
||
</UnstyledButton>
|
||
{taskDetail?.label_status &&
|
||
(TaskStatusEnum.get(taskDetail?.label_status) === "审核中" ||
|
||
TaskStatusEnum.get(taskDetail?.label_status) === "复审中") && (
|
||
<UnstyledButton
|
||
variant="transparent"
|
||
c={"red"}
|
||
fz={"sm"}
|
||
style={{ cursor: isView ? "default" : "pointer" }}
|
||
disabled={isView}
|
||
onClick={() => {
|
||
if (!isView) commitTaskByActionKey(4)
|
||
}}>
|
||
驳回
|
||
</UnstyledButton>
|
||
)}
|
||
<UnstyledButton
|
||
variant="transparent"
|
||
fz={"sm"}
|
||
c={isView ? "dimmed" : "red"}
|
||
disabled={isView}
|
||
onClick={() => {
|
||
if (!isView) commitTaskByActionKey(5)
|
||
}}>
|
||
无法标注
|
||
</UnstyledButton>
|
||
</Flex>
|
||
</Flex>
|
||
</Flex>
|
||
<Flex
|
||
h="50%"
|
||
justify="center"
|
||
style={{
|
||
borderBottom:
|
||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Flex justify="flex-start" align="center" px="md" flex={1} gap="xs">
|
||
<Popover position="bottom-start" zIndex={49} shadow="md">
|
||
<Popover.Target>
|
||
<Flex align="center">
|
||
<ActionIcon variant="transparent" c="var(--mantine-color-text)">
|
||
<Eclipse style={{ width: 20, height: 20, marginRight: 2 }} />
|
||
<ChevronDownIcon style={{ width: 12, height: 12 }} />
|
||
</ActionIcon>
|
||
</Flex>
|
||
</Popover.Target>
|
||
<Popover.Dropdown>
|
||
<Stack gap="md" mb={"md"}>
|
||
<Flex align="center" gap="xs">
|
||
<Box miw={36}>{imageFilter.brightness}</Box>
|
||
<Slider
|
||
w={96}
|
||
onChange={(value) => {
|
||
setImageFilter("brightness", value)
|
||
}}
|
||
value={imageFilter.brightness}
|
||
min={-100}
|
||
marks={[{ value: 0, label: "0" }]}
|
||
max={100}
|
||
/>
|
||
<Box>亮度</Box>
|
||
</Flex>
|
||
<Flex align="center" gap="xs">
|
||
<Box miw={36}>{imageFilter.saturate}</Box>
|
||
<Slider
|
||
w={96}
|
||
onChange={(value) => {
|
||
setImageFilter("saturate", value)
|
||
}}
|
||
value={imageFilter.saturate}
|
||
min={-100}
|
||
marks={[{ value: 0, label: "0" }]}
|
||
max={100}
|
||
/>
|
||
<Box>饱和度</Box>
|
||
</Flex>
|
||
<Flex align="center" gap="xs">
|
||
<Box miw={36}>{imageFilter.contrast}</Box>
|
||
<Slider
|
||
w={96}
|
||
onChange={(value) => {
|
||
setImageFilter("contrast", value)
|
||
}}
|
||
value={imageFilter.contrast}
|
||
min={-100}
|
||
marks={[{ value: 0, label: "0" }]}
|
||
max={100}
|
||
/>
|
||
<Box>对比度</Box>
|
||
</Flex>
|
||
</Stack>
|
||
</Popover.Dropdown>
|
||
</Popover>
|
||
<Divider orientation="vertical" />
|
||
<Flex justify="space-between" align="center" p={8} gap="xs">
|
||
{crosshairStatus === "hidden" && (
|
||
<LocateOff
|
||
style={{ width: 20, height: 20, cursor: "pointer" }}
|
||
onClick={() => {
|
||
setCrosshairStatus("line")
|
||
}}
|
||
/>
|
||
)}
|
||
{crosshairStatus === "line" && (
|
||
<Locate
|
||
style={{ width: 20, height: 20, cursor: "pointer" }}
|
||
onClick={() => {
|
||
setCrosshairStatus("carve")
|
||
}}
|
||
/>
|
||
)}
|
||
{crosshairStatus === "carve" && (
|
||
<LocateFixed
|
||
style={{ width: 20, height: 20, cursor: "pointer" }}
|
||
onClick={() => {
|
||
setCrosshairStatus("hidden")
|
||
}}
|
||
/>
|
||
)}
|
||
</Flex>
|
||
<Flex justify="space-between" align="center" p={8} gap={4}>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
style={{ cursor: "pointer" }}
|
||
c={assistToolEnabled ? "blue" : "var(--mantine-color-text)"}
|
||
onClick={() => {
|
||
setAssistToolEnabled(!assistToolEnabled)
|
||
}}>
|
||
<Eclipse size={16} />
|
||
</ActionIcon>
|
||
<NumberInput
|
||
value={assistToolSize}
|
||
w={58}
|
||
min={1}
|
||
size="xs"
|
||
hideControls
|
||
onChange={(v) => {
|
||
const next = Number(v)
|
||
if (Number.isFinite(next) && next >= 1) {
|
||
setAssistToolSize(next)
|
||
}
|
||
}}
|
||
onFocus={() => {
|
||
useKeyEventStore.getState().setFocusInput(true)
|
||
}}
|
||
onBlur={() => {
|
||
useKeyEventStore.getState().setFocusInput(false)
|
||
}}
|
||
/>
|
||
</Flex>
|
||
<Divider orientation="vertical" />
|
||
<Flex justify="center" align="center">
|
||
<ActionIcon
|
||
variant="transparent"
|
||
style={{ cursor: "pointer" }}
|
||
c={showTags ? "blue" : "var(--mantine-color-text)"}
|
||
onClick={() => setShowTags(!showTags)}>
|
||
<Tag size={16} />
|
||
</ActionIcon>
|
||
<Popover position="bottom" shadow="md">
|
||
<Popover.Target>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={
|
||
showTagsConfigs.length
|
||
? "blue"
|
||
: "var(--mantine-color-text)"
|
||
}>
|
||
<IconCaretDownFilled size={16} />
|
||
</ActionIcon>
|
||
</Popover.Target>
|
||
<Popover.Dropdown>{showTagConfigContainer}</Popover.Dropdown>
|
||
</Popover>
|
||
</Flex>
|
||
<Divider orientation="vertical" />
|
||
<Flex justify="space-between" align="center">
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={
|
||
drawOption !== "default" ? "gray" : "var(--mantine-color-text)"
|
||
}
|
||
onClick={() => {
|
||
setDrawOption("default")
|
||
}}>
|
||
<Copy size={16} />
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={
|
||
drawOption !== "intersect"
|
||
? "gray"
|
||
: "var(--mantine-color-text)"
|
||
}
|
||
onClick={() => {
|
||
setDrawOption("intersect")
|
||
}}>
|
||
<CopyMinus size={16} />
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={drawOption !== "unite" ? "gray" : "var(--mantine-color-text)"}
|
||
onClick={() => {
|
||
setDrawOption("unite")
|
||
}}>
|
||
<CopyPlus size={16} />
|
||
</ActionIcon>
|
||
</Flex>
|
||
<Divider orientation="vertical" />
|
||
<ActionIcon
|
||
variant="transparent"
|
||
style={{ cursor: "pointer", borderRadius: 8, padding: 4 }}
|
||
c={magnetFlag ? "blue" : "var(--mantine-color-text)"}
|
||
onClick={() => {
|
||
setMagnetFlag(!magnetFlag)
|
||
}}>
|
||
<MagnetIcon size={16} />
|
||
</ActionIcon>
|
||
<Flex justify="space-between" align="center" gap="xs">
|
||
<Checkbox
|
||
id="terms"
|
||
size="xs"
|
||
radius="sm"
|
||
checked={saveCurrentScale}
|
||
onChange={(event) => {
|
||
setSaveCurrentScale(event.currentTarget.checked)
|
||
}}
|
||
/>
|
||
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||
缓存缩放比例
|
||
</Text>
|
||
</Flex>
|
||
</Flex>
|
||
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
||
<NumberInput
|
||
value={checkSize}
|
||
w={50}
|
||
min={1}
|
||
size="xs"
|
||
radius="sm"
|
||
hideControls
|
||
onChange={(v) => {
|
||
if (v) setCheckSize(Number(v))
|
||
}}
|
||
onFocus={() => {
|
||
useKeyEventStore.getState().setFocusInput(true)
|
||
}}
|
||
onBlur={() => {
|
||
useKeyEventStore.getState().setFocusInput(false)
|
||
}}
|
||
/>
|
||
{renderEditIcon}
|
||
<Button
|
||
w="fit-content"
|
||
style={{ display: "none" }}
|
||
onClick={() => {
|
||
setEditMode(true)
|
||
}}>
|
||
{renderOperationIcon(getOperationSchema(activeOperation))}
|
||
</Button>
|
||
<Popover
|
||
position="bottom-start"
|
||
zIndex={49}
|
||
shadow="md"
|
||
width={280}
|
||
opened={
|
||
!!operationSchema?.sub_attributes_describe?.length &&
|
||
subAttrPresetShow
|
||
}
|
||
onChange={setSubAttrPresetShow}>
|
||
<Popover.Target>
|
||
<UnstyledButton
|
||
onClick={() => {
|
||
if (!operationSchema?.sub_attributes_describe?.length) return
|
||
setSubAttrPresetShow(!subAttrPresetShow)
|
||
}}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
borderRadius: 6,
|
||
padding: "2px 4px",
|
||
cursor: operationSchema?.sub_attributes_describe?.length
|
||
? "pointer"
|
||
: "default",
|
||
opacity: operationSchema?.sub_attributes_describe?.length
|
||
? 1
|
||
: 0.6,
|
||
}}>
|
||
{renderOperationIcon(operationSchema)}
|
||
<Text span size="xs" style={{ width: "max-content" }}>
|
||
{operationSchema?.label_class || ""}
|
||
</Text>
|
||
</UnstyledButton>
|
||
</Popover.Target>
|
||
<Popover.Dropdown>{subAttrForm}</Popover.Dropdown>
|
||
</Popover>
|
||
<Popover
|
||
position="bottom-start"
|
||
shadow="md"
|
||
width={280}
|
||
opened={operationPickerOpened}
|
||
onChange={setOperationPickerOpened}>
|
||
<Popover.Target>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={"var(--mantine-color-text)"}
|
||
onClick={() => {
|
||
setOperationPickerOpened((opened) => !opened)
|
||
if (operationPickerOpened) setOperationKeyword("")
|
||
}}>
|
||
<IconChevronDown size={16} />
|
||
</ActionIcon>
|
||
</Popover.Target>
|
||
<Popover.Dropdown p="xs">
|
||
<Stack gap="xs">
|
||
<TextInput
|
||
value={operationKeyword}
|
||
size="xs"
|
||
placeholder="搜索类别名 / 超类 / ID"
|
||
onChange={(event) => {
|
||
setOperationKeyword(event.currentTarget.value)
|
||
}}
|
||
onFocus={() => {
|
||
useKeyEventStore.getState().setFocusInput(true)
|
||
}}
|
||
onBlur={() => {
|
||
useKeyEventStore.getState().setFocusInput(false)
|
||
}}
|
||
/>
|
||
<Group justify="space-between" gap="xs">
|
||
<Text size="10px" c="dimmed">
|
||
共 {objectOperations.length} 个类别
|
||
</Text>
|
||
{operationKeyword ? (
|
||
<Text size="10px" c="dimmed">
|
||
匹配 {filteredObjectOperations.length} 个
|
||
</Text>
|
||
) : null}
|
||
</Group>
|
||
<ScrollArea.Autosize mah={320} offsetScrollbars>
|
||
<Stack gap={4}>
|
||
{filteredObjectOperations.length ? (
|
||
filteredObjectOperations.map((item) => {
|
||
const isActive =
|
||
item.category_id.toString() === activeOperation
|
||
|
||
return (
|
||
<UnstyledButton
|
||
key={item.category_id}
|
||
onClick={() => {
|
||
setActiveOperation(item.category_id.toString())
|
||
setOperationPickerOpened(false)
|
||
setOperationKeyword("")
|
||
}}
|
||
style={{
|
||
width: "100%",
|
||
borderRadius: 8,
|
||
padding: "8px 10px",
|
||
border: isActive
|
||
? "1px solid var(--mantine-color-blue-4)"
|
||
: "1px solid transparent",
|
||
background: isActive
|
||
? "var(--mantine-color-blue-light)"
|
||
: "transparent",
|
||
}}>
|
||
<Flex
|
||
align="center"
|
||
justify="space-between"
|
||
gap="xs">
|
||
<Group gap={8} wrap="nowrap">
|
||
<Box
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}>
|
||
{renderOperationIcon(
|
||
getOperationSchema(
|
||
item.category_id.toString()
|
||
)
|
||
)}
|
||
</Box>
|
||
<Box style={{ minWidth: 0 }}>
|
||
<Text
|
||
size="xs"
|
||
fw={isActive ? 600 : 500}
|
||
truncate="end">
|
||
{item.label_class || ""}
|
||
</Text>
|
||
<Text size="10px" c="dimmed" truncate="end">
|
||
ID: {item.category_id}
|
||
{item.super_category
|
||
? ` / ${item.super_category}`
|
||
: ""}
|
||
</Text>
|
||
</Box>
|
||
</Group>
|
||
{isActive ? (
|
||
<Text size="10px" c="blue" fw={700}>
|
||
当前
|
||
</Text>
|
||
) : null}
|
||
</Flex>
|
||
</UnstyledButton>
|
||
)
|
||
})
|
||
) : (
|
||
<Text size="xs" c="dimmed" ta="center" py="sm">
|
||
没有匹配的类别
|
||
</Text>
|
||
)}
|
||
</Stack>
|
||
</ScrollArea.Autosize>
|
||
</Stack>
|
||
</Popover.Dropdown>
|
||
</Popover>
|
||
</Flex>
|
||
<Flex justify="flex-end" align="center" px="xs" gap="xs" flex={1}>
|
||
<Button
|
||
variant="transparent"
|
||
style={{ display: "none" }}
|
||
leftSection={<Baseline style={{ width: 20, height: 20 }} />}>
|
||
<Text size="xs">自动标注</Text>
|
||
</Button>
|
||
<Divider orientation="vertical" />
|
||
<Flex justify="space-between" align="center" p={8} gap="md">
|
||
{!isView ? (
|
||
<>
|
||
<Flex align="center" gap={4}>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c="var(--mantine-color-text)"
|
||
style={{
|
||
width: "auto",
|
||
display: "flex",
|
||
gap: 4,
|
||
opacity: loadingData ? 0.5 : 1,
|
||
cursor: loadingData ? "not-allowed" : "pointer",
|
||
}}
|
||
onClick={() => {
|
||
if (!loadingData) handleSave()
|
||
}}>
|
||
<Cloud style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">保存</Text>
|
||
</ActionIcon>
|
||
<Flex align="center" justify="center">
|
||
<Popover position="bottom" shadow="md">
|
||
<Popover.Target>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c="var(--mantine-color-text)"
|
||
style={{ width: "auto", display: "flex", gap: 4 }}>
|
||
<IconChevronDown style={{ width: 20, height: 20 }} />
|
||
</ActionIcon>
|
||
</Popover.Target>
|
||
<Popover.Dropdown>
|
||
{showSaveConfigContainer()}
|
||
</Popover.Dropdown>
|
||
</Popover>
|
||
</Flex>
|
||
</Flex>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c="var(--mantine-color-text)"
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setBackupOpen(true)
|
||
}}>
|
||
<CloudDownload style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">备份</Text>
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c="var(--mantine-color-text)"
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setRecoverOpen(true)
|
||
}}>
|
||
<CloudCog style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">恢复</Text>
|
||
</ActionIcon>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
gap={4}
|
||
style={{ opacity: 0.5, cursor: "not-allowed" }}>
|
||
<Cloud style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">保存</Text>
|
||
</Flex>
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
gap={4}
|
||
style={{ opacity: 0.5, cursor: "not-allowed" }}>
|
||
<CloudDownload style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">备份</Text>
|
||
</Flex>
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
gap={4}
|
||
style={{ opacity: 0.5, cursor: "not-allowed" }}>
|
||
<CloudCog style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">恢复</Text>
|
||
</Flex>
|
||
</>
|
||
)}
|
||
</Flex>
|
||
<Divider orientation="vertical" />
|
||
<Flex justify="space-between" align="center" p={8} gap="md">
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={!showTaskList ? "gray" : "var(--mantine-color-text)"}
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setShowTaskList(!showTaskList)
|
||
}}>
|
||
<ClipboardList style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">任务</Text>
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={!showMultiFrame ? "gray" : "var(--mantine-color-text)"}
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setShowMultiFrame(!showMultiFrame)
|
||
}}>
|
||
<Film style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">多帧</Text>
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={!showGroupList ? "gray" : "var(--mantine-color-text)"}
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setShowGroupList(!showGroupList)
|
||
}}>
|
||
<GroupIcon style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">群组</Text>
|
||
</ActionIcon>
|
||
{projectDetail && [5, 6].includes(projectDetail.label_type) ? (
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={!showDescList ? "gray" : "var(--mantine-color-text)"}
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setShowDescList(!showDescList)
|
||
}}>
|
||
<BadgeInfoIcon style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">描述</Text>
|
||
</ActionIcon>
|
||
) : null}
|
||
<ActionIcon
|
||
variant="transparent"
|
||
c={!showObjectList ? "gray" : "var(--mantine-color-text)"}
|
||
style={{ width: "auto", display: "flex", gap: 4 }}
|
||
onClick={() => {
|
||
setShowObjectList(!showObjectList)
|
||
}}>
|
||
<Component style={{ width: 20, height: 20 }} />
|
||
<Text size="xs">对象</Text>
|
||
</ActionIcon>
|
||
</Flex>
|
||
</Flex>
|
||
</Flex>
|
||
{confirmOpen && (
|
||
<BackConfirmModal
|
||
open={confirmOpen}
|
||
handleOk={async () => {
|
||
try {
|
||
await handleSave()
|
||
} catch (error) {
|
||
console.log(error)
|
||
return
|
||
}
|
||
setConfirmOpen(false)
|
||
const url = backUrl ? basePath + backUrl : "/"
|
||
// router.push(url)
|
||
window.location.href = url
|
||
// 重置图片比例及选中标注对象
|
||
usePaperStore.getState().resetRasterScale()
|
||
useObjectStore.getState().resetPathAndOperationStatus()
|
||
}}
|
||
handleCancel={async () => {
|
||
try {
|
||
setConfirmOpen(false)
|
||
handleBackup("auto_backup")
|
||
const url = backUrl ? basePath + backUrl : "/"
|
||
// router.push(url)
|
||
window.location.href = url
|
||
// 重置图片比例及选中标注对象
|
||
usePaperStore.getState().resetRasterScale()
|
||
useObjectStore.getState().resetPathAndOperationStatus()
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setConfirmOpen(false)
|
||
}}
|
||
/>
|
||
)}
|
||
{backupOpen && (
|
||
<BackupModal
|
||
open={backupOpen}
|
||
handleCancel={() => {
|
||
setBackupOpen(false)
|
||
}}
|
||
handleOk={handleBackup}
|
||
/>
|
||
)}
|
||
{duplicateConfirmOpen && (
|
||
<ConfirmModal
|
||
open={duplicateConfirmOpen}
|
||
title="温馨提示"
|
||
content={`${
|
||
`【${duplicateName}】` || "备份名称"
|
||
}已存在,是否替换原有备份?`}
|
||
onOk={async () => {
|
||
try {
|
||
let newTaskLabelData = new Map()
|
||
let textData = new Map()
|
||
let metaData = new Map()
|
||
taskDetail!.data_name.forEach((item) => {
|
||
const name = item.name?.[0] ?? ""
|
||
if (labelData.get(name))
|
||
newTaskLabelData.set(name, labelData.get(name))
|
||
if (projectDetail?.label_type === 5) {
|
||
const desc = useDescToolsStore.getState().descData.get(name)
|
||
if (desc) {
|
||
textData.set(name, desc)
|
||
}
|
||
const meta = useDescToolsStore.getState().metaData.get(name)
|
||
if (meta) metaData.set(name, desc)
|
||
}
|
||
if (projectDetail?.label_type === 6) {
|
||
const qaDesc = useDescToolsStore.getState().qaData.get(name)
|
||
if (qaDesc) textData.set(name, qaDesc)
|
||
}
|
||
})
|
||
// 调整备份数据结果的点位比例
|
||
const adjustedData = adjustAllPoints(
|
||
newTaskLabelData,
|
||
usePaperStore.getState().reciprocalRasterScale
|
||
)
|
||
let taskLabelData = await storage.getItem(
|
||
taskDetail!.id.toString()
|
||
)
|
||
let newState = {
|
||
...taskLabelData?.state,
|
||
[duplicateName]: adjustedData,
|
||
[`${duplicateName}-text`]: {
|
||
text: textData,
|
||
meta: metaData,
|
||
},
|
||
}
|
||
storage.setItem(taskDetail!.id.toString(), {
|
||
state: newState,
|
||
})
|
||
setDuplicateConfirmOpen(false)
|
||
setDuplicateName("")
|
||
setBackupOpen(false)
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}}
|
||
onCancel={() => {
|
||
setDuplicateName("")
|
||
setDuplicateConfirmOpen(false)
|
||
}}
|
||
/>
|
||
)}
|
||
{recoverOpen && (
|
||
<RecoverModal
|
||
open={recoverOpen}
|
||
handleCancel={() => {
|
||
setRecoverOpen(false)
|
||
}}
|
||
handleOk={handleRecover}
|
||
/>
|
||
)}
|
||
{taskOpen && (
|
||
<TaskCacheModal
|
||
open={taskOpen}
|
||
handleClose={() => {
|
||
setTaskOpen(false)
|
||
}}
|
||
taskDetail={taskDetail}
|
||
/>
|
||
)}
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export default forwardRef(TopTools)
|