import { useCallback, useEffect, useMemo, useState } from 'react'; import styled, { css } from 'styled-components'; import { AttributeTree, CollapseWrapper, AttributeTreeWrapper, EllipsisText, uid, Tooltip, FlexLayout, } from '@labelu/components-react'; import { useTranslation } from '@labelu/i18n'; import type { EnumerableAttribute, GlobalAnnotationType, TagAnnotationEntity, TextAnnotationEntity, TextAttribute, } from '@labelu/interface'; import { DEFAULT_LABEL_COLOR, DEFAULT_LABEL_TEXT } from '@labelu/image'; import { ReactComponent as DeleteIcon } from '@/assets/icons/delete.svg'; import type { AnnotationDataInUI, AnnotationWithTool, GlobalAnnotation } from '@/context/annotation.context'; import { useAnnotationCtx } from '@/context/annotation.context'; import { useTool } from '@/context/tool.context'; import { useSample } from '@/context/sample.context'; import { tooltipStyle } from '@/Toolbar'; import AsideAttributeItem, { AttributeAction, Header } from './AsideAttributeItem'; const Wrapper = styled.div<{ collapsed: boolean }>` flex-shrink: 0; height: calc(100vh - var(--offset-top)); overflow: auto; display: flex; flex-direction: column; border-left: 1px solid rgba(235, 236, 240, 1); ${({ collapsed }) => (collapsed ? 'width: 0;' : 'width: 280px;')} `; const AttributeHeaderItem = styled.div<{ active: boolean }>` height: 100%; display: flex; padding: 0 0.5rem; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; border-bottom: 2px solid transparent; ${({ active }) => active && css` color: var(--color-primary); border-bottom: 2px solid var(--color-primary); `} &:hover { color: var(--color-primary); } .attribute__status { font-size: 12px; color: #999; } `; const TabHeader = styled.div` display: flex; flex-shrink: 0; width: 100%; align-items: center; justify-content: space-around; height: 68px; border-bottom: #e5e5e5 1px solid; `; const AsideWrapper = styled.div``; const Content = styled.div<{ activeKey: HeaderType }>` padding: 1rem 0; flex: 1 auto; min-height: 0; overflow: auto; & > ${AttributeTreeWrapper} { display: ${({ activeKey }) => (activeKey === 'global' ? 'block' : 'none')}; } & > ${CollapseWrapper as any} { display: ${({ activeKey }) => (activeKey === 'label' ? 'block' : 'none')}; } `; const Footer = styled.div` height: 48px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 14px; border-top: 1px solid rgba(235, 236, 240, 1); cursor: pointer; &:hover { color: red; } /* disabled */ &[aria-disabled='true'] { color: #999; cursor: not-allowed; } `; type HeaderType = 'global' | 'label'; interface ConfirmProps { title: string; onConfirm: () => void; onCancel?: () => void; } const Button = styled.button<{ primary?: boolean }>` border: 0; padding: 0.25rem 0.5rem; border-radius: 3px; cursor: pointer; ${({ primary }) => primary && css` background-color: var(--color-primary); color: #fff; `} `; function Confirm({ title, onConfirm, onCancel }: ConfirmProps) { const { t } = useTranslation(); return ( {title} ); } interface ClearActionProps { onClear: () => void; disabled?: boolean; } function ClearAction({ onClear, disabled }: ClearActionProps) { const [open, setOpen] = useState(false); // @ts-ignore const { t } = useTranslation(); const handleConfirm = () => { onClear?.(); setOpen(false); }; return ( { setOpen(false); }} /> } placement="top" > ); } export function AttributePanel() { const { engine, globalToolConfig, config, labelMapping, preLabelMapping } = useTool(); const { currentSample } = useSample(); const { sortedImageAnnotations, annotationsWithGlobal, allAnnotationsMapping, selectedAnnotation, preAnnotationsWithGlobal, onAnnotationsChange, onAnnotationClear, disabled, } = useAnnotationCtx(); const [collapsed, setCollapsed] = useState(false); const [modified, setModified] = useState(false); const globalAnnotations = useMemo(() => { return Object.values(annotationsWithGlobal).filter((item) => ['text', 'tag'].includes((item as GlobalAnnotation).type), ) as GlobalAnnotation[]; }, [annotationsWithGlobal]); // @ts-ignore const { t } = useTranslation(); const { imageAnnotationsGroup, defaultActiveKeys } = useMemo(() => { const imageAnnotationsGroupByLabel = new Map(); for (const item of sortedImageAnnotations) { const label = item.label ?? 'noneAttribute'; if (!imageAnnotationsGroupByLabel.has(label)) { imageAnnotationsGroupByLabel.set(label, []); } imageAnnotationsGroupByLabel?.get(label)?.push(item); } return { imageAnnotationsGroup: imageAnnotationsGroupByLabel, defaultActiveKeys: Array.from(imageAnnotationsGroupByLabel.keys()), }; }, [sortedImageAnnotations]); const globals = useMemo(() => { const _globals: (TextAttribute | EnumerableAttribute)[] = []; if (globalToolConfig.tag) { _globals.push(...globalToolConfig.tag); } if (globalToolConfig.text) { _globals.push(...globalToolConfig.text); } // 预标注的文本和分类需要跟用户配置合并且根据其value去重 Object.values(preLabelMapping?.tag ?? {})?.forEach((item) => { if (!_globals.some((innerItem) => innerItem.value === item.value)) { _globals.push({ ...item, disabled: true } as EnumerableAttribute); } }); Object.values(preLabelMapping?.text ?? {})?.forEach((item) => { if (!_globals.some((innerItem) => innerItem.value === item.value)) { _globals.push({ ...item, disabled: true } as TextAttribute); } }); return _globals; }, [globalToolConfig.tag, globalToolConfig.text, preLabelMapping?.tag, preLabelMapping?.text]); const flatGlobalTagAnnotations = useMemo(() => { const result = globalAnnotations; if (globalAnnotations.length === 0 && !modified) { [preAnnotationsWithGlobal?.tag, preAnnotationsWithGlobal?.text].forEach((values) => { if (values) { result.push(...(values as GlobalAnnotation[])); } }); } return result; }, [globalAnnotations, preAnnotationsWithGlobal?.tag, preAnnotationsWithGlobal?.text, modified]); const titles = useMemo(() => { const _titles = []; // 将文本描述和标签分类合并成全局配置 if ( globalToolConfig?.tag || globalToolConfig?.text || Object.keys(preLabelMapping.tag ?? {}).length || Object.keys(preLabelMapping.text ?? {}).length ) { let isCompleted = false; const globalAnnotationMapping: Record = {}; globalAnnotations.forEach((item) => { const _key = Object.keys(item.value)[0]; globalAnnotationMapping[_key] = item; }); /** 如果所有的文本描述都是必填的,那么只要有一个不存在,那么就是未完成 */ isCompleted = [...(globalToolConfig.text ?? []), ...(globalToolConfig.tag ?? [])] .filter((item) => item.required) .every((item) => globalAnnotationMapping[item.value]?.value?.[item.value]); _titles.push({ title: t('global'), key: 'global' as const, subtitle: isCompleted ? t('done') : t('undone'), }); } if (config?.line || config?.point || config?.polygon || config?.rect || config?.cuboid || config?.relation) { _titles.push({ title: t('labels'), key: 'label' as const, subtitle: `${sortedImageAnnotations.length}${t('markCount')}`, }); } return _titles; }, [ t, globalToolConfig.tag, globalToolConfig.text, preLabelMapping.tag, preLabelMapping.text, config?.line, config?.point, config?.polygon, config?.rect, config?.cuboid, config?.relation, globalAnnotations, sortedImageAnnotations.length, ]); const [activeKey, setActiveKey] = useState(globals.length === 0 ? 'label' : 'global'); useEffect(() => { const handleCollapse = () => { setCollapsed((prev) => !prev); }; document.addEventListener('attribute-collapse', handleCollapse as EventListener); return () => { document.removeEventListener('attribute-collapse', handleCollapse as EventListener); }; }, []); const handleOnChange = useCallback( ( changedValues: Partial>>, values: Record>, ) => { const newAnnotations: AnnotationWithTool[] = []; const existAnnotations: GlobalAnnotation[] = []; for (const type of Object.keys(changedValues)) { const annotationType = type as GlobalAnnotationType; const changedInnerValues = changedValues[type as GlobalAnnotationType] ?? {}; const allInnerValues = values[annotationType] ?? []; for (const field of Object.keys(changedInnerValues)) { const item = allInnerValues[field]; if (item.id && item.id in allAnnotationsMapping) { existAnnotations.push(item); } else { newAnnotations.push({ id: item.id || uid(), type: annotationType, tool: annotationType, value: item.value, }); } } } onAnnotationsChange([...existAnnotations, ...newAnnotations] as AnnotationWithTool[]); }, [onAnnotationsChange, allAnnotationsMapping], ); const handleClear = () => { if (!currentSample) { return; } setModified(true); onAnnotationClear(); if (activeKey === 'label') { engine?.clearData(); } }; const collapseItems = useMemo( () => Array.from(imageAnnotationsGroup).map(([label, _annotations]) => { const found = labelMapping[_annotations[0].tool]?.[label] ?? preLabelMapping?.[_annotations[0].tool]?.[label]; const labelText = found ? found?.key ?? label : label; return { label: (
{labelText}
), key: label, children: ( {_annotations.map((item) => { const labelOfAnnotation = labelMapping[item.tool]?.[label] ?? preLabelMapping?.[item.tool]?.[label]; return ( ); })} ), }; }), [imageAnnotationsGroup, labelMapping, preLabelMapping, selectedAnnotation?.id], ); return ( {titles.map((item) => { return ( setActiveKey(item.key)} active={item.key === activeKey} key={item.key} className="attribute-header__item" >
{item.title}
{item.subtitle}
); })}
); }