feat(project): init

This commit is contained in:
2026-02-03 18:05:47 +08:00
commit f37a119eff
188 changed files with 29246 additions and 0 deletions

View File

@@ -0,0 +1,716 @@
"use client"
import {
ActionIcon,
Box,
Button,
Flex,
Group,
ScrollArea,
Text,
TextInput,
} from "@mantine/core"
import { showNotification } from "@mantine/notifications"
import {
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconFlag,
} from "@tabler/icons-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Project } from "../api/project/typing"
import { Task } from "../api/task/typing"
import { LabelState } from "../LabelNossr"
import { useLabelStore } from "../store"
import { usePermissionStore } from "../store/auth"
import { useBottomToolsStore } from "../useBottomToolsStore"
import { useDescToolsStore } from "../useDescToolsStore"
import { useKeyboardStore } from "../useKeyBoardStore"
import { useOtherToolsStore } from "../useOtherToolsStore"
import { usePaperStore } from "../usePaperStore"
import { useTopToolsStore } from "../useTopToolsStore"
import { getSubAttrStatus } from "../util"
import CustomModal from "./CustomModal"
import MetaOperationTool from "./MetaOperationTool"
interface BottomToolsComponentProps {
labelState: LabelState
taskDetail?: Task.DataProps
projectDetail?: Project.DetailResponse
renderPolygons: (
operationId: string,
imageData: Map<string, [number, any[], any, any[]][]> | undefined
) => void | null
}
const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
const { labelState, projectDetail, taskDetail, renderPolygons } = props
const itemRefs = useRef<Record<string, HTMLDivElement | null>>({})
const { label: storeLabel, setLabel, pushStateStack } = useLabelStore()
const {
inputNumber,
setInputNumber,
activeImage,
setActiveImage,
setActiveImageId,
onlyKeyFrame,
setOnlyKeyFrame,
keyFrameData,
setKeyFrameData,
selectedItems,
setSelectedItems,
} = useBottomToolsStore()
const { metaOperation } = useDescToolsStore()
const { ctrl: ctrlKey, shift: shiftKey } = useKeyboardStore()
const [open, setOpen] = useState(false)
const [conflictOpen, setConflictOpen] = useState(false)
const isShowKeyFrame =
projectDetail &&
[5, 6].includes(projectDetail.label_type) &&
Array.from(labelState.keys()).some(
(k) => keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
) &&
onlyKeyFrame
const currentKeys = Array.from(labelState.keys()).filter((k) =>
isShowKeyFrame
? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
: true
)
// 上一帧图片id
const previousFrameKey = useMemo(() => {
let key = ""
const keys = Array.from(labelState.keys()).filter((k) =>
isShowKeyFrame
? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
: true
)
keys.forEach((k, index) => {
if (k === activeImage && index > 0) key = keys[index - 1]
})
return key
}, [activeImage, isShowKeyFrame, keyFrameData, labelState])
useEffect(() => {
const [firstKey] = labelState.keys()
setActiveImage(firstKey)
setInputNumber("1")
setActiveImageId(1)
}, [labelState, setActiveImage, setActiveImageId, setInputNumber])
const scrollToIndex = (index: number) => {
const key = currentKeys[index]
if (key && itemRefs.current[key]) {
itemRefs.current[key]?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
})
}
}
const closeRightContext = useCallback(() => {
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: false,
position: {
top: 0,
left: 0,
},
imageId: useOtherToolsStore.getState().imageId,
operationId: useOtherToolsStore.getState().operationId,
id: useOtherToolsStore.getState().id,
})
}, [])
const getOperationSchema = useCallback(
(operationId: string) => {
return (
projectDetail?.label_schema_list?.find(
(item) => item.category_id === +operationId
) || null
)
},
[projectDetail?.label_schema_list]
)
// child 是否存在不符合要求的子属性
const getOperationSubAttrStatus = useCallback(
(imageId: string) => {
let parentBool = labelState.get(imageId)?.some((operationId) => {
let bool = storeLabel
.get(imageId)
?.get(operationId)
?.some((child) => {
return getSubAttrStatus(
getOperationSchema(operationId),
child[2]?.sub_attributes
)
})
return bool
})
return parentBool
},
[getOperationSchema, labelState, storeLabel]
)
// 获取上一帧和当前帧标注对象数组
const getLabelObjectsData = () => {
let map1 =
storeLabel.get(previousFrameKey) ??
new Map<string, [number, any[], any, any[]][]>()
let arr1 = []
for (const value of map1.values()) arr1.push(...value)
let map2 =
storeLabel.get(activeImage) ??
new Map<string, [number, any[], any, any[]][]>()
let arr2 = []
for (const value of map2.values()) arr2.push(...value)
return [arr1, arr2]
}
// 上一帧是否有文本描述
const prevFramehasTextDesc = () => {
if (projectDetail?.label_type === 5) {
let flag = false
const currentDescData = structuredClone(
useDescToolsStore.getState().descData
)
const prevData1 = currentDescData.get(previousFrameKey) ?? new Map()
flag = prevData1.size ? true : false
if (flag) return true
const currentMetaData = structuredClone(
useDescToolsStore.getState().metaData
)
const prevData2 = currentMetaData.get(previousFrameKey) ?? []
flag = prevData2.length ? true : false
return flag
} else if (projectDetail?.label_type === 6) {
const currentQaData = structuredClone(useDescToolsStore.getState().qaData)
const prevData = currentQaData.get(previousFrameKey) ?? {}
return JSON.stringify(prevData) === "{}" ? false : true
}
return false
}
// 更新数据绘制
const updateDataRender = (
data: Map<string, [number, any[], any, any[]][]>
) => {
if (!renderPolygons) return
const list = usePaperStore.getState().group?.getItems({})
list?.forEach((item) => {
if (item.data.id) {
item.remove()
}
})
useTopToolsStore.getState().objectOperations?.forEach((item) => {
renderPolygons(item.category_id.toString(), data)
})
}
// 大语言标注继承上一帧时数据处理
const handleLLMData = () => {
// LLM
if (projectDetail?.label_type === 5) {
// 文本描述
const currentDescData = structuredClone(
useDescToolsStore.getState().descData
)
const prevData1 = currentDescData.get(previousFrameKey) ?? new Map()
currentDescData.set(activeImage, prevData1)
useDescToolsStore.getState().setDescData(currentDescData)
useDescToolsStore.getState().updateFlag(true)
// 元操作序列
const currentMetaData = structuredClone(
useDescToolsStore.getState().metaData
)
const prevData2 = currentMetaData.get(previousFrameKey) ?? []
currentMetaData.set(activeImage, prevData2)
useDescToolsStore.getState().setMetaData(currentMetaData)
}
// QA
if (projectDetail?.label_type === 6) {
const currentQaData = structuredClone(useDescToolsStore.getState().qaData)
const prevData = currentQaData.get(previousFrameKey) ?? {}
currentQaData.set(activeImage, prevData)
useDescToolsStore.getState().setQaData(currentQaData)
useDescToolsStore.getState().updateFlag(true)
}
}
// 继承上一帧的数据
const handleCopyData = () => {
// 图形绘制
const [perviousData] = getLabelObjectsData()
let map =
storeLabel.get(activeImage) ??
new Map<string, [number, any[], any, any[]][]>()
perviousData.forEach((item) => {
const id = item[2].category_id.toString()
if (map.has(id)) {
let data = map.get(id)
data!.push(item)
map.set(id, data as any)
} else {
map.set(id, [item])
}
})
let updateData = new Map()
for (const [key, value] of storeLabel) {
if (key === activeImage) updateData.set(key, map)
else updateData.set(key, value)
}
if (!storeLabel.has(activeImage)) updateData.set(activeImage, map)
updateDataRender(updateData.get(activeImage)!)
setLabel(updateData)
pushStateStack(updateData)
handleLLMData()
setOpen(false)
}
const handleCopyConflictData = (flag: boolean) => {
const [perviousData, nowData] = getLabelObjectsData()
let map = new Map<string, [number, any[], any, any[]][]>()
let arr = flag
? [
...perviousData,
...nowData.filter(
(item) => !perviousData.map((p) => p[0]).includes(item[0])
),
]
: [
...perviousData.filter(
(item) => !nowData.map((d) => d[0]).includes(item[0])
),
...nowData,
]
arr.forEach((item) => {
const id = item[2].category_id.toString()
if (map.has(id)) {
let data = map.get(id)
data!.push(item)
map.set(id, data as any)
} else {
map.set(id, [item])
}
})
let updateData = new Map()
for (const [key, value] of storeLabel) {
if (key === activeImage) updateData.set(key, map)
else updateData.set(key, value)
}
updateDataRender(updateData.get(activeImage)!)
setLabel(updateData)
pushStateStack(updateData)
handleLLMData()
setConflictOpen(false)
}
return (
<Box w="100%" h="100%">
{projectDetail?.label_type === 5 && metaOperation ? (
<Flex
h={48}
w="100%"
align="center"
style={{
borderTop:
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Box px="md" fw={600}>
</Box>
<Box
style={{
maxWidth: "calc(100% - 98px)",
overflowX: "auto",
height: "100%",
}}>
<MetaOperationTool />
</Box>
</Flex>
) : null}
<Box
h={28}
style={{
border:
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Flex justify="space-between" align="center" h="100%">
<Box flex={1} />
<Flex flex={1} align="center" justify="center" gap="xs">
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() => {
scrollToIndex(0)
currentKeys.length && setActiveImage(currentKeys[0])
setInputNumber("1")
setActiveImageId(1)
closeRightContext()
}}>
<IconChevronsLeft size={16} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() => {
if (inputNumber !== "1") {
let newActiveIndex = +inputNumber - 1
setInputNumber(newActiveIndex.toString())
setActiveImageId(newActiveIndex)
scrollToIndex(newActiveIndex - 1)
setActiveImage(currentKeys?.[newActiveIndex - 1])
closeRightContext()
}
}}>
<IconChevronLeft size={16} />
</ActionIcon>
<TextInput
w={42}
size="compact-xs"
radius={"sm"}
styles={{
input: {
height: 20,
fontSize: "14px",
textAlign: "center",
},
}}
value={inputNumber}
onChange={(e) => {
setInputNumber(e.target.value)
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
scrollToIndex(+inputNumber - 1)
setActiveImage(currentKeys?.[+inputNumber - 1])
setActiveImageId(+inputNumber)
closeRightContext()
}
}}
/>
<Text size="xs">/ {currentKeys.length}</Text>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() => {
if (+inputNumber < currentKeys.length) {
let newActiveIndex = +inputNumber + 1
setInputNumber(newActiveIndex.toString())
setActiveImageId(newActiveIndex)
scrollToIndex(newActiveIndex - 1)
setActiveImage(currentKeys?.[newActiveIndex - 1])
closeRightContext()
}
}}>
<IconChevronRight size={16} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() => {
scrollToIndex(currentKeys.length - 1)
setInputNumber(currentKeys.length.toString())
setActiveImageId(currentKeys.length)
setActiveImage(currentKeys?.[currentKeys.length - 1])
closeRightContext()
}}>
<IconChevronsRight size={16} />
</ActionIcon>
</Flex>
<Flex flex={1} justify="flex-end" align="center" gap="xs" pr="xs">
{projectDetail && [5, 6].includes(projectDetail.label_type) ? (
<>
{isShowKeyFrame ? (
<Button
variant="subtle"
size="compact-xs"
radius={"sm"}
onClick={() => {
const [firstKey] = labelState.keys()
setActiveImage(firstKey)
setInputNumber("1")
setActiveImageId(1)
setOnlyKeyFrame(false)
}}>
</Button>
) : (
<Button
variant="subtle"
size="compact-xs"
radius={"sm"}
onClick={() => {
let flag = false
keyFrameData.entries().forEach(([key, value]) => {
if (labelState.has(key) && value?.key_frame) flag = true
})
if (!flag) {
showNotification({
title: "警告",
message: "当前任务暂未设定关键帧",
color: "yellow",
})
return
}
const filterKeys = Array.from(labelState.keys()).filter(
(key) =>
keyFrameData.has(key) &&
keyFrameData.get(key)?.key_frame
)
setActiveImage(filterKeys[0])
setInputNumber("1")
setActiveImageId(1)
setOnlyKeyFrame(true)
}}>
</Button>
)}
{keyFrameData.has(activeImage) &&
keyFrameData.get(activeImage)?.key_frame ? (
<Button
variant="subtle"
size="compact-xs"
radius={"sm"}
disabled={
!taskDetail ||
![2, 4, 6].includes(taskDetail.label_status) ||
useTopToolsStore.getState().isView
}
onClick={() => {
if (!taskDetail) return
const data = keyFrameData.get(activeImage)!
data.key_frame = false
if (taskDetail.label_status === 2) {
data.label1_ts = 0
data.label1_uid = 0
} else if (taskDetail.label_status === 4) {
data.review1_ts = 0
data.review1_uid = 0
} else if (taskDetail.label_status === 6) {
data.review2_ts = 0
data.review2_uid = 0
}
setKeyFrameData(activeImage, data)
}}>
</Button>
) : (
<Button
variant="subtle"
size="compact-xs"
radius={"sm"}
disabled={
!taskDetail ||
![2, 4, 6].includes(taskDetail.label_status) ||
useTopToolsStore.getState().isView
}
onClick={() => {
if (!taskDetail) return
const data = keyFrameData.get(activeImage) || {
key_frame: false,
label1_ts: 0,
label1_uid: 0,
review1_ts: 0,
review1_uid: 0,
review2_ts: 0,
review2_uid: 0,
}
data.key_frame = true
if (taskDetail.label_status === 2) {
data.label1_ts =
useLabelStore.getState().labelTime.label
data.label1_uid = usePermissionStore.getState().user_id
} else if (taskDetail.label_status === 4) {
data.review1_ts =
useLabelStore.getState().labelTime.review1
data.review1_uid = usePermissionStore.getState().user_id
} else if (taskDetail.label_status === 6) {
data.review2_ts =
useLabelStore.getState().labelTime.review2
data.review2_uid = usePermissionStore.getState().user_id
}
setKeyFrameData(activeImage, data)
}}>
</Button>
)}
</>
) : null}
<Button
variant="subtle"
size="compact-xs"
radius={"sm"}
disabled={
(!getLabelObjectsData()[0].length && !prevFramehasTextDesc()) ||
useTopToolsStore.getState().isView
}
onClick={() => {
const [perviousData, nowData] = getLabelObjectsData()
// check merge conflict
if (!nowData.length) {
setOpen(true)
return
}
let flag = false
const perviousIds = perviousData.map((item) => item[0])
const nowIds = nowData.map((item) => item[0])
perviousIds.forEach((id) => {
if (nowIds.includes(id)) flag = true
})
if (flag) setConflictOpen(true)
else setOpen(true)
}}>
</Button>
</Flex>
</Flex>
</Box>
<Box
h={
projectDetail?.label_type === 5 && metaOperation
? "calc(100% - 76px)"
: "calc(100% - 28px)"
}
style={{
border:
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<ScrollArea.Autosize
mah="100%"
h={"100%"}
w="100%"
scrollbars="x"
style={{ display: "flex", alignItems: "center" }}>
<Flex gap={0} w="max-content" h="100%" align="center">
{currentKeys.map((key, index) => {
const isActive = activeImage === key
const isSelected =
selectedItems.length && selectedItems.includes(key)
const isHighlight = isActive || isSelected
let textColor = "inherit"
if (getOperationSubAttrStatus(key)) {
textColor = "red"
} else if (isHighlight) {
textColor = "white"
}
return (
<Box
key={key}
w={120}
h="100%"
p={6}
ref={(el) => {
itemRefs.current[key] = el
}}>
<Box
style={{
border: isHighlight
? "1px solid var(--mantine-color-blue-6)"
: "1px solid var(--mantine-color-gray-3)",
borderRadius: "6px",
backgroundColor: isHighlight
? "var(--mantine-color-blue-6)"
: undefined,
color: isHighlight ? "white" : undefined,
cursor: "pointer",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={(e) => {
if (e.shiftKey) {
e.preventDefault()
}
if (ctrlKey || shiftKey) {
setSelectedItems(key, ctrlKey, shiftKey)
} else {
setInputNumber(index + 1 + "")
setActiveImage(key)
setActiveImageId(index + 1)
setSelectedItems("", false, false)
}
closeRightContext()
}}>
{keyFrameData.has(key) &&
keyFrameData.get(key)?.key_frame ? (
<Box
pos="absolute"
top={0}
right={8}
c={isHighlight ? "white" : "gray"}>
<IconFlag size={16} />
</Box>
) : null}
<Text fw={600} size="lg" c={textColor}>
{index + 1}
</Text>
</Box>
</Box>
)
})}
</Flex>
</ScrollArea.Autosize>
</Box>
<CustomModal
title="提示"
open={open}
centered
onCancel={() => {
setOpen(false)
}}
onOk={handleCopyData}>
</CustomModal>
<CustomModal
title="提示"
open={conflictOpen}
centered
onCancel={() => {
setConflictOpen(false)
}}
footer={
<Group justify="flex-end" gap="xs" mt="md">
<Button
variant="default"
onClick={() => {
setConflictOpen(false)
}}>
</Button>
<Button
color="red"
onClick={() => {
handleCopyConflictData(false)
}}>
</Button>
<Button
onClick={() => {
handleCopyConflictData(true)
}}>
</Button>
</Group>
}>
</CustomModal>
</Box>
)
}
export default BottomTools