Files
labelimage/components/label/components/TopTools.tsx
2026-04-14 15:38:57 +08:00

2670 lines
90 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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,
SquareDashedMousePointer,
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, requestResizeGhost } = 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 [submitConfirmOpen, setSubmitConfirmOpen] = 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,
renderMode,
setRenderMode,
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 visibleRightPanelCount = useMemo(() => {
return [
showTaskList,
showGroupList,
showDescList &&
!!projectDetail &&
[5, 6].includes(projectDetail.label_type),
showObjectList,
].filter(Boolean).length
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
const toggleRightPanel = useCallback(
(visible: boolean, setter: (val: boolean) => void) => {
const nextVisible = !visible
const nextCount = visibleRightPanelCount + (nextVisible ? 1 : -1)
if (
(visibleRightPanelCount === 0 && nextCount === 1) ||
(visibleRightPanelCount === 1 && nextCount === 0)
) {
requestResizeGhost()
}
setter(nextVisible)
},
[requestResizeGhost, visibleRightPanelCount]
)
// 当前是否可以操作功能按键
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()
}
const requestSubmitTaskConfirm = useCallback(() => {
setSubmitConfirmOpen(true)
}, [])
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,
requestSubmitTaskConfirm,
}))
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>
)}
<Tooltip label={renderMode ? "关闭渲染" : "开启渲染"}>
<ActionIcon
variant="transparent"
c={renderMode ? "var(--mantine-color-text)" : "gray"}
onClick={() => {
setRenderMode(!renderMode)
}}>
<Eclipse style={{ width: 20, height: 20 }} />
</ActionIcon>
</Tooltip>
<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) requestSubmitTaskConfirm()
}}>
</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)
}}>
<SquareDashedMousePointer 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={() => {
toggleRightPanel(showTaskList, setShowTaskList)
}}>
<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={() => {
toggleRightPanel(showGroupList, setShowGroupList)
}}>
<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={() => {
toggleRightPanel(showDescList, setShowDescList)
}}>
<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={() => {
toggleRightPanel(showObjectList, setShowObjectList)
}}>
<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}
/>
)}
{submitConfirmOpen && (
<ConfirmModal
open={submitConfirmOpen}
title="提交任务"
content="是否确定提交当前任务?"
onOk={async () => {
setSubmitConfirmOpen(false)
await commitTaskByActionKey()
}}
onCancel={() => {
setSubmitConfirmOpen(false)
}}
/>
)}
{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)