"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, IconPlayerTrackNext, IconPlayerTrackNextFilled, IconPlayerTrackPrev, IconPlayerTrackPrevFilled, } 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 { useLabelTimeStore } from "../useTimerStore" import { useTopToolsStore } from "../useTopToolsStore" import { getSubAttrStatus } from "../util" import { safeClone } from "../utils/clone" import CustomModal from "./CustomModal" import MetaOperationTool from "./MetaOperationTool" interface BottomToolsComponentProps { labelState: LabelState taskDetail?: Task.DataProps projectDetail?: Project.DetailResponse renderPolygons: ( operationId: string, imageData: Map | undefined ) => void | null } type PlaybackDirection = "backward" | "forward" const BottomTools: React.FC = (props) => { const { labelState, projectDetail, taskDetail, renderPolygons } = props const itemRefs = useRef>({}) const storeLabel = useLabelStore((state) => state.label) const setLabel = useLabelStore((state) => state.setLabel) const pushStateStack = useLabelStore((state) => state.pushStateStack) const { inputNumber, setInputNumber, activeImage, pendingImage, frameSwitchStatus, setActiveImage, setActiveImageId, requestFrameSwitch, 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 [playDirection, setPlayDirection] = useState( null ) const allKeys = useMemo(() => Array.from(labelState.keys()), [labelState]) const isShowKeyFrame = projectDetail && [5, 6].includes(projectDetail.label_type) && allKeys.some( (k) => keyFrameData.has(k) && keyFrameData.get(k)?.key_frame ) && onlyKeyFrame const currentKeys = useMemo( () => allKeys.filter((k) => isShowKeyFrame ? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame : true ), [allKeys, isShowKeyFrame, keyFrameData] ) // 上一帧图片id const previousFrameKey = useMemo(() => { const currentIndex = currentKeys.findIndex((k) => k === activeImage) return currentIndex > 0 ? currentKeys[currentIndex - 1] : "" }, [activeImage, currentKeys]) useEffect(() => { if (!currentKeys.length) { setActiveImage("") setInputNumber("") setActiveImageId(0) return } const currentIndex = activeImage ? currentKeys.findIndex((k) => k === activeImage) : -1 if (currentIndex >= 0) { setInputNumber(String(currentIndex + 1)) const absoluteIndex = allKeys.findIndex((k) => k === activeImage) setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 0) return } const firstKey = currentKeys[0] const absoluteIndex = allKeys.findIndex((k) => k === firstKey) setActiveImage(firstKey) setInputNumber("1") setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 1) }, [ activeImage, allKeys, currentKeys, setActiveImage, setActiveImageId, setInputNumber, ]) const scrollToIndex = useCallback( (index: number) => { const key = currentKeys[index] if (key && itemRefs.current[key]) { itemRefs.current[key]?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }) } }, [currentKeys] ) 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 goToVisibleIndex = useCallback( (index: number) => { if (index < 0 || index >= currentKeys.length) return false const nextImage = currentKeys[index] if (!nextImage) return false scrollToIndex(index) if ( frameSwitchStatus === "preparing" && pendingImage && pendingImage === nextImage ) { closeRightContext() return true } if (nextImage !== activeImage) { requestFrameSwitch(nextImage) } closeRightContext() return true }, [ activeImage, closeRightContext, currentKeys, frameSwitchStatus, pendingImage, requestFrameSwitch, scrollToIndex, ] ) const stepVisibleImage = useCallback( (direction: PlaybackDirection) => { const currentIndex = currentKeys.findIndex((k) => k === activeImage) if (currentIndex < 0) return false const nextIndex = direction === "backward" ? currentIndex - 1 : currentIndex + 1 return goToVisibleIndex(nextIndex) }, [activeImage, currentKeys, goToVisibleIndex] ) const togglePlayback = useCallback( (direction: PlaybackDirection) => { if (currentKeys.length < 2) return setPlayDirection((current) => (current === direction ? null : direction)) }, [currentKeys.length] ) useEffect(() => { if (!playDirection) return if (currentKeys.length < 2) { setPlayDirection(null) return } const timer = window.setInterval(() => { const moved = stepVisibleImage(playDirection) if (!moved) { setPlayDirection(null) } }, 1000) return () => { window.clearInterval(timer) } }, [currentKeys.length, playDirection, stepVisibleImage]) 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() let arr1 = [] for (const value of map1.values()) arr1.push(...value) let map2 = storeLabel.get(activeImage) ?? new Map() 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 = safeClone(useDescToolsStore.getState().descData) const prevData1 = currentDescData.get(previousFrameKey) ?? new Map() flag = prevData1.size ? true : false if (flag) return true const currentMetaData = safeClone(useDescToolsStore.getState().metaData) const prevData2 = currentMetaData.get(previousFrameKey) ?? [] flag = prevData2.length ? true : false return flag } else if (projectDetail?.label_type === 6) { const currentQaData = safeClone(useDescToolsStore.getState().qaData) const prevData = currentQaData.get(previousFrameKey) ?? {} return JSON.stringify(prevData) === "{}" ? false : true } return false } // 更新数据绘制 const updateDataRender = ( data: Map ) => { 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 = safeClone(useDescToolsStore.getState().descData) const prevData1 = currentDescData.get(previousFrameKey) ?? new Map() currentDescData.set(activeImage, prevData1) useDescToolsStore.getState().setDescData(currentDescData) useDescToolsStore.getState().updateFlag(true) // 元操作序列 const currentMetaData = safeClone(useDescToolsStore.getState().metaData) const prevData2 = currentMetaData.get(previousFrameKey) ?? [] currentMetaData.set(activeImage, prevData2) useDescToolsStore.getState().setMetaData(currentMetaData) } // QA if (projectDetail?.label_type === 6) { const currentQaData = safeClone(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() 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() 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 ( {projectDetail?.label_type === 5 && metaOperation ? ( 元操作序列 ) : null} { goToVisibleIndex(0) }}> { togglePlayback("backward") }}> {playDirection === "backward" ? ( ) : ( )} { stepVisibleImage("backward") }}> { setInputNumber(e.target.value) }} onKeyDown={(e) => { if (e.key === "Enter") { const targetIndex = Number(inputNumber) if ( Number.isInteger(targetIndex) && targetIndex >= 1 && targetIndex <= currentKeys.length ) { goToVisibleIndex(targetIndex - 1) } else { const currentIndex = currentKeys.findIndex( (key) => key === activeImage ) setInputNumber( currentIndex >= 0 ? String(currentIndex + 1) : "" ) } } }} /> / {currentKeys.length} { stepVisibleImage("forward") }}> { togglePlayback("forward") }}> {playDirection === "forward" ? ( ) : ( )} { goToVisibleIndex(currentKeys.length - 1) }}> {projectDetail && [5, 6].includes(projectDetail.label_type) ? ( <> {isShowKeyFrame ? ( ) : ( )} {keyFrameData.has(activeImage) && keyFrameData.get(activeImage)?.key_frame ? ( ) : ( )} ) : null} {currentKeys.map((key, index) => { const isActive = activeImage === key const isPending = frameSwitchStatus === "preparing" && pendingImage === key && !isActive 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 ( { itemRefs.current[key] = el }}> { if (e.shiftKey) { e.preventDefault() } if (ctrlKey || shiftKey) { setSelectedItems(key, ctrlKey, shiftKey) } else { goToVisibleIndex(index) setSelectedItems("", false, false) } closeRightContext() }}> {keyFrameData.has(key) && keyFrameData.get(key)?.key_frame ? ( ) : null} {index + 1} ) })} { setOpen(false) }} onOk={handleCopyData}> 确实要复制上一帧数据吗? { setConflictOpen(false) }} footer={ }> 与上一帧数据存在冲突,是否要复制上一帧数据并覆盖吗? ) } export default BottomTools