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: (
),
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}
);
})}
);
}